@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.
- package/README.md +340 -65
- package/dist/assetSymbols.d.ts +25 -0
- package/dist/assetSymbols.js +218 -0
- package/dist/assetSymbols.js.map +1 -0
- package/dist/build.d.ts +27 -2
- package/dist/build.js +71 -14
- package/dist/build.js.map +1 -1
- package/dist/buildSettings.d.ts +15 -0
- package/dist/buildSettings.js +74 -0
- package/dist/buildSettings.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +66 -11
- package/dist/cli.js.map +1 -1
- package/dist/commands/build.js +6 -5
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/check.js +2 -2
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/dev.d.ts +5 -2
- package/dist/commands/dev.js +19 -13
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.d.ts +18 -0
- package/dist/commands/init.js +59 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/kit.d.ts +3 -0
- package/dist/commands/kit.js +21 -0
- package/dist/commands/kit.js.map +1 -0
- package/dist/commands/shared.d.ts +28 -4
- package/dist/commands/shared.js +106 -11
- package/dist/commands/shared.js.map +1 -1
- package/dist/commands/test.d.ts +22 -0
- package/dist/commands/test.js +95 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/compat.d.ts +44 -6
- package/dist/compat.js +64 -36
- package/dist/compat.js.map +1 -1
- package/dist/config.d.ts +48 -10
- package/dist/config.js +64 -8
- package/dist/config.js.map +1 -1
- package/dist/coreml.d.ts +194 -0
- package/dist/coreml.js +626 -0
- package/dist/coreml.js.map +1 -0
- package/dist/icu.d.ts +17 -4
- package/dist/icu.js +32 -18
- package/dist/icu.js.map +1 -1
- package/dist/kit.d.ts +133 -0
- package/dist/kit.js +403 -0
- package/dist/kit.js.map +1 -0
- package/dist/objc.d.ts +31 -0
- package/dist/objc.js +319 -0
- package/dist/objc.js.map +1 -0
- package/dist/packagePatches.d.ts +24 -0
- package/dist/packagePatches.js +107 -0
- package/dist/packagePatches.js.map +1 -0
- package/dist/packages.d.ts +59 -0
- package/dist/packages.js +149 -0
- package/dist/packages.js.map +1 -0
- package/dist/project.d.ts +97 -4
- package/dist/project.js +157 -20
- package/dist/project.js.map +1 -1
- package/dist/protobuf.d.ts +62 -0
- package/dist/protobuf.js +233 -0
- package/dist/protobuf.js.map +1 -0
- package/dist/stage.d.ts +175 -28
- package/dist/stage.js +551 -85
- package/dist/stage.js.map +1 -1
- package/dist/strings.d.ts +44 -0
- package/dist/strings.js +386 -0
- package/dist/strings.js.map +1 -0
- package/dist/template.d.ts +39 -0
- package/dist/template.js +678 -0
- package/dist/template.js.map +1 -0
- package/dist/testRunner.d.ts +1 -0
- package/dist/testRunner.js +44 -0
- package/dist/testRunner.js.map +1 -0
- package/dist/threadStacks.d.ts +14 -0
- package/dist/threadStacks.js +78 -0
- package/dist/threadStacks.js.map +1 -0
- package/dist/toolchain.d.ts +7 -1
- package/dist/toolchain.js +8 -2
- package/dist/toolchain.js.map +1 -1
- package/dist/urlSchemes.d.ts +11 -0
- package/dist/urlSchemes.js +58 -0
- package/dist/urlSchemes.js.map +1 -0
- package/dist/vendor.d.ts +109 -0
- package/dist/vendor.js +440 -0
- package/dist/vendor.js.map +1 -0
- package/dist/wasm.d.ts +28 -0
- package/dist/wasm.js +84 -0
- package/dist/wasm.js.map +1 -0
- 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/
|
|
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` (
|
|
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
|
|
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
|
-
`--
|
|
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`)
|
|
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).
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
|
67
|
-
|
|
68
|
-
|
|
|
69
|
-
| `
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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.
|
|
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
|
-
"
|
|
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`
|
|
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
|
|
146
|
-
|
|
147
|
-
and
|
|
148
|
-
`
|
|
149
|
-
SwiftData
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
WordScramble, Animations, iExpense, Moonshot, Navigation,
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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[];
|