SwiftBrowser docs
View as MarkdownEdit on GitHub

Contributing#

How to build, test, deploy and release SwiftBrowser itself. If you want to run your own app in the browser instead, start with docs/getting-started.md.

Local development#

You need Swift 6.4.0, its Swift SDK for WebAssembly, and Node 22. On Linux, swiftly installs the toolchain:

swiftly install 6.4.0 --use
swift sdk install \
  https://download.swift.org/swift-6.4.0-release/wasm-sdk/swift-6.4.0-RELEASE/swift-6.4.0-RELEASE_wasm.artifactbundle.tar.gz \
  --checksum f07b7be3c586d92d7a07051fc6d303b87ebea67eadc40640ba59d5a8b79aa86d

Then:

swift test                      # unit tests for the shim and reconciler (native Linux)
npm ci && npm run build:packages    # the CLI, which builds the examples (and the web renderer's Node side)
scripts/install-binaryen.sh     # once: wasm-opt, which shrinks the modules by about half
scripts/build-wasm.sh           # builds every Examples/<App> through the CLI into Web/public/<App>.wasm
scripts/snapshot-test.sh        # runs them headless (events + virtual clock, en_US) and diffs the op streams (--update to accept)
cd Web && npm run dev           # serves the apps in an iPhone frame at http://localhost:5173

Each example's __snapshots__/events.txt is the script the snapshot replays, one event per line, naming elements by what they show rather than by id: tap:"Add", text:"Name":Paul, select:"Sort":"Rating", back. A selector that matches nothing or more than one element fails the run (docs/ops-protocol.md).

The dev server shows Counter by default and any other example at ?app=Todos. It watches Sources/ and Examples/, rebuilds the Wasm module on save, and reloads the page with the previous @State values restored where the view tree still matches (see the state snapshot section of docs/ops-protocol.md).

scripts/build-wasm.sh Todos     # one app only

Do not build the package on macOS: the shim module is named SwiftUI and collides with the system framework. On a Mac, open the example in Xcode as plain SwiftUI, or let CI's ios-source-compat job type-check it for you.

Release modules go through Binaryen's wasm-opt -Oz --strip-debug when scripts/install-binaryen.sh has been run (CI always does): a small example drops from 35 MB to 17.4 MB (4.5 MB with brotli). The dev server's hot rebuilds skip the step to stay fast and keep function names in stack traces.

Deploying the examples#

The deploy job of .github/workflows/ci.yml publishes the web renderer with every example to Cloudflare Pages, so the apps can be opened in a real mobile browser. It takes the modules the examples matrix built, so it compiles no Swift. Pushes to main deploy the production site; every pull request gets a preview deployment on a branch alias and a comment with the links. The deployed site has a landing page at / that lists the apps with QR codes, and runs each app at /app/?app=<Name>. On a phone the app page drops the fake iPhone frame and uses the real screen and safe areas; in Safari, Share → Add to Home Screen runs it full screen.

The job needs two repository secrets (Settings → Secrets and variables → Actions); without them it still builds the site, and skips the deploy:

Secret Value
CLOUDFLARE_API_TOKEN an API token created at dash.cloudflare.com → My Profile → API Tokens with the Cloudflare Pages: Edit permission
CLOUDFLARE_ACCOUNT_ID the account id from the dashboard URL (dash.cloudflare.com/<account id>)

The Pages project is named swiftbrowser (CLOUDFLARE_PAGES_PROJECT in the workflow); the workflow creates it on the first run if it does not exist. The site is then at https://swiftbrowser.pages.dev, and previews at https://<branch>.swiftbrowser.pages.dev.

Deploying the docs#

The documentation site in DocsSite/ is built from the Markdown in this repository (the root README, docs/, the package READMEs and this file) by .github/workflows/docs.yml, which deploys it to the Cloudflare Pages project swiftbrowser-docs (https://swiftbrowser-docs.pages.dev) with the same two secrets. Edit the Markdown, not the site; cd DocsSite && npm install && npm run dev previews it at http://localhost:5174.

Releasing to npm#

.github/workflows/publish.yml publishes the four packages (@swiftbrowser/cli, @swiftbrowser/web, @swiftbrowser/xcodeproj, @swiftbrowser/swift) together, at one version, with npm provenance while the repository is public (npm rejects provenance from a private one, so a private repository publishes without it). Start it from Actions → Publish to npm → Run workflow with the version to release (0.2.0, or patch / minor / major). The run bumps every manifest and the lockfile (npm run set-version <version> does the same locally), runs the typecheck and unit tests, builds, commits Release vX.Y.Z to the branch it ran on, tags it, publishes the packages in dependency order and creates the GitHub release with generated notes. Tick dry run to do all of that except the push and the publish. Pushing a vX.Y.Z tag by hand also publishes, if the manifests already carry that version.

Publishing authenticates in one of two ways:

Option Setup
NPM_TOKEN secret Settings → Secrets and variables → Actions: a granular access token with read and write access to the @swiftbrowser packages, allowed to bypass 2FA
Trusted publishing On npmjs.com, each package's Settings → Trusted Publisher: this repository, workflow publish.yml. No secret; the workflow's OIDC token is enough

A release that failed half-way is resumed by running the workflow again with the same version: the job publishes from the commit the existing tag points at, packages already on the registry are skipped and an existing release is reused.

Continuous integration#

.github/workflows/ci.yml runs these jobs on every push and pull request:

  • swift-tests: native Linux build and swift test.
  • hello-wasm: builds HelloWasm for Wasm and runs it in wasmtime, a canary for the SDK install.
  • examples: a matrix with one runner per example (the list is in the workflow; the wasm job fails when an Examples/ directory is missing from it), each building its module through the CLI and uploading it. Serially the seven took 13 minutes; in parallel the stage takes one build.
  • wasm: downloads the modules, runs every example's headless snapshot under Node's WASI, type-checks and unit-tests the workspaces, builds the web renderer and runs the Playwright tests against the real modules in Chromium. Needs no Swift.
  • deploy: builds the site from the matrix's modules and publishes it to Cloudflare Pages (see "Deploying the examples").
  • cli: the CLI against Hacking with SwiftUI's Xcode projects.
  • ios-source-compat: type-checks every example against Apple's SwiftUI with the iOS simulator SDK on macOS.

.github/workflows/docs.yml runs when Markdown or DocsSite/ changes: it type-checks, tests and builds the docs site, and deploys it (see "Deploying the docs").

Layout#

Sources/SwiftUI/        the shim: API/ (public SwiftUI surface) and Runtime/ (state, reconciler, ops)
Sources/HelloWasm/      smallest possible Swift-on-Wasm program
Examples/Counter/       the first app and its headless snapshot
Examples/Todos/         navigation, lists, input and @Observable in one app
Examples/Gallery/       layout, animation, sheets and typography screens
Examples/Settings/      tabs, forms, pickers, path navigation, tasks and GeometryReader
Examples/FoodTruck/     Apple's Food Truck sample, trimmed: Foundation, asset catalogs, ObservableObject
Sources/FoundationBridge/ what apps need from Foundation that corelibs lacks on WASI: Timer, UserDefaults, Bundle, URLSession, AsyncImage, Locale
Sources/CoreImage/      Core Image's built-in filters and QR code generator, as recipes the page draws
Examples/HackingWithSwift/ five Hacking with SwiftUI projects: gestures, swipe actions, edit mode, alerts, controls
Examples/CupcakeCorner/ Hacking with SwiftUI project 10: AsyncImage and URLSession over the browser's fetch
Examples/PhotoPicker/   PhotosUI's PhotosPicker over the browser's file chooser
Examples/Filters/       Core Image: Instafilter's filters on a photo, Hot Prospects' QR code
Examples/HotProspects/  Hacking with SwiftUI project 16 (no Me tab): the CodeScanner and UserNotifications stand-ins
Web/                    Vite + TypeScript renderer and device frame
DocsSite/               the documentation site, built from the repository's Markdown
Tests/SwiftUITests/     swift-testing suite for the shim
scripts/                build-wasm.sh, snapshot-test.sh, run-wasi.mjs, set-version.mjs (release versions)
docs/                   getting started, compatibility, architecture, the ops protocol and the history