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