@swiftbrowser/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +330 -64
  2. package/dist/assetSymbols.d.ts +25 -0
  3. package/dist/assetSymbols.js +218 -0
  4. package/dist/assetSymbols.js.map +1 -0
  5. package/dist/build.d.ts +17 -2
  6. package/dist/build.js +40 -13
  7. package/dist/build.js.map +1 -1
  8. package/dist/buildSettings.d.ts +15 -0
  9. package/dist/buildSettings.js +74 -0
  10. package/dist/buildSettings.js.map +1 -0
  11. package/dist/cli.d.ts +6 -0
  12. package/dist/cli.js +60 -11
  13. package/dist/cli.js.map +1 -1
  14. package/dist/commands/build.js +6 -5
  15. package/dist/commands/build.js.map +1 -1
  16. package/dist/commands/check.js +2 -2
  17. package/dist/commands/check.js.map +1 -1
  18. package/dist/commands/dev.d.ts +5 -2
  19. package/dist/commands/dev.js +19 -13
  20. package/dist/commands/dev.js.map +1 -1
  21. package/dist/commands/init.d.ts +18 -0
  22. package/dist/commands/init.js +57 -0
  23. package/dist/commands/init.js.map +1 -0
  24. package/dist/commands/kit.d.ts +3 -0
  25. package/dist/commands/kit.js +21 -0
  26. package/dist/commands/kit.js.map +1 -0
  27. package/dist/commands/shared.d.ts +25 -4
  28. package/dist/commands/shared.js +104 -10
  29. package/dist/commands/shared.js.map +1 -1
  30. package/dist/compat.d.ts +44 -6
  31. package/dist/compat.js +64 -36
  32. package/dist/compat.js.map +1 -1
  33. package/dist/config.d.ts +48 -10
  34. package/dist/config.js +64 -8
  35. package/dist/config.js.map +1 -1
  36. package/dist/coreml.d.ts +194 -0
  37. package/dist/coreml.js +626 -0
  38. package/dist/coreml.js.map +1 -0
  39. package/dist/icu.d.ts +17 -4
  40. package/dist/icu.js +32 -18
  41. package/dist/icu.js.map +1 -1
  42. package/dist/kit.d.ts +133 -0
  43. package/dist/kit.js +403 -0
  44. package/dist/kit.js.map +1 -0
  45. package/dist/objc.d.ts +31 -0
  46. package/dist/objc.js +319 -0
  47. package/dist/objc.js.map +1 -0
  48. package/dist/packagePatches.d.ts +24 -0
  49. package/dist/packagePatches.js +107 -0
  50. package/dist/packagePatches.js.map +1 -0
  51. package/dist/packages.d.ts +59 -0
  52. package/dist/packages.js +149 -0
  53. package/dist/packages.js.map +1 -0
  54. package/dist/project.d.ts +45 -4
  55. package/dist/project.js +91 -18
  56. package/dist/project.js.map +1 -1
  57. package/dist/protobuf.d.ts +62 -0
  58. package/dist/protobuf.js +233 -0
  59. package/dist/protobuf.js.map +1 -0
  60. package/dist/stage.d.ts +155 -27
  61. package/dist/stage.js +483 -79
  62. package/dist/stage.js.map +1 -1
  63. package/dist/strings.d.ts +44 -0
  64. package/dist/strings.js +386 -0
  65. package/dist/strings.js.map +1 -0
  66. package/dist/template.d.ts +29 -0
  67. package/dist/template.js +438 -0
  68. package/dist/template.js.map +1 -0
  69. package/dist/threadStacks.d.ts +14 -0
  70. package/dist/threadStacks.js +78 -0
  71. package/dist/threadStacks.js.map +1 -0
  72. package/dist/toolchain.d.ts +7 -1
  73. package/dist/toolchain.js +8 -2
  74. package/dist/toolchain.js.map +1 -1
  75. package/dist/urlSchemes.d.ts +11 -0
  76. package/dist/urlSchemes.js +58 -0
  77. package/dist/urlSchemes.js.map +1 -0
  78. package/dist/vendor.d.ts +109 -0
  79. package/dist/vendor.js +440 -0
  80. package/dist/vendor.js.map +1 -0
  81. package/dist/wasm.d.ts +28 -0
  82. package/dist/wasm.js +84 -0
  83. package/dist/wasm.js.map +1 -0
  84. package/package.json +7 -6
package/README.md CHANGED
@@ -4,11 +4,12 @@ Run an iOS SwiftUI app in the browser, from its Xcode project, without a Mac.
4
4
 
5
5
  ```sh
6
6
  npx @swiftbrowser/cli doctor --install # Swift 6.4, the wasm SDK, wasm-opt
7
+ npx @swiftbrowser/cli init MyApp # a new SwiftUI app, as an Xcode project
7
8
  npx @swiftbrowser/cli dev ~/Projects/MyApp # http://localhost:5173, rebuilds on save
8
9
  npx @swiftbrowser/cli build ~/Projects/MyApp # ./dist: a static site to deploy anywhere
9
10
  ```
10
11
 
11
- The app's sources compile unchanged against [SwiftBrowser](https://github.com/dabbott/SwiftBrowser)'s
12
+ The app's sources compile unchanged against [SwiftBrowser](https://github.com/noya-app/SwiftBrowser)'s
12
13
  SwiftUI shim and run as WebAssembly inside an iPhone frame (or full screen on a
13
14
  phone: open the dev server's QR code, then Share → Add to Home Screen). What
14
15
  the shim does not provide is a compile error, never a wrong rendering.
@@ -17,19 +18,57 @@ the shim does not provide is a compile error, never a wrong rendering.
17
18
 
18
19
  | Command | What it does |
19
20
  |---|---|
21
+ | `init [<dir>]` | Writes a new SwiftUI app into `<dir>` (default `.`, created if missing): an Xcode 16 project (`<Name>.xcodeproj`, one app target, iOS 18) whose sources live in a synchronized folder `<Name>/` (`<Name>App.swift`, `ContentView.swift`, an asset catalog), plus a `.gitignore` and a README. It opens in Xcode as is and runs with `dev`; any Swift file added under `<Name>/` belongs to both. `--name <Name>` sets the app name (default: from the directory, `my-app` → `MyApp`), `--bundle-id` the bundle identifier (default `com.example.<Name>`). An existing directory is fine as long as `<Name>.xcodeproj` and `<Name>/` are free; its `.gitignore` gets the missing lines and its README is kept. |
20
22
  | `doctor [--install]` | Checks Node ≥ 22, Swift 6.4, the Swift SDK for WebAssembly and `wasm-opt`; `--install` fetches the SDK (`swift sdk install`) and Binaryen into `~/.cache/swiftbrowser`. Swift itself comes from [swiftly](https://www.swift.org/install/) (`swiftly install 6.4.0`), or point `SWIFT_TOOLCHAIN_BIN` at a toolchain's `usr/bin`. |
21
23
  | `check <path>` | Lists what the app imports (supported / rewritten / unsupported, with the usual way around each), then compiles the app natively against the shim. Seconds, no wasm; every error names your file and line. |
22
24
  | `dev <path>` | Builds the app for WebAssembly and serves it with the SwiftBrowser dev server: landing page with a QR code at `/`, the app at `/app/?app=<Name>`; saving a Swift file rebuilds and reloads with the app's state carried over. `--port`, `--host` (expose on the LAN for phones), `--open`. |
23
- | `build <path> [-o dist]` | A release build, optimized and stripped with `wasm-opt` (about 18 MB for a small app, 4.6 MB with brotli; 4.5 MB and 1.2 MB in lean mode), and a complete static site around it. `--no-optimize` skips wasm-opt. |
25
+ | `build <path> [-o dist]` | A release build, optimized and stripped with `wasm-opt` (17.4 MB for a small app, 4.5 MB with brotli), and a complete static site around it. `--no-optimize` skips wasm-opt. |
26
+ | `kit` | Compiles SwiftBrowser for WebAssembly ahead of a build (the first `dev` or `build` does it otherwise) and prints where the kit is. |
24
27
 
25
28
  `<path>` is an `.xcodeproj`, a folder holding one (directly or one level
26
29
  down), or a plain folder of Swift sources. Options for all three:
27
30
  `--target <name>` when the project has several app targets, `--config <file>`,
28
- `--foundation lean|full`, `--verbose`.
31
+ `--verbose`.
32
+
33
+ `dev` and `build` build apps against SwiftBrowser compiled ahead of time
34
+ (with Swift 6.4 and its Swift SDK for WebAssembly): every module of the shim,
35
+ and an archive of each module's objects, in a *kit* under
36
+ `~/.cache/swiftbrowser/kits/` (`SWIFTBROWSER_CACHE` moves the cache). A
37
+ released CLI comes with its kit, @swiftbrowser/kit (23 MB to download) and
38
+ the macros for your machine (@swiftbrowser/kit-linux-x64, -linux-arm64 or
39
+ -darwin-arm64, 4 MB): the first build copies it into the cache and checks
40
+ that your toolchain reads its modules. Where it cannot (another host, a
41
+ toolchain whose compiler reads only modules it wrote), or in a checkout of
42
+ this repository, the first build compiles the kit itself, in a minute or two. A kit takes 83 MB: 50 MB of archives, 15 MB of
43
+ modules and the 18 MB macro executable; SwiftPM's scratch path for building
44
+ kits takes 325 MB more.
45
+ Every app built after that, in any project, compiles its own code and
46
+ packages only: Counter's Swift build takes 6 s where it took 100 s. The app's
47
+ staged package depends on a package in the kit that stands in for
48
+ SwiftBrowser's, with the same products, whose targets link the archives of
49
+ the modules the app's imports need, and the compiler finds the modules
50
+ themselves in the kit (src/kit.ts). The kit is named by the shim's sources,
51
+ the toolchain and the SDK, so a change to any of them (a newer
52
+ `@swiftbrowser/cli`, an edit to the shim in this repository) builds another;
53
+ SwiftPM's scratch path for kits stays in the cache, so that rebuild compiles
54
+ only the modules the change touched, and the three most recent kits are kept.
55
+ `SWIFTBROWSER_KIT=0` builds the shim from its sources in the app's own build
56
+ instead, as before kits. `check` compiles natively and does not use the kit.
57
+
58
+ Building several apps in a row (a CI job, a script) can compile the
59
+ SwiftBrowser package once for all of them: set `SWIFTBROWSER_BUILD_ROOT` to a
60
+ directory, and every app is staged at `<root>/<Name>/` and built with one
61
+ SwiftPM scratch path, `<root>/.build`. After the first app, a `check` takes a
62
+ few seconds instead of compiling the shim natively again. SwiftPM recompiles a
63
+ dependency when the staging package's parent directory changes, which is why
64
+ apps staged in their own projects cannot share it. One build at a time per
65
+ root (SwiftPM locks the scratch path), and two apps with the same name take
66
+ turns in the same staging directory.
29
67
 
30
68
  ## What happens to your project
31
69
 
32
- Nothing, outside `<project>/.swiftbrowser/` (add it to `.gitignore`). The CLI
70
+ Nothing, outside `<project>/.swiftbrowser/` (add it to `.gitignore`), or
71
+ outside `$SWIFTBROWSER_BUILD_ROOT` when that is set. The CLI
33
72
  reads the Xcode project (`project.pbxproj`, including Xcode 16 synchronized
34
73
  folders) for the app target's Swift sources, asset catalogs, display name and
35
74
  app icon, then writes a SwiftPM package under `.swiftbrowser/<Name>/` whose one
@@ -37,20 +76,56 @@ executable target is a copy of those sources, adjusted for the shim:
37
76
 
38
77
  - `import SwiftUI` also imports Foundation and the shim's `FoundationBridge`
39
78
  (Apple's SwiftUI re-exports Foundation and Combine, so `UUID`, `JSONEncoder`,
40
- `UserDefaults`, `Timer.publish` are in scope without an import). In lean mode
41
- it imports `FoundationEssentials` and `FoundationLite` instead, and
42
- `import Foundation` becomes those two (see Foundation below). Both also
43
- import Observation, so `@Observable` works in a file that imports only
44
- Foundation, as it does under Xcode.
79
+ `UserDefaults`, `Timer.publish` are in scope without an import). Both it and
80
+ `import Foundation` also import Observation, so `@Observable` works in a file
81
+ that imports only Foundation, as it does under Xcode; `import Foundation`
82
+ brings `FoundationBridge` too, so a default argument such as
83
+ `session: URLSession = .shared` finds the bridge's `shared`.
84
+ - `x as? AnyObject` becomes `x as? _AnyObjectValue` in a file that reads a
85
+ number off it (`.doubleValue`, `.intValue`…), which Objective-C looks up on
86
+ any object at run time.
45
87
  - A generated `_SwiftBrowserFoundation.swift` settles the names both the shim
46
88
  and Foundation declare (`CGFloat`, `CGPoint`, `CGSize`, `CGRect`, `Timer`,
47
- `RunLoop`, `UserDefaults`, `Bundle`) with typealiases, in the shim's favor.
89
+ `RunLoop`, `UserDefaults`, `Bundle`, `DispatchQueue`) with typealiases, in
90
+ the shim's favor, and brings what Darwin's Foundation has and Linux's lacks
91
+ (`OperationQueue`, `CFAbsoluteTimeGetCurrent`, `os_unfair_lock`,
92
+ associated objects).
48
93
  - The target's Copy Bundle Resources (or `resources` in `swiftbrowser.json`)
49
94
  are served next to the module and mounted at `/bundle` before the app
50
95
  starts, so `Bundle.main.url(forResource:withExtension:)` and
51
96
  `Data(contentsOf:)` read them. Asset catalogs are handled separately
52
- (`Image("name")`, `Color("name")`).
53
- - `#Preview { … }` blocks are blanked (the shim has no preview macro).
97
+ (`Image("name")`, `Color("name")`): the app's and its packages' make one
98
+ catalog on the page; PNG, JPEG and SVG images are served as they are and
99
+ PDF images (vector artwork) as the PNGs pdf.js renders them to at 3×.
100
+ - Each module gets the asset symbols Xcode generates from its catalogs: a
101
+ static member of `ImageResource` or `ColorResource` per image, symbol and
102
+ color set (`Image(.floor1)`, `ImageResource.Fountain.terracotta` in a
103
+ namespace folder), and the `Color` / `ShapeStyle` members
104
+ (`.foregroundStyle(.accent)`) when the target sets
105
+ `ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS`, as Xcode's
106
+ templates do.
107
+ - Each Core ML model becomes `<Class>.mlmodel.swift`, the class Xcode would
108
+ generate for it with the model embedded (see "Core ML" below).
109
+ - `#Preview { … }` blocks and `PreviewProvider` structs are blanked (the shim
110
+ has no previews).
111
+ - Submodule imports of the shim's frameworks (`import UIKit.UIImage`,
112
+ `import os.log`) import the module; `delegate?.urlSession?(…)` (an
113
+ Objective-C optional call) becomes `delegate?.urlSession(…)`; and
114
+ `ProcessInfo.processInfo.physicalMemory`, which reads 0 on WASI, becomes
115
+ 4 GiB there.
116
+ - What Swift allows only with the Objective-C runtime is rewritten
117
+ (`packages/cli/src/objc.ts`): an `@objc` protocol becomes class-bound and
118
+ its `optional` requirements plain ones with default implementations
119
+ (nothing for `Void`, nil for an optional, the zero of `Bool`, numbers,
120
+ strings and collections; a requirement obsoleted in Swift is removed);
121
+ calls written for an optional requirement (`delegate?.method?(…)`, in any
122
+ file of the module) call it plainly; an `override` in an extension
123
+ (`description`, `isEqual(_:)` on an `NSObject` subclass) is removed, so the
124
+ superclass's stays; `#keyPath(Type.property)` becomes `"property"`; and
125
+ `ProcessInfo()` becomes `ProcessInfo.processInfo`. What remains of the
126
+ runtime (calling a method on a class found with `NSClassFromString`, an
127
+ `IMP` from `method(for:)`) does not compile and has to go behind
128
+ `#if !SWIFTBROWSER`.
54
129
  - `SWIFTBROWSER` is defined, so platform-specific code can go behind
55
130
  `#if SWIFTBROWSER` / `#if !SWIFTBROWSER`, like `#if os(iOS)`.
56
131
 
@@ -60,37 +135,56 @@ was released with).
60
135
 
61
136
  ## Foundation
62
137
 
63
- Two modes, measured through the same pipeline (release, `wasm-opt -Os`,
64
- stripped) on the Counter example:
138
+ Apps link the real Foundation (swift-corelibs-foundation, as the wasm SDK
139
+ ships it): `DateFormatter`, `NumberFormatter`, `NSString`, `Locale`-aware
140
+ `formatted()`, `Measurement`, `NSRegularExpression`, `JSONEncoder`, and the
141
+ rest compile as on iOS. Measured through the pipeline (release, `wasm-opt
142
+ -Oz`, stripped) on the Counter example:
65
143
 
66
- | Mode | What `import Foundation` means | Module | gzipped | brotli |
67
- |---|---|---|---|---|
68
- | `full` (default) | the wasm SDK's Foundation, ICU data trimmed to English | 18.3 MB | 6.7 MB | 4.6 MB |
69
- | `full`, `"locales": ["*"]` | the same with ICU's whole data package | 47 MB | 19.4 MB | 12.5 MB |
70
- | `lean` | `FoundationEssentials` + `FoundationLite` | 4.5 MB | 1.7 MB | 1.2 MB |
144
+ | ICU data | Module | gzipped | brotli |
145
+ |---|---|---|---|
146
+ | trimmed to English (default), 3.9 MB of it | 17.4 MB | 6.5 MB | 4.5 MB |
147
+ | `"locales": ["*"]`, the whole 34 MB package | 47 MB | 19 MB | 12 MB |
71
148
 
72
- **Full** links the real Foundation (swift-corelibs-foundation, as the wasm SDK
73
- ships it): `DateFormatter`, `NumberFormatter`, `NSString`, `Locale`-aware
74
- `formatted()`, `Measurement`, `NSRegularExpression`, `UserDefaults`, and the
75
- rest compile as on iOS. The SDK's Foundation carries ICU's complete 34 MB
76
- locale data package; the CLI pulls that package out of the SDK's archive, keeps
77
- the locale-independent tables plus the bundles of the languages in `"locales"`
78
- (default `["en"]`, 4.7 MB; the region part of a tag is ignored, every regional
79
- variant of a language stays), and links the trimmed package in the original's
80
- place. Formatting for a language that was dropped falls back to ICU's root
81
- locale (`11/14/2023, 22:13` and `€ 0.00` for French instead of
82
- `14/11/2023 22:13` and `0,00 €`); it never fails. Each language adds 100 to
83
- 300 kB, Chinese 1.7 MB (its collation table); `["*"]` keeps everything. The
84
- trimmed package is cached per SDK and language set under
85
- `~/.cache/swiftbrowser/icu/`; the SDK itself is not modified. What Foundation
86
- lacks on WASI, the shim's `FoundationBridge` supplies: `Timer` (corelibs' needs
87
- a run loop WASI does not have) with `Timer.publish` + `onReceive`, and
88
- `RunLoop.main` for its signature; `UserDefaults`, which Foundation's cannot
89
- persist on WASI, saved by the browser per app (localStorage) and restored at
90
- the next launch; and `Bundle`, whose Foundation counterpart traps, reading the
91
- app's resources. Known gaps of corelibs Foundation on WASI: `NotificationCenter`'s
149
+ The SDK's Foundation carries ICU's complete 34 MB locale data package; the CLI
150
+ pulls that package out of the SDK's archive, keeps the locale-independent
151
+ tables plus the bundles of the languages in `"locales"` (default `["en"]`;
152
+ the region part of a tag is ignored, every regional variant of a language
153
+ stays), and links the trimmed package in the original's place. Formatting for
154
+ a language that was dropped falls back to ICU's root locale (`11/14/2023,
155
+ 22:13` and `€ 0.00` for French instead of `14/11/2023 22:13` and `0,00 €`); it
156
+ never fails. Each language adds 100 to 300 kB, Chinese 1.7 MB (its collation
157
+ table); `["*"]` keeps everything. Dropped for good: the legacy character-set
158
+ converters (`String(data:encoding:)` with Shift JIS, Windows-1252 and the like
159
+ returns nil; UTF-8/16/32, ASCII and Latin-1 need none), the StringPrep
160
+ profiles and the spoof-checker data. `"collation": false` also drops the
161
+ collation tables (0.9 MB) when the app never compares strings in a locale:
162
+ `localizedCompare`, `localizedStandardCompare` and `.localizedStandard`
163
+ sorting then order by Unicode scalar. `locales` also chooses the languages
164
+ of the app's string tables: its string catalogs (`.xcstrings`) and
165
+ `<language>.lproj` `.strings` / `.stringsdict` files are compiled for those
166
+ languages and the catalog's source language, as Xcode compiles them (`Text`,
167
+ `String(localized:)` and `Bundle.localizedString` look keys up in them). The
168
+ trimmed package is cached per SDK,
169
+ language set and collation choice under `~/.cache/swiftbrowser/icu/`; the SDK
170
+ itself is not modified.
171
+
172
+ What Foundation lacks on WASI, the shim's `FoundationBridge` supplies: `Timer`
173
+ (corelibs' needs a run loop WASI does not have) with `Timer.publish` +
174
+ `onReceive`, and `RunLoop.main` for its signature; `UserDefaults`, which
175
+ Foundation's cannot persist on WASI, saved by the browser per app
176
+ (localStorage) and restored at the next launch; `Bundle`, whose Foundation
177
+ counterpart traps, reading the app's resources; and `URLSession`, `URLRequest`,
178
+ `HTTPURLResponse`, `URLError` and `AsyncImage` over the browser's `fetch`
179
+ (corelibs moved networking to FoundationNetworking, which the SDK does not
180
+ ship). Known gaps of corelibs Foundation on WASI: `NotificationCenter`'s
92
181
  closure observers and `OperationQueue` are missing.
93
182
 
183
+ Earlier releases had a `lean` mode (`FoundationEssentials` plus a small
184
+ formatting layer, about a quarter of the size). It is gone: `--foundation`
185
+ and `"foundation"` in `swiftbrowser.json` are accepted and ignored with a
186
+ warning, and an `import FoundationLite` becomes `import FoundationBridge`.
187
+
94
188
  ### Locale and time zone
95
189
 
96
190
  The app runs in the viewer's time zone and, within the languages it ships, the
@@ -108,14 +202,7 @@ formats in US English regardless.
108
202
  The hook behind this: swift-foundation hard-codes `en_001` on WASI, so the
109
203
  CLI redirects that one function to FoundationBridge at link time
110
204
  (`--wrap`); it depends on the SDK's swift-foundation, which the CLI pins and
111
- CI checks. Lean mode does not have it.
112
-
113
- **Lean** (`--foundation lean`, or `"foundation": "lean"` in `swiftbrowser.json`)
114
- is for apps that must stay small: `FoundationEssentials` (Date, Calendar, UUID,
115
- Data, JSON, URL) plus `FoundationLite`, SwiftBrowser's Foundation-compatible
116
- layer (number and date format styles, `IndexSet`, `CharacterSet`,
117
- `UserDefaults`, `Timer`). A Foundation API outside it is a compile error naming
118
- it. Output is fixed to US English conventions.
205
+ CI checks.
119
206
 
120
207
  ## swiftbrowser.json (optional)
121
208
 
@@ -130,37 +217,216 @@ it. Output is fixed to US English conventions.
130
217
  "resources": ["MyApp/missions.json", "MyApp/Data"],
131
218
  "icon": "MyApp/Assets.xcassets/AppIcon.appiconset/icon-1024.png",
132
219
  "swiftLanguageMode": "5",
133
- "foundation": "full",
134
220
  "locales": ["en"],
135
- "defines": ["DEMO"]
221
+ "collation": true,
222
+ "defines": ["DEMO"],
223
+ "corsProxy": "https://swiftbrowser-cors-proxy.you.workers.dev/",
224
+ "packages": [
225
+ { "url": "https://github.com/twostraws/CodeScanner", "from": "2.5.2", "products": ["CodeScanner"] },
226
+ { "path": "../Shared", "products": ["SharedKit"] }
227
+ ]
136
228
  }
137
229
  ```
138
230
 
139
231
  Everything is optional; `sources`, `catalogs` and `resources` replace what the
140
232
  Xcode project says when set (a plain folder has resources only through the
141
- config); `locales` matters in full Foundation mode only (see above).
233
+ config); `locales` and `collation` pick the ICU data that ships (see above).
234
+
235
+ `packages` adds Swift packages to the ones the Xcode target links: the only
236
+ way a plain folder of sources depends on one. Each has a `url` with one rule
237
+ (`from`: up to the next major version, `exact`, `branch` or `revision`), or
238
+ a local `path` (relative to the project), and the `products` the app
239
+ imports. They are built as described in "Swift packages".
240
+
241
+ `corsProxy` (or `--cors-proxy <url>` on `dev` and `build`) names a CORS proxy:
242
+ the page retries a cross-origin GET or HEAD the browser refused (the server
243
+ does not allow the page's origin, as image servers often don't) through
244
+ `<corsProxy>?url=<target>`. The proxy is a small Cloudflare Worker you deploy
245
+ yourself ([packages/cors-proxy](https://github.com/noya-app/SwiftBrowser/tree/main/packages/cors-proxy));
246
+ it is meant for screenshots and demos.
247
+
248
+ `blockHosts` (or `--block-host <host>`, repeatable) makes the app's requests
249
+ to those hosts and their subdomains fail as if offline, without leaving the
250
+ page. It is for a public deploy of an app whose analytics or other services
251
+ belong to someone else, such as Ice Cubes' TelemetryDeck signals.
252
+
253
+ A module larger than 24 MiB (Cloudflare Pages takes files up to 25 MiB) is
254
+ published as `<App>.wasm.0`, `.1`… with a `<App>.wasm.parts.json` manifest,
255
+ and the page joins the parts before compiling (so does `scripts/run-wasi.mjs`,
256
+ given `<App>.wasm`). Unoptimized builds (`--no-optimize`) are often this
257
+ large.
258
+
259
+ On Linux, Binaryen's release `wasm-opt` is linked against musl, whose threads
260
+ get 128 KiB stacks; a large module overflows them. The CLI runs a copy (made
261
+ once, in its cache) whose program header asks musl for 8 MiB thread stacks,
262
+ so `wasm-opt` uses every core: 106 s for Ice Cubes on four, where one thread
263
+ took 300 s, for the same module byte for byte (src/threadStacks.ts). If it
264
+ still crashes, the CLI runs it again on one thread with a large stack.
265
+
266
+ The app target's language mode (`SWIFT_VERSION`) and concurrency settings come
267
+ from the Xcode project as well: `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`
268
+ becomes `.defaultIsolation(MainActor.self)` in the staged package, and
269
+ `SWIFT_APPROACHABLE_CONCURRENCY` and each `SWIFT_UPCOMING_FEATURE_…` its
270
+ upcoming features (those the language mode does not have already), so an app
271
+ written against Xcode 26's main-actor default compiles as it does in Xcode.
142
272
 
143
273
  ## Frameworks
144
274
 
145
- Supported: SwiftUI (the shim's compatibility matrix is in the repository
146
- README), Observation, Swift Charts, Foundation (full mode), FoundationEssentials
147
- and FoundationLite (lean mode).
148
- `check` names the frameworks it knows have no browser counterpart yet (UIKit,
149
- SwiftData, Core Data, Combine, MapKit, Core ML, AVFoundation, UserNotifications,
150
- StoreKit, WidgetKit, PhotosUI, WebKit, …) with the usual way out: keep that code
151
- behind `#if !SWIFTBROWSER` and give the browser a stand-in behind `#if SWIFTBROWSER`.
275
+ Supported: SwiftUI (see the shim's
276
+ [compatibility matrix](https://github.com/noya-app/SwiftBrowser/blob/main/docs/compatibility.md)), Observation, Swift Charts, Foundation, networking (`URLSession`,
277
+ `URLRequest`, `HTTPURLResponse`, `URLError` and `AsyncImage` run over the
278
+ browser's `fetch`, so the servers an app talks to must allow its origin, CORS),
279
+ and SwiftData: `@Model`, `ModelContainer`, `ModelContext`, `@Query`,
280
+ `#Predicate`, relationships with their delete rules, autosave. The models live
281
+ in memory while the app runs and the page keeps them in IndexedDB, one database
282
+ per app (`sb-swiftdata:<App>`); `?swiftdata=reset` on the app's URL starts it
283
+ with no data. An app that imports SwiftData also builds SwiftData's macros,
284
+ which need swift-syntax: SwiftPM fetches it on the first build and uses its
285
+ prebuilt release where swift.org publishes one (Swift 6.4 on Linux and macOS),
286
+ else compiles it once (several minutes).
287
+ Files an app writes in its home (`URL.documentsDirectory` and the other
288
+ iOS directories, through `Data`, `String` or `FileManager`) are kept the same
289
+ way, in `sb-files:<App>`; `?files=reset` starts with none.
290
+ `check` names the frameworks it knows have no browser counterpart yet (Core
291
+ Data, WidgetKit, Metal, …) with the usual way out: keep that code
292
+ behind `#if !SWIFTBROWSER` and give the browser a stand-in behind
293
+ `#if SWIFTBROWSER`.
294
+ The frameworks under SwiftUI that libraries build on are there, as far as
295
+ image and utility code uses them: UIKit's `UIImage`, `UIColor` and
296
+ `UIScreen` (its view classes exist so that code declaring UIKit views
297
+ compiles; none can be shown), Core Graphics contexts that draw, scale and
298
+ clip images, Image I/O sources, Combine (OpenCombine), CryptoKit's hashes and
299
+ os's locks and logs. AVKit and AVFoundation are empty: an unused import
300
+ compiles, and `check` warns about it. With them, real packages such as
301
+ [Nuke](https://github.com/kean/Nuke) build from their own sources.
302
+ StoreKit, Core Location and LocalAuthentication import fine, as far as apps
303
+ usually use them: `requestReview` does nothing, `CLLocationCoordinate2D` is a
304
+ plain struct (`CLLocation` measures distances), and an `LAContext` evaluation
305
+ always succeeds. MapKit's SwiftUI
306
+ `Map` draws its region without map data: a plain surface with latitude and
307
+ longitude lines, with its annotations, markers and overlays where they are on
308
+ it; `MapReader` converts a tap's location to a coordinate. It does not pan or
309
+ zoom. UIKit's `MKMapView` draws the same map at its camera. WeatherKit's
310
+ requests throw `WeatherError.permissionDenied` (Apple's service answers only
311
+ apps signed with the capability), and passkey and password requests
312
+ (`ASAuthorizationController`, SwiftUI's `AuthorizationController`) end in
313
+ `ASAuthorizationError.notHandled`, so an app takes its own fallback. PhotosUI's
314
+ `PhotosPicker` opens the browser's file chooser, and the picked files' bytes
315
+ load with `loadTransferable(type: Data.self)`. Core Image
316
+ (`import CoreImage`, or `import CoreImage.CIFilterBuiltins`, which the staging
317
+ turns into `import CoreImage`) has its common built-in filters and the QR code
318
+ generator, drawn by the page; `UIImage` and `CGImage` come with SwiftUI, as on
319
+ iOS. UserNotifications runs over the browser's Notification API: the app asks
320
+ for permission with the browser's prompt, schedules its requests with timers of
321
+ its own and the page shows each one as it comes due. Notifications are
322
+ delivered only while the page is open (a closed tab delivers nothing), and
323
+ Safari on iPhone shows them only for an app added to the Home Screen.
324
+
325
+ ### Core ML
326
+
327
+ The target's Core ML models (`.mlmodel` files and `.mlpackage` bundles in its
328
+ Sources phase or synchronized folders; in a plain folder, every one found, or
329
+ those `"sources"` names) compile into the classes Xcode generates for them:
330
+ `SleepCalculator.mlmodel` becomes `SleepCalculator`, `SleepCalculatorInput`
331
+ and `SleepCalculatorOutput`, with `init(configuration:)`,
332
+ `prediction(wake:estimatedSleep:coffee:)` (each input typed as Xcode types
333
+ it), `prediction(input:)`, `predictions(inputs:)`, the `load` functions,
334
+ `model` and `urlOfModelInThisBundle`. The CLI reads the model's protobuf and
335
+ embeds its specification in the class; the shim's `CoreML` module evaluates
336
+ it (`MLModel`, `MLFeatureValue`, `MLMultiArray`, `MLDictionaryFeatureProvider`
337
+ and friends). That covers what Create ML makes from tables: linear and
338
+ logistic regression, boosted trees, random forests and decision trees, and
339
+ the one-hot encoders, imputers, normalizers, scalers and vectorizers around
340
+ them. Any other model (a neural network, an ML program, an image, sound or
341
+ text model) stops the build with its type named, for example
342
+ `Classifier.mlmodel: the model is a neural network (neuralNetwork), which the
343
+ browser cannot run`. `dev` rebuilds when a Swift file changes; a retrained model
344
+ is picked up by the next rebuild.
345
+
346
+ ## Swift packages
347
+
348
+ The Xcode target's package dependencies (File › Add Package Dependencies)
349
+ are added to the staging package: each remote package with the version rule
350
+ Xcode records (up to next major or minor, exact, a range, a branch or a
351
+ revision), each local package by path. SwiftPM fetches and builds them for
352
+ the browser with the app, so a pure-Swift package works if it builds for
353
+ WASI.
354
+
355
+ A package that imports SwiftUI (or Charts, SwiftData, or another framework
356
+ SwiftBrowser stands in for) is built against the shim, from a copy under
357
+ `.swiftbrowser/<Name>/Packages/`: its sources get the same adjustments as
358
+ the app's, and lines appended to its manifest link the shim's modules to the
359
+ targets that import them and define `SWIFTBROWSER`. A package that depends
360
+ on such a package, directly or not, is copied too and pointed at the copy.
361
+ Your package folders and SwiftPM's checkouts are never written to.
362
+
363
+ A package that cannot work in the browser and that SwiftBrowser knows is
364
+ replaced by a module of the same name instead, and never fetched (when one of
365
+ your packages depends on it, that package gets the stand-in too). Today
366
+ there are none.
367
+ [WrappingHStack](https://github.com/dkk/WrappingHStack) builds from its own
368
+ sources: it measures its items with `UIHostingController`, whose
369
+ `sizeThatFits(in:)` answers from the page's measurements a render later.
370
+ [CodeScanner](https://github.com/twostraws/CodeScanner) builds from its own
371
+ sources over AVFoundation's capture sessions: it scans with the camera where
372
+ the browser has `BarcodeDetector` (Chrome on Android and macOS) and asks
373
+ for the code's text elsewhere. `check` lists each package and which way it
374
+ went:
375
+
376
+ ```
377
+ ✓ package CodeScanner (github.com/twostraws/CodeScanner, exactly 2.5.2): built against the shim (CodeScanner imports AVFoundation, SwiftUI, UIKit) (CodeScanner)
378
+ ✓ package Nuke (github.com/kean/Nuke, exactly 13.2.0): built against the shim (Nuke imports AVKit, Combine, CoreGraphics, CoreImage, CryptoKit, ImageIO, UIKit, UniformTypeIdentifiers, os; NukeUI imports Combine, SwiftUI, UIKit; …) (Nuke, NukeUI)
379
+ ✓ package swift-algorithms (github.com/apple/swift-algorithms, up to next major from 1.2.0): a SwiftPM dependency of the build (Algorithms)
380
+ ✓ package Tags (/work/Tags): built against the shim (Tags imports SwiftUI) (Tags)
381
+ ✓ package SwiftUI-Shimmer 1.5.1, a dependency of a dependency: built against the shim (Shimmer imports SwiftUI)
382
+ ```
383
+
384
+ A package's `type: .dynamic` libraries are built as automatic ones (the
385
+ browser build is one module), so Apple's Backyard Birds sample builds with
386
+ its three local packages as they are:
387
+
388
+ ```sh
389
+ git clone --depth 1 https://github.com/apple/sample-backyard-birds
390
+ npx @swiftbrowser/cli dev sample-backyard-birds --target "Backyard Birds"
391
+ ```
392
+
393
+ Apple's Food Truck sample builds as it is too:
394
+
395
+ ```sh
396
+ git clone --depth 1 https://github.com/apple/sample-food-truck
397
+ npx @swiftbrowser/cli dev sample-food-truck --target "Food Truck"
398
+ ```
399
+
400
+ A package SwiftBrowser has a patch for is built from its own sources with
401
+ a few passages replaced first: code that only works with the Objective-C
402
+ runtime and no general adjustment can take. Today that is
403
+ [RevenueCat](https://github.com/RevenueCat/purchases-ios-spm), patched for
404
+ 5.92.0 (packages/cli/src/packagePatches.ts). The patch names each passage
405
+ exactly; when the version SwiftPM resolves no longer has one, the build stops
406
+ and says which, and the package has to be pinned to the release the patch
407
+ was written for (or the patch updated).
408
+
409
+ A package that needs a framework the browser has no counterpart for (Core
410
+ Data, Metal, a UIKit view on screen…) fails to compile with an error naming
411
+ it; keep its uses behind `#if !SWIFTBROWSER` in your own packages. Images a
412
+ package downloads itself (Nuke does, with `URLSession`) go through the
413
+ browser's `fetch`, so their host must allow CORS. The SwiftUI it uses is held
414
+ to the same [compatibility](../../docs/compatibility.md) as an app's.
152
415
 
153
416
  ## Example
154
417
 
155
- Twelve of the nineteen "Hacking with SwiftUI" projects run from their Xcode
156
- projects without a change (WeSplit, Guess the Flag, Views and Modifiers,
157
- WordScramble, Animations, iExpense, Moonshot, Navigation, Accessibility,
158
- Flashzilla, Layout and Geometry, SnowSeeker); `check` names what stops each
159
- of the others, which all need a framework the browser has no counterpart for
160
- yet (Core ML, URLSession, SwiftData, Core Image, MapKit):
418
+ All twenty "Hacking with SwiftUI" projects run from their Xcode projects
419
+ without a change (WeSplit, Guess the Flag, Views and Modifiers, BetterRest,
420
+ WordScramble, Animations, iExpense, Moonshot, Navigation, Cupcake Corner,
421
+ Bookworm, SwiftDataProject, Instafilter, Bucket List, Accessibility, Hot
422
+ Prospects, Flashzilla, Layout and Geometry, SnowSeeker, Drawing):
161
423
 
162
424
  ```sh
163
425
  git clone --depth 1 https://github.com/twostraws/HackingWithSwift
164
- npx @swiftbrowser/cli check HackingWithSwift/SwiftUI/project4 # BetterRest: Core ML
426
+ npx @swiftbrowser/cli dev HackingWithSwift/SwiftUI/project4 # BetterRest: Core ML
427
+ npx @swiftbrowser/cli dev HackingWithSwift/SwiftUI/project13 # Instafilter: PhotosPicker, Core Image
428
+ npx @swiftbrowser/cli dev HackingWithSwift/SwiftUI/project14 # Bucket List: MapKit, Face ID
429
+ npx @swiftbrowser/cli dev HackingWithSwift/SwiftUI/project16 # Hot Prospects: CodeScanner (the real package), notifications
165
430
  npx @swiftbrowser/cli dev HackingWithSwift/SwiftUI/project17 # Flashzilla
431
+ npx @swiftbrowser/cli dev HackingWithSwift/SwiftUI/project11 # Bookworm: SwiftData
166
432
  ```
@@ -0,0 +1,25 @@
1
+ /** The staged file that holds a module's asset symbols. */
2
+ export declare const ASSET_SYMBOLS_FILE = "_SwiftBrowserAssetSymbols.swift";
3
+ /**
4
+ * The member an asset's name becomes, as Xcode names it: a trailing `Color`
5
+ * (of a color set) or `Image` (of an image set) dropped, then lower camel
6
+ * case (`Floor 1` → `floor1`, `AccentColor` → `accent`, `bird-food` →
7
+ * `birdFood`); one that starts with a digit gets an underscore.
8
+ */
9
+ export declare function assetIdentifier(name: string, kind: 'image' | 'color'): string;
10
+ /** The type a namespace folder becomes (`Fountain`, `bird houses` → `BirdHouses`). */
11
+ export declare function namespaceIdentifier(name: string): string;
12
+ /** Members a module's sources declare themselves (`static let floor1`), which a generated one would redeclare. */
13
+ export declare function declaredStaticMembers(sources: readonly string[]): Set<string>;
14
+ /**
15
+ * The Swift source of a module's asset symbols, for its catalogs (empty
16
+ * when they have no image, symbol or color sets). `extensions` adds the
17
+ * `Color` and `ShapeStyle` members of the top level's colors, except those
18
+ * SwiftUI has or the module declares itself (`declared`).
19
+ */
20
+ export declare function assetSymbolsSource(catalogs: readonly string[], options?: {
21
+ extensions?: boolean;
22
+ declared?: ReadonlySet<string>;
23
+ }): string;
24
+ /** The asset catalogs in a package target's folder (SwiftPM processes them as the target's resources). */
25
+ export declare function targetCatalogs(sourceDir: string): string[];