@swiftbrowser/cli 0.1.0 → 0.2.0

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