@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/dist/stage.js
CHANGED
|
@@ -4,10 +4,48 @@
|
|
|
4
4
|
// is never written to outside that folder.
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { ASSET_SYMBOLS_FILE, assetSymbolsSource, declaredStaticMembers } from './assetSymbols.js';
|
|
8
|
+
import { swiftSettingsEntries } from './buildSettings.js';
|
|
9
|
+
import { compileLocalizations } from './strings.js';
|
|
10
|
+
import { LINKED_FRAMEWORKS, importsOf } from './compat.js';
|
|
11
|
+
import { ModelError, compileModel } from './coreml.js';
|
|
7
12
|
import { ICU_TARGET, icuAssembly } from './icu.js';
|
|
8
|
-
|
|
13
|
+
import { extensionOverrides, keyPathStrings, objcProtocols, optionalRequirementCalls, optionalRequirementNames } from './objc.js';
|
|
14
|
+
import { planPackages } from './packages.js';
|
|
15
|
+
/**
|
|
16
|
+
* `$SWIFTBROWSER_BUILD_ROOT`, resolved; unset by default. With it, every app is
|
|
17
|
+
* staged at `<root>/<App>/` and built with one SwiftPM scratch path,
|
|
18
|
+
* `<root>/.build`, so a run that compiles several apps compiles the
|
|
19
|
+
* SwiftBrowser package once. SwiftPM recompiles a dependency whenever the
|
|
20
|
+
* root package's parent directory changes, so apps staged in their own
|
|
21
|
+
* projects cannot share it even with a common scratch path. One build at a
|
|
22
|
+
* time per root: SwiftPM locks the scratch path.
|
|
23
|
+
*/
|
|
24
|
+
export function buildRoot() {
|
|
25
|
+
const root = process.env['SWIFTBROWSER_BUILD_ROOT'];
|
|
26
|
+
return root ? path.resolve(root) : undefined;
|
|
27
|
+
}
|
|
28
|
+
/** Where an app's staging package lives: `<project>/.swiftbrowser/<App>/`, or under the build root. */
|
|
9
29
|
export function stagingDir(app) {
|
|
10
|
-
|
|
30
|
+
const root = buildRoot();
|
|
31
|
+
return root ? path.join(root, app.name) : path.join(app.root, '.swiftbrowser', app.name);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Where `swiftbrowser test` stages the app with its tests: beside the app's
|
|
35
|
+
* own staging (`<App>.tests/`), so that `test` and `dev` or `build` do not
|
|
36
|
+
* rewrite each other's package, and each stays incremental.
|
|
37
|
+
*/
|
|
38
|
+
export function testStagingDir(app) {
|
|
39
|
+
return `${stagingDir(app)}.tests`;
|
|
40
|
+
}
|
|
41
|
+
/** The staged folder of a test target's sources, relative to the staging package. */
|
|
42
|
+
export function testTargetPath(name) {
|
|
43
|
+
return path.join('Tests', name);
|
|
44
|
+
}
|
|
45
|
+
/** The `swift build` arguments that select the shared scratch path under the build root, if any. */
|
|
46
|
+
export function scratchPathArgs() {
|
|
47
|
+
const root = buildRoot();
|
|
48
|
+
return root ? ['--scratch-path', path.join(root, '.build')] : [];
|
|
11
49
|
}
|
|
12
50
|
/** The built module, optimized or not. */
|
|
13
51
|
export function wasmOutput(app) {
|
|
@@ -16,55 +54,246 @@ export function wasmOutput(app) {
|
|
|
16
54
|
/**
|
|
17
55
|
* Adjusts one source for the shim without moving any line, so compiler
|
|
18
56
|
* diagnostics keep their line numbers:
|
|
19
|
-
* - `import SwiftUI` also imports
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* -
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
57
|
+
* - `import SwiftUI` also imports Foundation, FoundationBridge and Observation,
|
|
58
|
+
* as Apple's SwiftUI re-exports Foundation (and Foundation re-exports
|
|
59
|
+
* Observation, so `@Observable` works in a file that imports either alone);
|
|
60
|
+
* - `import Foundation` also imports Observation, for the same reason, and
|
|
61
|
+
* (`bridged`: in a module built against the bridge) FoundationBridge, whose
|
|
62
|
+
* `URLSession`, `UserDefaults`… the alias file names: a public default
|
|
63
|
+
* argument (`session: URLSession = .shared`) may only use a member of a
|
|
64
|
+
* module its own file imports;
|
|
65
|
+
* - `import CoreImage.CIFilterBuiltins` → `import CoreImage`, and likewise
|
|
66
|
+
* `import UIKit.UIImage`, `import os.log`, `import CoreGraphics.CGImage`:
|
|
67
|
+
* on Apple platforms those are Clang submodules; the shim's frameworks are
|
|
68
|
+
* Swift modules, which have no submodules to import;
|
|
69
|
+
* - `delegate?.urlSession?(…)` → `delegate?.urlSession(…)`: the delegate
|
|
70
|
+
* methods are `@objc optional` on Apple platforms, plain requirements with
|
|
71
|
+
* default implementations in FoundationBridge;
|
|
72
|
+
* - `ProcessInfo.processInfo.physicalMemory` → `_swiftBrowserPhysicalMemory` (the alias file's):
|
|
73
|
+
* swift-foundation answers 0 on WASI, and limits sized from it refuse everything;
|
|
74
|
+
* - `import Dispatch` → `import FoundationBridge`: the SDK has no Dispatch on
|
|
75
|
+
* WASI, and the bridge's queues run on the main actor (a package that
|
|
76
|
+
* imports it is built from a copy, vendor.ts);
|
|
77
|
+
* - `import ObjectiveC` → `import FoundationBridge`, which has the associated
|
|
78
|
+
* objects (CoreFoundation.swift), and `unsafeBitCast(Selector(…), to:
|
|
79
|
+
* UnsafeRawPointer.self)` → `Selector(…)._uniquePointer`: on Apple platforms
|
|
80
|
+
* a selector is a uniqued pointer, the usual key of an associated object;
|
|
81
|
+
* here it is a struct, which the bit cast would trap on (`selectorPointers`;
|
|
82
|
+
* a package that imports ObjectiveC is built from a copy, vendor.ts);
|
|
83
|
+
* - `import FoundationLite` → `import FoundationBridge`: FoundationLite was the
|
|
84
|
+
* lean mode's formatting layer (Phase 14 folded it into Foundation); a file
|
|
85
|
+
* that still names it gets the bridge, which holds what remains;
|
|
86
|
+
* - `configuration.delegateClass = SceneDelegate.self` records how to make
|
|
87
|
+
* the scene delegate (`sceneDelegateClasses`);
|
|
88
|
+
* - `x as? AnyObject` → `x as? _AnyObjectValue` in a file that reads a number
|
|
89
|
+
* off it (`.doubleValue`, `.intValue`…): Objective-C finds those members on
|
|
90
|
+
* any object at run time, which the browser's Swift cannot (`numberCasts`);
|
|
91
|
+
* - `@objc` protocols become class-bound, with their `optional` requirements
|
|
92
|
+
* as plain ones that have default implementations, and calls of those
|
|
93
|
+
* requirements (`delegate?.method?(…)`, in any of the module's files:
|
|
94
|
+
* `optionalRequirements`) plain calls; an `override` in an extension is
|
|
95
|
+
* blanked; `#keyPath(Type.property)` becomes `"property"` (objc.ts);
|
|
96
|
+
* - `ProcessInfo()` → `ProcessInfo.processInfo`;
|
|
28
97
|
* - `#Preview { … }` blocks (and `#Preview("name", traits: …) { … }`) become
|
|
29
98
|
* blank lines: the shim has no preview macro and previews are not part of
|
|
30
|
-
* the app.
|
|
99
|
+
* the app. So do the older `struct …: PreviewProvider { … }` declarations,
|
|
100
|
+
* which packages still carry (with preview-only modifiers inside).
|
|
31
101
|
*/
|
|
32
|
-
export function transformSource(text, options) {
|
|
102
|
+
export function transformSource(text, options = {}) {
|
|
33
103
|
let foundationRewrites = 0;
|
|
34
104
|
let out = text;
|
|
35
|
-
// On
|
|
36
|
-
//
|
|
37
|
-
// does not, so the same modules
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
105
|
+
// On iOS `import SwiftUI` re-exports Foundation (UUID, JSON, Date,
|
|
106
|
+
// UserDefaults are in scope without an import) and UIKit (`UIColor`,
|
|
107
|
+
// `UIApplication.shared`); the shim's SwiftUI does not, so the same modules
|
|
108
|
+
// are imported next to it (and UIKit is linked wherever SwiftUI is, featuresOf).
|
|
109
|
+
// An access level on the import (`public import SwiftUI`, with
|
|
110
|
+
// `InternalImportsByDefault`) goes on its companions too.
|
|
111
|
+
const companions = ['Foundation', 'FoundationBridge', 'Observation', 'UIKit'];
|
|
112
|
+
out = out.replace(/^([ \t]*)((?:(?:public|package|internal|fileprivate|private)[ \t]+)?)import SwiftUI[ \t]*(\r?)$/gm, (_, indent, access, cr) => `${indent}${access}import SwiftUI; ${companions.map((module) => `${access}import ${module}`).join('; ')}${cr}`);
|
|
113
|
+
const foundationCompanions = options.bridged ? ['FoundationBridge', 'Observation'] : ['Observation'];
|
|
114
|
+
out = out.replace(/^([ \t]*)((?:(?:public|package|internal|fileprivate|private)[ \t]+)?)import Foundation[ \t]*(\r?)$/gm, (_, indent, access, cr) => `${indent}${access}import Foundation; ${foundationCompanions.map((module) => `${access}import ${module}`).join('; ')}${cr}`);
|
|
115
|
+
out = out.replace(SUBMODULE_IMPORT, (_, lead, module, cr) => `${lead}import ${module}${cr}`);
|
|
116
|
+
// URLSession's delegate methods are `@objc optional` on Apple platforms;
|
|
117
|
+
// FoundationBridge's are requirements with defaults (URLSession.swift).
|
|
118
|
+
out = out.replace(/\.urlSession\?\(/g, '.urlSession(');
|
|
119
|
+
// swift-foundation reports no physical memory on WASI (FoundationBridge/ProcessInfo.swift).
|
|
120
|
+
out = out.replace(/\bProcessInfo\.processInfo\.physicalMemory\b/g, '_swiftBrowserPhysicalMemory');
|
|
121
|
+
// The browser runs iOS apps: `#if os(iOS)` holds, as it does on the
|
|
122
|
+
// iPhone the page stands for (the build's real OS is WASI or Linux).
|
|
123
|
+
out = out.replace(IOS_CONDITION, (line) => line.replace(/\bos\(\s*iOS\s*\)/g, '(os(iOS) || SWIFTBROWSER)'));
|
|
124
|
+
// The SDK has no Dispatch on WASI: the bridge's queues stand in (Dispatch.swift).
|
|
125
|
+
out = out.replace(/^([ \t]*(?:@[A-Za-z_]+[ \t]+)*(?:(?:public|package|internal|fileprivate|private)[ \t]+)?)import Dispatch[ \t]*(\r?)$/gm, (_, lead, cr) => `${lead}import FoundationBridge${cr}`);
|
|
126
|
+
// Associated objects without the Objective-C runtime (FoundationBridge/CoreFoundation.swift).
|
|
127
|
+
out = out.replace(/^([ \t]*(?:@[A-Za-z_]+[ \t]+)*(?:(?:public|package|internal|fileprivate|private)[ \t]+)?)import ObjectiveC[ \t]*(\r?)$/gm, (_, lead, cr) => `${lead}import FoundationBridge${cr}`);
|
|
128
|
+
out = selectorPointers(out);
|
|
129
|
+
out = out.replace(/^([ \t]*)import FoundationLite[ \t]*(\r?)$/gm, (_, indent, cr) => {
|
|
130
|
+
foundationRewrites += 1;
|
|
131
|
+
return `${indent}import FoundationBridge${cr}`;
|
|
132
|
+
});
|
|
133
|
+
// Objective-C protocols and what Swift allows only with the runtime (objc.ts).
|
|
134
|
+
out = objcProtocols(out);
|
|
135
|
+
out = optionalRequirementCalls(out, options.optionalRequirements ?? optionalRequirementNames([text]));
|
|
136
|
+
out = extensionOverrides(out);
|
|
137
|
+
out = keyPathStrings(out);
|
|
138
|
+
// `ProcessInfo()` is the process's info on Apple platforms; corelibs' initializer is internal.
|
|
139
|
+
out = out.replace(/\bProcessInfo\(\)/g, 'ProcessInfo.processInfo');
|
|
140
|
+
out = rewriteObjC(out);
|
|
141
|
+
out = sceneDelegateClasses(out);
|
|
142
|
+
out = numberCasts(out);
|
|
53
143
|
const stripped = stripPreviews(out);
|
|
54
144
|
return { text: stripped.text, previewsStripped: stripped.count, foundationRewrites };
|
|
55
145
|
}
|
|
56
|
-
/**
|
|
146
|
+
/**
|
|
147
|
+
* `configuration.delegateClass = SceneDelegate.self` →
|
|
148
|
+
* `configuration._setDelegateClass(SceneDelegate.self) { SceneDelegate() }`:
|
|
149
|
+
* UIKit makes the scene delegate through the Objective-C runtime, and Swift
|
|
150
|
+
* without it can only make an instance from a metatype through a `required`
|
|
151
|
+
* initializer (UIKit/SceneDelegates.swift). Same line, so diagnostics keep
|
|
152
|
+
* their places.
|
|
153
|
+
*/
|
|
154
|
+
export function sceneDelegateClasses(text) {
|
|
155
|
+
if (!text.includes('delegateClass'))
|
|
156
|
+
return text;
|
|
157
|
+
return text.replace(/(\b[A-Za-z_][\w.]*)\.delegateClass\s*=\s*([A-Z][\w.]*)\.self\b/g, '$1._setDelegateClass($2.self) { $2() }');
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* `as? AnyObject` → `as? _AnyObjectValue` where the file reads a number from
|
|
161
|
+
* the result: `(dict[key] as? AnyObject)?.doubleValue` looks the member up on
|
|
162
|
+
* any object through Objective-C (nil where there is none); FoundationBridge's
|
|
163
|
+
* `_AnyObjectValue` (Selector.swift) has those members, optional likewise, on
|
|
164
|
+
* the numbers, booleans and strings a dictionary of `Any` holds.
|
|
165
|
+
*/
|
|
166
|
+
export function numberCasts(text) {
|
|
167
|
+
if (!/\bas\?\s*AnyObject\b/.test(text))
|
|
168
|
+
return text;
|
|
169
|
+
if (!/\.(?:doubleValue|floatValue|intValue|integerValue|int64Value|uint64Value|uintValue|boolValue)\b/.test(text))
|
|
170
|
+
return text;
|
|
171
|
+
return text.replace(/\bas\?(\s*)AnyObject\b/g, 'as?$1_AnyObjectValue');
|
|
172
|
+
}
|
|
173
|
+
/** Whether a source uses the Objective-C runtime's selectors or attributes. */
|
|
174
|
+
export function usesObjC(text) {
|
|
175
|
+
return /#selector\s*\(|@objc\b|@objcMembers\b/.test(text);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Whether a source calls POSIX mutex functions. WASI's libc module leaves
|
|
179
|
+
* `pthread.h` out, so a package that does is built from a copy: there
|
|
180
|
+
* `#if os(iOS)` holds (Darwin's `os_unfair_lock` branch) and FoundationBridge
|
|
181
|
+
* declares the mutex functions for the page's single thread (Locks.swift).
|
|
182
|
+
*/
|
|
183
|
+
/**
|
|
184
|
+
* `unsafeBitCast(Selector(#function), to: UnsafeRawPointer.self)` →
|
|
185
|
+
* `Selector(#function)._uniquePointer`: the address that stands for the
|
|
186
|
+
* selector's name (FoundationBridge/Selector.swift), which on Apple
|
|
187
|
+
* platforms the bit cast gives, the selector being a uniqued pointer.
|
|
188
|
+
*/
|
|
189
|
+
export function selectorPointers(text) {
|
|
190
|
+
if (!text.includes('unsafeBitCast'))
|
|
191
|
+
return text;
|
|
192
|
+
return text.replace(/\bunsafeBitCast\(\s*(Selector\((?:[^()]|\([^()]*\))*\))\s*,\s*to:\s*UnsafeRawPointer\.self\s*\)/g, '$1._uniquePointer');
|
|
193
|
+
}
|
|
194
|
+
/** Whether a source imports ObjectiveC, which the SDK does not have on WASI (transformSource). */
|
|
195
|
+
export function importsObjectiveC(text) {
|
|
196
|
+
return importsOf(text).includes('ObjectiveC');
|
|
197
|
+
}
|
|
198
|
+
/** Whether a source imports Dispatch, which the SDK does not have on WASI (transformSource). */
|
|
199
|
+
export function importsDispatch(text) {
|
|
200
|
+
return importsOf(text).includes('Dispatch');
|
|
201
|
+
}
|
|
202
|
+
export function usesPosixMutex(text) {
|
|
203
|
+
return /\bpthread_mutex(?:attr)?_(?!t\b)[a-z]+\s*\(/.test(text);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Objective-C without its runtime (FoundationBridge/Selector.swift). The
|
|
207
|
+
* browser's Swift has no Objective-C interop, so `@objc` (and the Interface
|
|
208
|
+
* Builder attributes that imply it) is dropped, and `#selector(handle(_:))`
|
|
209
|
+
* becomes `Selector(Self.handle(_:), name: "handle(_:)")`: the method itself,
|
|
210
|
+
* which the target/selector APIs call on their target. A qualified method
|
|
211
|
+
* (`Type.method`) is used as it is; one of another object
|
|
212
|
+
* (`#selector(model.refresh)`) is called bound; `getter:`/`setter:`
|
|
213
|
+
* selectors only have a name. A method the file declares `@MainActor` is
|
|
214
|
+
* bound to its target in a closure instead,
|
|
215
|
+
* `Selector(name: "tick", isolated: { (target: Self) in target.tick })`:
|
|
216
|
+
* Swift 6 rejects naming a main-actor method of a class that is not
|
|
217
|
+
* `Sendable` unapplied (Gifu's display link proxy).
|
|
218
|
+
*/
|
|
219
|
+
export function rewriteObjC(text) {
|
|
220
|
+
if (!usesObjC(text))
|
|
221
|
+
return text;
|
|
222
|
+
let out = text.replace(/@(?:objc\b(?:\([^)\n]*\))?|(?:objcMembers|IBAction|IBOutlet|IBInspectable|IBDesignable)\b)[ \t]*/g, '');
|
|
223
|
+
const mainActorMethods = new Set([...out.matchAll(/@MainActor\b(?:\s*@\w+(?:\([^)\n]*\))?)*\s*(?:(?:public|private|fileprivate|internal|open|final|override|dynamic|nonisolated)\s+)*func\s+`?([A-Za-z_][A-Za-z0-9_]*)/g)].map((m) => m[1]));
|
|
224
|
+
let result = '';
|
|
225
|
+
let index = 0;
|
|
226
|
+
const selectors = /#selector\s*\(/g;
|
|
227
|
+
for (;;) {
|
|
228
|
+
selectors.lastIndex = index;
|
|
229
|
+
const match = selectors.exec(out);
|
|
230
|
+
if (!match)
|
|
231
|
+
break;
|
|
232
|
+
const open = match.index + match[0].length - 1;
|
|
233
|
+
const close = matchingParen(out, open);
|
|
234
|
+
if (close < 0)
|
|
235
|
+
break;
|
|
236
|
+
const inner = out.slice(open + 1, close).trim();
|
|
237
|
+
const name = inner.replace(/\s+/g, '');
|
|
238
|
+
result += out.slice(index, match.index);
|
|
239
|
+
const accessor = /^(?:getter|setter)\s*:\s*(.+)$/s.exec(inner);
|
|
240
|
+
let method = inner.replace(/^self\s*\.\s*/, '');
|
|
241
|
+
const qualified = /^([A-Z][A-Za-z0-9_]*)\s*\.\s*(.+)$/s.exec(method);
|
|
242
|
+
const unqualified = method.replace(/^`?([A-Za-z_][A-Za-z0-9_]*)`?/, '$1');
|
|
243
|
+
const base = /^([A-Za-z_][A-Za-z0-9_]*)/.exec(qualified ? qualified[2] : unqualified)?.[1];
|
|
244
|
+
if (accessor) {
|
|
245
|
+
result += `Selector(${JSON.stringify(accessor[1].replace(/\s+/g, ''))})`;
|
|
246
|
+
}
|
|
247
|
+
else if (base && mainActorMethods.has(base) && (qualified || !/^[A-Za-z_][A-Za-z0-9_]*\s*\./.test(method))) {
|
|
248
|
+
const type = qualified ? qualified[1] : 'Self';
|
|
249
|
+
const member = (qualified ? qualified[2] : unqualified).replace(/\s+/g, '');
|
|
250
|
+
result += `Selector(name: ${JSON.stringify(name)}, isolated: { (target: ${type}) in target.${member} })`;
|
|
251
|
+
}
|
|
252
|
+
else if (/^[A-Z][A-Za-z0-9_]*\s*\./.test(method)) {
|
|
253
|
+
result += `Selector(${method}, name: ${JSON.stringify(name)})`;
|
|
254
|
+
}
|
|
255
|
+
else if (/^[A-Za-z_][A-Za-z0-9_]*\s*\./.test(method)) {
|
|
256
|
+
result += `Selector(bound: ${method}, name: ${JSON.stringify(name)})`;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
method = method.replace(/^`?([A-Za-z_][A-Za-z0-9_]*)`?/, '$1');
|
|
260
|
+
result += `Selector(Self.${method}, name: ${JSON.stringify(name)})`;
|
|
261
|
+
}
|
|
262
|
+
index = close + 1;
|
|
263
|
+
}
|
|
264
|
+
result += out.slice(index);
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
/** Index of the `)` closing the `(` at `open`; -1 when unbalanced. */
|
|
268
|
+
function matchingParen(text, open) {
|
|
269
|
+
let depth = 0;
|
|
270
|
+
for (let i = open; i < text.length; i += 1) {
|
|
271
|
+
if (text[i] === '(')
|
|
272
|
+
depth += 1;
|
|
273
|
+
else if (text[i] === ')') {
|
|
274
|
+
depth -= 1;
|
|
275
|
+
if (depth === 0)
|
|
276
|
+
return i;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return -1;
|
|
280
|
+
}
|
|
281
|
+
/** A `#if` or `#elseif` line that tests `os(iOS)`. */
|
|
282
|
+
const IOS_CONDITION = /^[ \t]*#(?:if|elseif)\b[^\n]*\bos\(\s*iOS\s*\)[^\n]*$/gm;
|
|
283
|
+
/** `import UIKit.UIImage`-style imports of the frameworks the shim provides as Swift modules. */
|
|
284
|
+
const SUBMODULE_IMPORT = /^([ \t]*(?:@[A-Za-z_]+[ \t]+)*)import[ \t]+(CoreImage|UIKit|CoreGraphics|ImageIO|os)\.[A-Za-z_][A-Za-z0-9_.]*[ \t]*(\r?)$/gm;
|
|
285
|
+
/** Replaces every `#Preview …{ … }` block and `PreviewProvider` struct with blank lines of the same count. */
|
|
57
286
|
export function stripPreviews(text) {
|
|
58
287
|
let count = 0;
|
|
59
288
|
let result = '';
|
|
60
289
|
let index = 0;
|
|
61
|
-
const pattern = /^[ \t]
|
|
290
|
+
const pattern = /^[ \t]*(?:#Preview\b|(?:(?:@\w+(?:\([^)\n]*\))?|public|internal|fileprivate|private)\s+)*struct[ \t]+\w+[ \t]*:[^{\n]*\bPreviewProvider\b)/gm;
|
|
62
291
|
for (;;) {
|
|
63
292
|
pattern.lastIndex = index;
|
|
64
293
|
const match = pattern.exec(text);
|
|
65
294
|
if (!match)
|
|
66
295
|
break;
|
|
67
|
-
|
|
296
|
+
let start = match.index;
|
|
68
297
|
const open = text.indexOf('{', start);
|
|
69
298
|
if (open < 0)
|
|
70
299
|
break;
|
|
@@ -75,6 +304,16 @@ export function stripPreviews(text) {
|
|
|
75
304
|
let end = close + 1;
|
|
76
305
|
while (end < text.length && (text[end] === ' ' || text[end] === '\t'))
|
|
77
306
|
end += 1;
|
|
307
|
+
// Attribute lines just above it (`@available(iOS 17, *)`) go with it.
|
|
308
|
+
for (;;) {
|
|
309
|
+
const lineEnd = start - 1;
|
|
310
|
+
if (lineEnd < index || text[lineEnd] !== '\n')
|
|
311
|
+
break;
|
|
312
|
+
const lineStart = text.lastIndexOf('\n', lineEnd - 1) + 1;
|
|
313
|
+
if (lineStart < index || !/^[ \t]*@\w+(?:\([^)\n]*\))?[ \t]*\r?$/.test(text.slice(lineStart, lineEnd)))
|
|
314
|
+
break;
|
|
315
|
+
start = lineStart;
|
|
316
|
+
}
|
|
78
317
|
result += text.slice(index, start);
|
|
79
318
|
const removed = text.slice(start, end);
|
|
80
319
|
result += removed.replace(/[^\n]/g, '');
|
|
@@ -161,32 +400,76 @@ function skipString(text, start) {
|
|
|
161
400
|
}
|
|
162
401
|
return -1;
|
|
163
402
|
}
|
|
164
|
-
/** The
|
|
165
|
-
export function
|
|
403
|
+
/** The features `sources` (their text) call for. */
|
|
404
|
+
export function featuresOf(sources) {
|
|
405
|
+
const imported = new Set(sources.flatMap((text) => importsOf(text)));
|
|
406
|
+
// `import SwiftUI` brings UIKit along, as on iOS (transformSource).
|
|
407
|
+
if (imported.has('SwiftUI'))
|
|
408
|
+
imported.add('UIKit');
|
|
409
|
+
return {
|
|
410
|
+
swiftData: imported.has('SwiftData'),
|
|
411
|
+
frameworks: LINKED_FRAMEWORKS.filter((framework) => imported.has(framework)),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* The Package.swift of the staging package; with `tests` (the names of the
|
|
416
|
+
* app's test targets, for `swiftbrowser test`), a test target for each that
|
|
417
|
+
* depends on the app's.
|
|
418
|
+
*/
|
|
419
|
+
export function packageManifest(app, swiftPackagePath, features = { swiftData: false, frameworks: [] }, vendored = new Map(), tests = []) {
|
|
166
420
|
const languageMode = app.swiftLanguageMode === '6' ? '.v6' : '.v5';
|
|
167
421
|
const defines = ['SWIFTBROWSER', ...app.defines].map((d) => `.define(${JSON.stringify(d)})`).join(', ');
|
|
168
|
-
|
|
169
|
-
|
|
422
|
+
// The Xcode target's concurrency settings (buildSettings.ts);
|
|
423
|
+
// `.defaultIsolation` needs tools version 6.2.
|
|
424
|
+
const concurrency = swiftSettingsEntries(app.swiftSettings);
|
|
425
|
+
const toolsVersion = app.swiftSettings?.defaultIsolationMainActor ? '6.2' : '6.0';
|
|
426
|
+
const swiftSettings = [defines, `.swiftLanguageMode(${languageMode})`, ...concurrency].join(', ');
|
|
427
|
+
const products = [...new Set(['SwiftUI', 'Charts', 'FoundationBridge', 'os', ...(features.swiftData ? ['SwiftData'] : []), ...features.frameworks])];
|
|
428
|
+
// The Xcode project's package dependencies that build for the browser are
|
|
429
|
+
// real dependencies; the ones the shim stands in for are linked above, by
|
|
430
|
+
// import (packages.ts). A package that imports SwiftUI is built from its
|
|
431
|
+
// copy, adjusted for the shim (vendor.ts); a copy the app does not depend on
|
|
432
|
+
// directly is declared too, so it takes the original's place deeper in the
|
|
433
|
+
// graph.
|
|
434
|
+
const packages = planPackages(app.packages).flatMap((plan) => (plan.kind === 'dependency' ? [plan] : []));
|
|
435
|
+
const copy = (identity) => {
|
|
436
|
+
const dir = vendored.get(identity)?.dir;
|
|
437
|
+
return dir ? `.package(path: ${JSON.stringify(dir)})` : undefined;
|
|
438
|
+
};
|
|
439
|
+
const transitive = [...vendored.keys()].filter((identity) => !packages.some((plan) => plan.identity === identity));
|
|
440
|
+
const linked = [
|
|
170
441
|
...products.map((p) => `.product(name: ${JSON.stringify(p)}, package: "SwiftBrowser")`),
|
|
171
|
-
...(
|
|
172
|
-
]
|
|
442
|
+
...packages.flatMap((plan) => plan.package.products.map((p) => `.product(name: ${JSON.stringify(p)}, package: ${JSON.stringify(plan.identity)})`)),
|
|
443
|
+
];
|
|
444
|
+
const deps = [...linked, JSON.stringify(ICU_TARGET)].join(',\n ');
|
|
445
|
+
// A test module imports what the app's does (and `@testable import`s the app).
|
|
446
|
+
const testTargets = tests.map((name) => `
|
|
447
|
+
.testTarget(
|
|
448
|
+
name: ${JSON.stringify(name)},
|
|
449
|
+
dependencies: [
|
|
450
|
+
${[JSON.stringify(app.name), ...linked].join(',\n ')}
|
|
451
|
+
],
|
|
452
|
+
path: ${JSON.stringify(testTargetPath(name).split(path.sep).join('/'))},
|
|
453
|
+
swiftSettings: [${swiftSettings}]
|
|
454
|
+
),`).join('');
|
|
455
|
+
const packageDeps = [
|
|
456
|
+
`.package(name: "SwiftBrowser", path: ${JSON.stringify(swiftPackagePath)})`,
|
|
457
|
+
...packages.map((plan) => copy(plan.identity) ?? plan.declaration),
|
|
458
|
+
...transitive.map((identity) => copy(identity)),
|
|
459
|
+
];
|
|
173
460
|
// Locale.current follows the host's choice (SB_LOCALE): the two functions
|
|
174
461
|
// that hard-code en_001 in swift-foundation are redirected to FoundationBridge.
|
|
175
|
-
const localeWrap =
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
// (and preferred languages) to FoundationBridge/Locale.swift.
|
|
462
|
+
const localeWrap = `
|
|
463
|
+
// Route swift-foundation's hard-coded current locale (and preferred
|
|
464
|
+
// languages) to FoundationBridge/Locale.swift.
|
|
179
465
|
+ ["-Xlinker", "--wrap=$s20FoundationEssentials11LocaleCacheV11preferencesAA0C11PreferencesV_SbtyF",
|
|
180
|
-
"-Xlinker", "--wrap=$s20FoundationEssentials6LocaleV18preferredLanguagesSaySSGvgZ"]
|
|
181
|
-
|
|
182
|
-
const icuTarget = app.foundation === 'full'
|
|
183
|
-
? `
|
|
466
|
+
"-Xlinker", "--wrap=$s20FoundationEssentials6LocaleV18preferredLanguagesSaySSGvgZ"]`;
|
|
467
|
+
const icuTarget = `
|
|
184
468
|
// Foundation's ICU reads its locale data from a symbol this target
|
|
185
469
|
// defines, trimmed to the app's languages, ahead of the SDK's 34 MB
|
|
186
470
|
// original (see @swiftbrowser/cli src/icu.ts).
|
|
187
|
-
.target(name: ${JSON.stringify(ICU_TARGET)}, path: ${JSON.stringify(ICU_TARGET)})
|
|
188
|
-
|
|
189
|
-
return `// swift-tools-version: 6.0
|
|
471
|
+
.target(name: ${JSON.stringify(ICU_TARGET)}, path: ${JSON.stringify(ICU_TARGET)}),${testTargets}`;
|
|
472
|
+
return `// swift-tools-version: ${toolsVersion}
|
|
190
473
|
// Generated by @swiftbrowser/cli for ${app.displayName}. Do not edit: it is
|
|
191
474
|
// rewritten on every build. Sources/ holds the app's sources, adjusted for
|
|
192
475
|
// the shim (see stage.ts); the SwiftBrowser package is referenced by path.
|
|
@@ -194,7 +477,8 @@ import PackageDescription
|
|
|
194
477
|
|
|
195
478
|
// C-ABI entry points the browser renderer calls; the linker must keep them.
|
|
196
479
|
let wasmExports = ["sb_event", "sb_ops_ptr", "sb_ops_len", "sb_ops_clear",
|
|
197
|
-
"sb_alloc", "sb_event_json", "sb_state_ptr", "sb_state_len", "sb_run_jobs"
|
|
480
|
+
"sb_alloc", "sb_event_json", "sb_state_ptr", "sb_state_len", "sb_run_jobs",
|
|
481
|
+
"sb_bytes", "sb_bytes_count", "sb_bytes_ptr", "sb_bytes_len"]
|
|
198
482
|
.flatMap { ["-Xlinker", "--export=\\($0)"] }
|
|
199
483
|
// The reconciler recurses once per view; 8 MB of stack like a native thread.
|
|
200
484
|
+ ["-Xlinker", "-z", "-Xlinker", "stack-size=8388608"]${localeWrap}
|
|
@@ -203,7 +487,7 @@ let package = Package(
|
|
|
203
487
|
name: ${JSON.stringify(app.name)},
|
|
204
488
|
platforms: [.macOS(.v14)],
|
|
205
489
|
products: [.executable(name: ${JSON.stringify(app.name)}, targets: [${JSON.stringify(app.name)}])],
|
|
206
|
-
dependencies: [.
|
|
490
|
+
dependencies: [${packageDeps.length === 1 ? packageDeps[0] : `\n ${packageDeps.join(',\n ')},\n `}],
|
|
207
491
|
targets: [
|
|
208
492
|
.executableTarget(
|
|
209
493
|
name: ${JSON.stringify(app.name)},
|
|
@@ -211,33 +495,61 @@ let package = Package(
|
|
|
211
495
|
${deps}
|
|
212
496
|
],
|
|
213
497
|
path: "Sources",
|
|
214
|
-
swiftSettings: [${
|
|
498
|
+
swiftSettings: [${swiftSettings}],
|
|
215
499
|
linkerSettings: [.unsafeFlags(wasmExports, .when(platforms: [.wasi]))]
|
|
216
500
|
),${icuTarget}
|
|
217
501
|
]
|
|
218
502
|
)
|
|
219
503
|
`;
|
|
220
504
|
}
|
|
505
|
+
/** Where the compiled string tables go in the staging directory (`<language>.lproj` folders). */
|
|
506
|
+
export const LOCALIZATION_DIR = 'Localization';
|
|
221
507
|
/** The staged file that settles names declared by both the shim and Foundation. */
|
|
222
508
|
export const FULL_FOUNDATION_FILE = '_SwiftBrowserFoundation.swift';
|
|
223
509
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* its `Timer` needs CFRunLoop and does not link).
|
|
510
|
+
* The app imports both the shim and Foundation, and both declare a few names
|
|
511
|
+
* (a module does not shadow another's declarations, only the current module's
|
|
512
|
+
* do). A typealias in the app module does: these point the shared names at
|
|
513
|
+
* the shim's types (what its `frame(width:)` and friends take) and at
|
|
514
|
+
* FoundationBridge's `Timer` and `RunLoop`, which Foundation lacks on WASI
|
|
515
|
+
* (`RunLoop` is marked unavailable, its `Timer` needs CFRunLoop and does not
|
|
516
|
+
* link).
|
|
232
517
|
*/
|
|
233
|
-
export function fullFoundationSource() {
|
|
518
|
+
export function fullFoundationSource(features = { swiftData: false, frameworks: [] }, declared = new Set()) {
|
|
519
|
+
// A name the module declares itself (an app's own `Operation` or `Timer`)
|
|
520
|
+
// is the module's: an alias of it would be a redeclaration.
|
|
521
|
+
const text = fullFoundationTemplate(features);
|
|
522
|
+
if (declared.size === 0)
|
|
523
|
+
return text;
|
|
524
|
+
return text
|
|
525
|
+
.split('\n')
|
|
526
|
+
.filter((line) => {
|
|
527
|
+
const alias = /^public typealias ([A-Za-z_][A-Za-z0-9_]*) = /.exec(line);
|
|
528
|
+
return !alias || !declared.has(alias[1]);
|
|
529
|
+
})
|
|
530
|
+
.join('\n');
|
|
531
|
+
}
|
|
532
|
+
/** The type names a module's sources declare at the top level (lines that start with the declaration). */
|
|
533
|
+
export function declaredTypeNames(sources) {
|
|
534
|
+
const names = new Set();
|
|
535
|
+
const pattern = /^(?:@[A-Za-z_]+(?:\([^)\n]*\))?\s+)*(?:(?:public|package|internal|private|fileprivate|open|final|indirect)\s+)*(?:class|struct|enum|protocol|actor|typealias)\s+([A-Za-z_][A-Za-z0-9_]*)/gm;
|
|
536
|
+
for (const text of sources) {
|
|
537
|
+
for (const match of text.matchAll(pattern))
|
|
538
|
+
names.add(match[1]);
|
|
539
|
+
}
|
|
540
|
+
return names;
|
|
541
|
+
}
|
|
542
|
+
function fullFoundationTemplate(features) {
|
|
234
543
|
return `// Generated by @swiftbrowser/cli; rewritten on every build.
|
|
235
544
|
// The shim and Foundation both declare these names. A typealias in the app
|
|
236
545
|
// module settles each in the shim's favor (SwiftUI's geometry) or on the
|
|
237
546
|
// bridge (Timer and RunLoop, which the SDK's Foundation does not have on WASI).
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
import
|
|
547
|
+
// The imports are public: a package may make imports internal by default
|
|
548
|
+
// (\`InternalImportsByDefault\`), and public aliases need public imports.
|
|
549
|
+
public import SwiftUI
|
|
550
|
+
public import Foundation
|
|
551
|
+
public import FoundationBridge
|
|
552
|
+
public import os
|
|
241
553
|
|
|
242
554
|
// Public, so the app's own public API may use the names (a public method of
|
|
243
555
|
// a Layout returning CGSize, for one).
|
|
@@ -251,12 +563,101 @@ public typealias RunLoop = FoundationBridge.RunLoop
|
|
|
251
563
|
// Foundation's own cannot persist on WASI, and its Bundle.main traps.
|
|
252
564
|
public typealias UserDefaults = FoundationBridge.UserDefaults
|
|
253
565
|
public typealias Bundle = FoundationBridge.Bundle
|
|
254
|
-
|
|
566
|
+
// Networking runs over the browser's fetch; Foundation's own is unavailable on
|
|
567
|
+
// WASI (it moved to FoundationNetworking, which the SDK does not ship).
|
|
568
|
+
public typealias URLSession = FoundationBridge.URLSession
|
|
569
|
+
public typealias URLSessionConfiguration = FoundationBridge.URLSessionConfiguration
|
|
570
|
+
public typealias URLSessionTask = FoundationBridge.URLSessionTask
|
|
571
|
+
public typealias URLSessionDataTask = FoundationBridge.URLSessionDataTask
|
|
572
|
+
public typealias URLSessionUploadTask = FoundationBridge.URLSessionUploadTask
|
|
573
|
+
public typealias URLSessionDelegate = FoundationBridge.URLSessionDelegate
|
|
574
|
+
public typealias URLSessionTaskDelegate = FoundationBridge.URLSessionTaskDelegate
|
|
575
|
+
public typealias URLSessionDataDelegate = FoundationBridge.URLSessionDataDelegate
|
|
576
|
+
public typealias URLRequest = FoundationBridge.URLRequest
|
|
577
|
+
public typealias URLResponse = FoundationBridge.URLResponse
|
|
578
|
+
public typealias HTTPURLResponse = FoundationBridge.HTTPURLResponse
|
|
579
|
+
public typealias URLError = FoundationBridge.URLError
|
|
580
|
+
// The SDK has no Dispatch on WASI; the bridge's queues run on the main actor.
|
|
581
|
+
public typealias DispatchQueue = FoundationBridge.DispatchQueue
|
|
582
|
+
public typealias DispatchTime = FoundationBridge.DispatchTime
|
|
583
|
+
public typealias DispatchTimeInterval = FoundationBridge.DispatchTimeInterval
|
|
584
|
+
public typealias DispatchQoS = FoundationBridge.DispatchQoS
|
|
585
|
+
public typealias DispatchSpecificKey = FoundationBridge.DispatchSpecificKey
|
|
586
|
+
// Dispatch declares DispatchTime's arithmetic globally (so that .now() + 0.5
|
|
587
|
+
// finds it); declared here, it reaches files that import only Foundation.
|
|
588
|
+
func + (time: DispatchTime, seconds: Double) -> DispatchTime { time._adding(nanoseconds: seconds * 1_000_000_000) }
|
|
589
|
+
func - (time: DispatchTime, seconds: Double) -> DispatchTime { time._adding(nanoseconds: -seconds * 1_000_000_000) }
|
|
590
|
+
func + (time: DispatchTime, interval: DispatchTimeInterval) -> DispatchTime { FoundationBridge.DispatchTime._plus(time, interval) }
|
|
591
|
+
func - (time: DispatchTime, interval: DispatchTimeInterval) -> DispatchTime { FoundationBridge.DispatchTime._minus(time, interval) }
|
|
592
|
+
public typealias DispatchSource = FoundationBridge.DispatchSource
|
|
593
|
+
public typealias DispatchWorkItem = FoundationBridge.DispatchWorkItem
|
|
594
|
+
public typealias DispatchWorkItemFlags = FoundationBridge.DispatchWorkItemFlags
|
|
595
|
+
public typealias DispatchGroup = FoundationBridge.DispatchGroup
|
|
596
|
+
public typealias DispatchSourceProtocol = FoundationBridge.DispatchSourceProtocol
|
|
597
|
+
public typealias DispatchSourceMemoryPressure = FoundationBridge.DispatchSourceMemoryPressure
|
|
598
|
+
public typealias DispatchSourceTimer = FoundationBridge.DispatchSourceTimer
|
|
599
|
+
// Nor Operation and OperationQueue (the bridge's run on the main actor), nor
|
|
600
|
+
// the rest of URL loading's types.
|
|
601
|
+
public typealias Operation = FoundationBridge.Operation
|
|
602
|
+
public typealias BlockOperation = FoundationBridge.BlockOperation
|
|
603
|
+
public typealias OperationQueue = FoundationBridge.OperationQueue
|
|
604
|
+
public typealias URLCache = FoundationBridge.URLCache
|
|
605
|
+
public typealias CachedURLResponse = FoundationBridge.CachedURLResponse
|
|
606
|
+
public typealias URLSessionTaskMetrics = FoundationBridge.URLSessionTaskMetrics
|
|
607
|
+
public typealias URLAuthenticationChallenge = FoundationBridge.URLAuthenticationChallenge
|
|
608
|
+
public typealias URLCredential = FoundationBridge.URLCredential
|
|
609
|
+
public typealias URLProtectionSpace = FoundationBridge.URLProtectionSpace
|
|
610
|
+
public typealias URLSessionWebSocketTask = FoundationBridge.URLSessionWebSocketTask
|
|
611
|
+
// Objective-C's selectors without its runtime (FoundationBridge/Selector.swift).
|
|
612
|
+
public typealias Selector = FoundationBridge.Selector
|
|
613
|
+
// WASI's Foundation keeps only the message-based center; the bridge has the classic one.
|
|
614
|
+
public typealias NotificationCenter = FoundationBridge.NotificationCenter
|
|
615
|
+
func NSSelectorFromString(_ name: String) -> FoundationBridge.Selector { FoundationBridge.NSSelectorFromString(name) }
|
|
616
|
+
func NSStringFromSelector(_ selector: FoundationBridge.Selector) -> String { FoundationBridge.NSStringFromSelector(selector) }
|
|
617
|
+
public typealias URLSessionWebSocketDelegate = FoundationBridge.URLSessionWebSocketDelegate
|
|
618
|
+
// Nor RelativeDateTimeFormatter ("2 hr. ago").
|
|
619
|
+
public typealias RelativeDateTimeFormatter = FoundationBridge.RelativeDateTimeFormatter
|
|
620
|
+
// What Darwin's Foundation brings along: the Core Foundation clock, the
|
|
621
|
+
// Objective-C runtime's autorelease pools and associated objects, and os's lock.
|
|
622
|
+
public typealias CFAbsoluteTime = FoundationBridge.CFAbsoluteTime
|
|
623
|
+
public typealias CFTimeInterval = FoundationBridge.CFTimeInterval
|
|
624
|
+
func CFAbsoluteTimeGetCurrent() -> CFAbsoluteTime { FoundationBridge.CFAbsoluteTimeGetCurrent() }
|
|
625
|
+
func autoreleasepool<Result>(invoking body: () throws -> Result) rethrows -> Result { try FoundationBridge.autoreleasepool(invoking: body) }
|
|
626
|
+
public typealias objc_AssociationPolicy = FoundationBridge.objc_AssociationPolicy
|
|
627
|
+
func objc_setAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: Any?, _ policy: objc_AssociationPolicy) { FoundationBridge.objc_setAssociatedObject(object, key, value, policy) }
|
|
628
|
+
func objc_getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> Any? { FoundationBridge.objc_getAssociatedObject(object, key) }
|
|
629
|
+
func class_getInstanceSize(_ cls: AnyClass?) -> Int { FoundationBridge.class_getInstanceSize(cls) }
|
|
630
|
+
func sysctlbyname(_ name: UnsafePointer<CChar>?, _ oldp: UnsafeMutableRawPointer?, _ oldlenp: UnsafeMutablePointer<Int>?, _ newp: UnsafeMutableRawPointer?, _ newlen: Int) -> Int32 { FoundationBridge.sysctlbyname(name, oldp, oldlenp, newp, newlen) }
|
|
631
|
+
// ProcessInfo.processInfo.physicalMemory, which reads 0 on WASI (the staging rewrites it to this).
|
|
632
|
+
var _swiftBrowserPhysicalMemory: UInt64 { FoundationBridge._physicalMemory }
|
|
633
|
+
public typealias os_unfair_lock = os.os_unfair_lock
|
|
634
|
+
public typealias os_unfair_lock_t = os.os_unfair_lock_t
|
|
635
|
+
public typealias os_unfair_lock_s = os.os_unfair_lock_s
|
|
636
|
+
func os_unfair_lock_lock(_ lock: os_unfair_lock_t) { os.os_unfair_lock_lock(lock) }
|
|
637
|
+
func os_unfair_lock_unlock(_ lock: os_unfair_lock_t) { os.os_unfair_lock_unlock(lock) }
|
|
638
|
+
func os_unfair_lock_trylock(_ lock: os_unfair_lock_t) -> Bool { os.os_unfair_lock_trylock(lock) }
|
|
639
|
+
${features.swiftData ? SWIFTDATA_PREDICATE : ''}`;
|
|
255
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* For an app that imports SwiftData: `#Predicate` expands with SwiftData's
|
|
643
|
+
* copy of Foundation's macro, which accepts `localizedStandardContains`,
|
|
644
|
+
* `localizedCompare` and `caseInsensitiveCompare` as Apple's does (the
|
|
645
|
+
* open-source macro rejects them). A macro declared in the app module shadows
|
|
646
|
+
* Foundation's; the type `Predicate` stays Foundation's.
|
|
647
|
+
*/
|
|
648
|
+
const SWIFTDATA_PREDICATE = `
|
|
649
|
+
// SwiftData: #Predicate as on Apple platforms (Sources/SwiftDataMacros/PredicateMacro.swift).
|
|
650
|
+
@freestanding(expression)
|
|
651
|
+
macro Predicate<each Input>(_ body: (repeat each Input) -> Bool) -> Foundation.Predicate<repeat each Input> = #externalMacro(module: "SwiftDataMacros", type: "PredicateMacro")
|
|
652
|
+
`;
|
|
256
653
|
/** The directory the staged sources keep their layout relative to. */
|
|
257
654
|
export function commonSourceBase(app) {
|
|
258
|
-
|
|
259
|
-
|
|
655
|
+
return commonDirectory(app.sources);
|
|
656
|
+
}
|
|
657
|
+
/** The deepest directory holding every one of `files`. */
|
|
658
|
+
export function commonDirectory(files) {
|
|
659
|
+
let base = path.dirname(files[0]);
|
|
660
|
+
for (const file of files) {
|
|
260
661
|
while (!file.startsWith(`${base}${path.sep}`) && base !== path.dirname(base))
|
|
261
662
|
base = path.dirname(base);
|
|
262
663
|
}
|
|
@@ -287,19 +688,24 @@ function listFiles(dir, out = []) {
|
|
|
287
688
|
* staged files whose source is gone are removed.
|
|
288
689
|
*/
|
|
289
690
|
export function stage(app, swiftPackagePath, options = {}) {
|
|
290
|
-
const
|
|
691
|
+
const tests = options.tests ? (app.tests ?? []) : [];
|
|
692
|
+
const dir = options.tests ? testStagingDir(app) : stagingDir(app);
|
|
291
693
|
const sourcesDir = path.join(dir, 'Sources');
|
|
292
694
|
mkdirSync(sourcesDir, { recursive: true });
|
|
293
|
-
|
|
695
|
+
const texts = new Map(app.sources.map((file) => [file, readFileSync(file, 'utf8')]));
|
|
696
|
+
const testTexts = new Map(tests.flatMap((target) => target.sources).map((file) => [file, readFileSync(file, 'utf8')]));
|
|
697
|
+
const models = compileModels(app.models ?? []);
|
|
698
|
+
// The generated model classes import CoreML, which links it; a test that
|
|
699
|
+
// imports a framework links it as well.
|
|
700
|
+
const features = featuresOf([...texts.values(), ...models.map((model) => model.source), ...testTexts.values()]);
|
|
701
|
+
const optionalRequirements = optionalRequirementNames(texts.values());
|
|
702
|
+
writeIfChanged(path.join(dir, 'Package.swift'), packageManifest(app, swiftPackagePath, features, options.vendored, tests.map((target) => target.name)));
|
|
294
703
|
const icuDir = path.join(dir, ICU_TARGET);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
else {
|
|
301
|
-
rmSync(icuDir, { recursive: true, force: true });
|
|
302
|
-
}
|
|
704
|
+
// Tests link the SDK's whole ICU data: a test bundle links its
|
|
705
|
+
// dependencies' objects as one archive, after the SDK's (no trimmed data).
|
|
706
|
+
writeIfChanged(path.join(icuDir, 'icudt.S'), icuAssembly(options.tests ? null : (options.icu ?? null)));
|
|
707
|
+
// SwiftPM insists on a public headers directory for a C target.
|
|
708
|
+
writeIfChanged(path.join(icuDir, 'include', `${ICU_TARGET}.h`), '// Nothing to include: the target holds data only.\n');
|
|
303
709
|
const base = commonSourceBase(app);
|
|
304
710
|
const originals = new Map();
|
|
305
711
|
const keep = new Set();
|
|
@@ -307,16 +713,26 @@ export function stage(app, swiftPackagePath, options = {}) {
|
|
|
307
713
|
for (const file of app.sources) {
|
|
308
714
|
const relative = path.relative(base, file);
|
|
309
715
|
const staged = path.join(sourcesDir, relative);
|
|
310
|
-
const transformed = transformSource(
|
|
716
|
+
const transformed = transformSource(texts.get(file), { bridged: true, optionalRequirements });
|
|
311
717
|
previewsStripped += transformed.previewsStripped;
|
|
312
718
|
writeIfChanged(staged, transformed.text);
|
|
313
719
|
originals.set(staged, file);
|
|
314
720
|
keep.add(staged);
|
|
315
721
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
722
|
+
const generated = path.join(sourcesDir, FULL_FOUNDATION_FILE);
|
|
723
|
+
writeIfChanged(generated, fullFoundationSource(features, declaredTypeNames([...texts.values()])));
|
|
724
|
+
keep.add(generated);
|
|
725
|
+
const assetSymbols = app.assetSymbols === false ? ''
|
|
726
|
+
: assetSymbolsSource(app.catalogs, { extensions: app.assetSymbols === 'extensions', declared: declaredStaticMembers([...texts.values()]) });
|
|
727
|
+
if (assetSymbols) {
|
|
728
|
+
const file = path.join(sourcesDir, ASSET_SYMBOLS_FILE);
|
|
729
|
+
writeIfChanged(file, assetSymbols);
|
|
730
|
+
keep.add(file);
|
|
731
|
+
}
|
|
732
|
+
for (const model of models) {
|
|
733
|
+
const file = path.join(sourcesDir, modelSourceName(model));
|
|
734
|
+
writeIfChanged(file, model.source);
|
|
735
|
+
keep.add(file);
|
|
320
736
|
}
|
|
321
737
|
for (const stale of listFiles(sourcesDir)) {
|
|
322
738
|
if (!keep.has(stale))
|
|
@@ -324,7 +740,57 @@ export function stage(app, swiftPackagePath, options = {}) {
|
|
|
324
740
|
}
|
|
325
741
|
// Drop directories left empty by removals.
|
|
326
742
|
pruneEmptyDirs(sourcesDir, sourcesDir);
|
|
327
|
-
|
|
743
|
+
// Each test target's sources, adjusted like the app's, with the alias file
|
|
744
|
+
// as well: a test module imports Foundation and the app, which both declare
|
|
745
|
+
// `UserDefaults`, `URLSession`…; its own aliases settle them as the app's do.
|
|
746
|
+
const testsDir = path.join(dir, 'Tests');
|
|
747
|
+
const keepTests = new Set();
|
|
748
|
+
for (const target of tests) {
|
|
749
|
+
const targetDir = path.join(dir, testTargetPath(target.name));
|
|
750
|
+
const targetBase = commonDirectory(target.sources);
|
|
751
|
+
const testRequirements = optionalRequirementNames([...texts.values(), ...target.sources.map((file) => testTexts.get(file))]);
|
|
752
|
+
for (const file of target.sources) {
|
|
753
|
+
const staged = path.join(targetDir, path.relative(targetBase, file));
|
|
754
|
+
const transformed = transformSource(testTexts.get(file), { bridged: true, optionalRequirements: testRequirements });
|
|
755
|
+
writeIfChanged(staged, transformed.text);
|
|
756
|
+
originals.set(staged, file);
|
|
757
|
+
keepTests.add(staged);
|
|
758
|
+
}
|
|
759
|
+
const aliases = path.join(targetDir, FULL_FOUNDATION_FILE);
|
|
760
|
+
writeIfChanged(aliases, fullFoundationSource(features, declaredTypeNames(target.sources.map((file) => testTexts.get(file)))));
|
|
761
|
+
keepTests.add(aliases);
|
|
762
|
+
}
|
|
763
|
+
if (existsSync(testsDir)) {
|
|
764
|
+
for (const stale of listFiles(testsDir)) {
|
|
765
|
+
if (!keepTests.has(stale))
|
|
766
|
+
rmSync(stale, { force: true });
|
|
767
|
+
}
|
|
768
|
+
pruneEmptyDirs(testsDir, testsDir);
|
|
769
|
+
}
|
|
770
|
+
// Errors in a copied local package name the user's file, as the app's do.
|
|
771
|
+
for (const copy of options.vendored?.values() ?? []) {
|
|
772
|
+
if (copy.local)
|
|
773
|
+
originals.set(`${copy.dir}${path.sep}`, `${copy.source}${path.sep}`);
|
|
774
|
+
}
|
|
775
|
+
// The string tables, compiled beside the package (not a source of it).
|
|
776
|
+
const localization = compileLocalizations(app.localizations ?? [], app.locales, path.join(dir, LOCALIZATION_DIR));
|
|
777
|
+
return { dir, originals, previewsStripped, localizations: localization.dirs, localizationWarnings: localization.warnings };
|
|
778
|
+
}
|
|
779
|
+
/** The staged file of a model's generated class: `<Class>.mlmodel.swift`, a name no app source has. */
|
|
780
|
+
export function modelSourceName(model) {
|
|
781
|
+
return `${model.className}.mlmodel.swift`;
|
|
782
|
+
}
|
|
783
|
+
/** Compiles each Core ML model of the app (coreml.ts); two models may not make the same class. */
|
|
784
|
+
export function compileModels(files) {
|
|
785
|
+
const models = files.map((file) => compileModel(file));
|
|
786
|
+
const byClass = new Map();
|
|
787
|
+
for (const model of models) {
|
|
788
|
+
const other = byClass.get(model.className);
|
|
789
|
+
if (other)
|
|
790
|
+
throw new ModelError(`two Core ML models would generate the class ${model.className}:\n ${other}\n ${model.file}`);
|
|
791
|
+
byClass.set(model.className, model.file);
|
|
792
|
+
}
|
|
793
|
+
return models;
|
|
328
794
|
}
|
|
329
795
|
function pruneEmptyDirs(dir, root) {
|
|
330
796
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|