@swiftbrowser/native 0.11.0 → 0.13.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 (46) hide show
  1. package/README.md +38 -1
  2. package/bin/swiftbrowser-native.mjs +27 -8
  3. package/dist/bridge.d.ts +7 -0
  4. package/dist/bridge.d.ts.map +1 -1
  5. package/dist/bridge.js.map +1 -1
  6. package/dist/build.d.ts +22 -2
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +25 -8
  9. package/dist/build.js.map +1 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/runtime.d.ts +7 -3
  15. package/dist/runtime.d.ts.map +1 -1
  16. package/dist/runtime.js +16 -3
  17. package/dist/runtime.js.map +1 -1
  18. package/dist/site.d.ts +8 -0
  19. package/dist/site.d.ts.map +1 -1
  20. package/dist/site.js +24 -5
  21. package/dist/site.js.map +1 -1
  22. package/dist/worker.d.ts +87 -0
  23. package/dist/worker.d.ts.map +1 -0
  24. package/dist/worker.js +213 -0
  25. package/dist/worker.js.map +1 -0
  26. package/package.json +3 -3
  27. package/src/bridge.ts +12 -0
  28. package/src/build.ts +40 -8
  29. package/src/index.ts +1 -0
  30. package/src/runtime.ts +16 -3
  31. package/src/site.ts +31 -5
  32. package/src/worker.ts +236 -0
  33. package/swift/Package.swift +1 -0
  34. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +46 -2
  35. package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +2 -1
  36. package/swift/Sources/SwiftBrowserNative/Host/WorkerHost.swift +279 -0
  37. package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +8 -1
  38. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Charts.swift +340 -0
  39. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Inspector.swift +29 -0
  40. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +8 -1
  41. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +58 -6
  42. package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +25 -7
  43. package/swift/Sources/SwiftBrowserProtocol/Props.swift +170 -0
  44. package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +2 -0
  45. package/swift/Sources/SwiftBrowserProtocol/Style.swift +41 -0
  46. package/wasm/SwiftBrowserNative.wasm +2 -2
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Workers for a JavaScriptCore app: a second context on its own thread, as
3
+ * a page has Web Workers. The host (Native/Sources/SwiftBrowserNative/Host/WorkerHost.swift)
4
+ * runs each one in a `JSVirtualMachine` of its own with a run loop, the
5
+ * runtime's globals (timers, `fetch`, `console`, `TextDecoder`) and the
6
+ * bundle `swiftbrowser-native build --worker <name>=<entry>` wrote next to
7
+ * app.js (`<name>.worker.js`). Messages cross as text: JSON, with typed
8
+ * arrays, `ArrayBuffer`s and `Error`s encoded (`encodeMessage`), since
9
+ * values cannot be shared between virtual machines. Drawn in a page
10
+ * (`swiftbrowser-native site`), the same app code gets the page's own
11
+ * `Worker` over the same bundle, published with the site.
12
+ *
13
+ * ```ts
14
+ * import { createWorker } from '@swiftbrowser/native';
15
+ * const worker = createWorker('render'); // render.worker.js
16
+ * worker.onmessage = (event) => console.log(event.data);
17
+ * worker.postMessage({ draw: 'something' });
18
+ * ```
19
+ */
20
+ import { type NativeBridge } from './bridge';
21
+ /** What `Worker` and a worker's `self` have in common, as an app uses them. */
22
+ export interface WorkerLike {
23
+ postMessage(data: unknown, transfer?: unknown[]): void;
24
+ terminate(): void;
25
+ onmessage: ((event: WorkerMessageEvent) => void) | null;
26
+ onerror: ((event: WorkerErrorEvent) => void) | null;
27
+ addEventListener(type: 'message', listener: (event: WorkerMessageEvent) => void): void;
28
+ addEventListener(type: 'error', listener: (event: WorkerErrorEvent) => void): void;
29
+ removeEventListener(type: 'message' | 'error', listener: (event: never) => void): void;
30
+ }
31
+ export interface WorkerMessageEvent {
32
+ type: 'message';
33
+ data: unknown;
34
+ }
35
+ export interface WorkerErrorEvent {
36
+ type: 'error';
37
+ message: string;
38
+ }
39
+ /**
40
+ * A message as text: JSON, with a typed array or `ArrayBuffer` as
41
+ * `{ $sbBytes, $sbType }` (base64) and an `Error` as `{ $sbError }`. What
42
+ * JSON drops (functions, `undefined` in an object, a `Map`) is dropped here
43
+ * too; a `Date` arrives as its ISO string.
44
+ */
45
+ export declare function encodeMessage(data: unknown): string;
46
+ /** The value `encodeMessage` encoded. */
47
+ export declare function decodeMessage(text: string): unknown;
48
+ type Listener<E> = (event: E) => void;
49
+ /** `Worker` over the bridge, in the app's context. */
50
+ export declare class NativeWorker implements WorkerLike {
51
+ #private;
52
+ private static nextId;
53
+ private static readonly live;
54
+ onmessage: Listener<WorkerMessageEvent> | null;
55
+ onerror: Listener<WorkerErrorEvent> | null;
56
+ /** `name` is the bundle's: `<name>.worker.js` next to app.js. A URL's last path segment serves as the name (`render.worker.js` → `render`). */
57
+ constructor(name: string | {
58
+ href: string;
59
+ } | URL, native?: NativeBridge | undefined);
60
+ postMessage(data: unknown): void;
61
+ terminate(): void;
62
+ addEventListener(type: 'message', listener: Listener<WorkerMessageEvent>): void;
63
+ addEventListener(type: 'error', listener: Listener<WorkerErrorEvent>): void;
64
+ removeEventListener(type: 'message' | 'error', listener: Listener<never>): void;
65
+ /** The bridge's `__sbWorkerMessage(id, text)`. */
66
+ static receive(id: number, text: string): void;
67
+ /** The bridge's `__sbWorkerError(id, message)`: an uncaught exception in the worker. */
68
+ static fail(id: number, message: string): void;
69
+ }
70
+ /** The bundle name a worker was asked for: `render`, or the `render` of `.../render.worker.js`. */
71
+ export declare function workerName(name: string | {
72
+ href: string;
73
+ } | URL): string;
74
+ /**
75
+ * A worker from the bundle `<name>.worker.js`: the page's own `Worker` when
76
+ * the app is drawn in a page (the bundle published with the site), else the
77
+ * host's over the bridge.
78
+ */
79
+ export declare function createWorker(name: string): WorkerLike;
80
+ /**
81
+ * A worker's own globals over its bridge: `self`, `postMessage`, `onmessage`
82
+ * and `onerror` as properties the script assigns, `addEventListener`,
83
+ * `close`, and `__sbWorkerReceive`, which the host calls with each message.
84
+ */
85
+ export declare function installWorkerScope(native: NativeBridge, target: Record<string, unknown>): string[];
86
+ export {};
87
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"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"}
package/dist/worker.js ADDED
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Workers for a JavaScriptCore app: a second context on its own thread, as
3
+ * a page has Web Workers. The host (Native/Sources/SwiftBrowserNative/Host/WorkerHost.swift)
4
+ * runs each one in a `JSVirtualMachine` of its own with a run loop, the
5
+ * runtime's globals (timers, `fetch`, `console`, `TextDecoder`) and the
6
+ * bundle `swiftbrowser-native build --worker <name>=<entry>` wrote next to
7
+ * app.js (`<name>.worker.js`). Messages cross as text: JSON, with typed
8
+ * arrays, `ArrayBuffer`s and `Error`s encoded (`encodeMessage`), since
9
+ * values cannot be shared between virtual machines. Drawn in a page
10
+ * (`swiftbrowser-native site`), the same app code gets the page's own
11
+ * `Worker` over the same bundle, published with the site.
12
+ *
13
+ * ```ts
14
+ * import { createWorker } from '@swiftbrowser/native';
15
+ * const worker = createWorker('render'); // render.worker.js
16
+ * worker.onmessage = (event) => console.log(event.data);
17
+ * worker.postMessage({ draw: 'something' });
18
+ * ```
19
+ */
20
+ import { nativeBridge } from './bridge';
21
+ import { base64Decode, base64Encode } from './encoding';
22
+ // MARK: Messages
23
+ const TYPED_ARRAYS = {
24
+ Uint8Array,
25
+ Uint8ClampedArray,
26
+ Int8Array,
27
+ Uint16Array,
28
+ Int16Array,
29
+ Uint32Array,
30
+ Int32Array,
31
+ Float32Array,
32
+ Float64Array,
33
+ };
34
+ /**
35
+ * A message as text: JSON, with a typed array or `ArrayBuffer` as
36
+ * `{ $sbBytes, $sbType }` (base64) and an `Error` as `{ $sbError }`. What
37
+ * JSON drops (functions, `undefined` in an object, a `Map`) is dropped here
38
+ * too; a `Date` arrives as its ISO string.
39
+ */
40
+ export function encodeMessage(data) {
41
+ return JSON.stringify(data === undefined ? null : data, (_key, value) => {
42
+ if (value instanceof ArrayBuffer)
43
+ return { $sbBytes: base64Encode(new Uint8Array(value)), $sbType: 'ArrayBuffer' };
44
+ if (ArrayBuffer.isView(value)) {
45
+ const name = value.constructor.name;
46
+ const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
47
+ return { $sbBytes: base64Encode(bytes), $sbType: name && name in TYPED_ARRAYS ? name : 'Uint8Array' };
48
+ }
49
+ if (value instanceof Error)
50
+ return { $sbError: { name: value.name, message: value.message, stack: value.stack ?? null } };
51
+ return value;
52
+ });
53
+ }
54
+ /** The value `encodeMessage` encoded. */
55
+ export function decodeMessage(text) {
56
+ return JSON.parse(text, (_key, value) => {
57
+ if (value && typeof value === 'object') {
58
+ const record = value;
59
+ if (typeof record['$sbBytes'] === 'string') {
60
+ const bytes = base64Decode(record['$sbBytes']);
61
+ const type = record['$sbType'];
62
+ if (type === 'ArrayBuffer')
63
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
64
+ const Constructor = TYPED_ARRAYS[type in TYPED_ARRAYS ? type : 'Uint8Array'];
65
+ // A view over the decoded bytes, aligned for the element size.
66
+ const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
67
+ return new Constructor(buffer);
68
+ }
69
+ if (record['$sbError'] && typeof record['$sbError'] === 'object') {
70
+ const spec = record['$sbError'];
71
+ const error = new Error(spec.message ?? '');
72
+ if (spec.name)
73
+ error.name = spec.name;
74
+ if (spec.stack)
75
+ error.stack = spec.stack;
76
+ return error;
77
+ }
78
+ }
79
+ return value;
80
+ });
81
+ }
82
+ /** `Worker` over the bridge, in the app's context. */
83
+ export class NativeWorker {
84
+ static nextId = 1;
85
+ static live = new Map();
86
+ #id;
87
+ #native;
88
+ #terminated = false;
89
+ #messageListeners = new Set();
90
+ #errorListeners = new Set();
91
+ onmessage = null;
92
+ onerror = null;
93
+ /** `name` is the bundle's: `<name>.worker.js` next to app.js. A URL's last path segment serves as the name (`render.worker.js` → `render`). */
94
+ constructor(name, native = nativeBridge()) {
95
+ if (!native?.workerStart || !native.workerPost || !native.workerTerminate) {
96
+ throw new Error('Worker: this host has no workers (SwiftBrowser Native 0.13 or later)');
97
+ }
98
+ this.#native = native;
99
+ this.#id = NativeWorker.nextId++;
100
+ NativeWorker.live.set(this.#id, this);
101
+ const failure = native.workerStart(this.#id, workerName(name));
102
+ if (failure) {
103
+ NativeWorker.live.delete(this.#id);
104
+ throw new Error(failure);
105
+ }
106
+ }
107
+ postMessage(data) {
108
+ if (this.#terminated)
109
+ return;
110
+ this.#native.workerPost(this.#id, encodeMessage(data));
111
+ }
112
+ terminate() {
113
+ if (this.#terminated)
114
+ return;
115
+ this.#terminated = true;
116
+ NativeWorker.live.delete(this.#id);
117
+ this.#native.workerTerminate(this.#id);
118
+ }
119
+ addEventListener(type, listener) {
120
+ if (type === 'message')
121
+ this.#messageListeners.add(listener);
122
+ else
123
+ this.#errorListeners.add(listener);
124
+ }
125
+ removeEventListener(type, listener) {
126
+ if (type === 'message')
127
+ this.#messageListeners.delete(listener);
128
+ else
129
+ this.#errorListeners.delete(listener);
130
+ }
131
+ /** The bridge's `__sbWorkerMessage(id, text)`. */
132
+ static receive(id, text) {
133
+ const worker = NativeWorker.live.get(id);
134
+ if (!worker)
135
+ return;
136
+ const event = { type: 'message', data: decodeMessage(text) };
137
+ worker.onmessage?.(event);
138
+ for (const listener of worker.#messageListeners)
139
+ listener(event);
140
+ }
141
+ /** The bridge's `__sbWorkerError(id, message)`: an uncaught exception in the worker. */
142
+ static fail(id, message) {
143
+ const worker = NativeWorker.live.get(id);
144
+ if (!worker)
145
+ return;
146
+ const event = { type: 'error', message };
147
+ worker.onerror?.(event);
148
+ for (const listener of worker.#errorListeners)
149
+ listener(event);
150
+ }
151
+ }
152
+ /** The bundle name a worker was asked for: `render`, or the `render` of `.../render.worker.js`. */
153
+ export function workerName(name) {
154
+ const text = typeof name === 'string' ? name : name.href;
155
+ const last = text.split('/').pop() ?? text;
156
+ return last.replace(/\.(m?[jt]sx?)$/, '').replace(/\.worker$/, '');
157
+ }
158
+ /**
159
+ * A worker from the bundle `<name>.worker.js`: the page's own `Worker` when
160
+ * the app is drawn in a page (the bundle published with the site), else the
161
+ * host's over the bridge.
162
+ */
163
+ export function createWorker(name) {
164
+ const base = typeof __SB_WORKER_BASE__ === 'string' ? __SB_WORKER_BASE__ : undefined;
165
+ const PageWorker = globalThis.Worker;
166
+ if (typeof PageWorker === 'function' && base !== undefined)
167
+ return new PageWorker(`${base}${workerName(name)}.worker.js`);
168
+ if (nativeBridge()?.workerStart)
169
+ return new NativeWorker(name);
170
+ if (typeof PageWorker === 'function')
171
+ return new PageWorker(`${workerName(name)}.worker.js`);
172
+ throw new Error(`createWorker: no host for "${name}" (not in a SwiftBrowser Native app or a page)`);
173
+ }
174
+ // MARK: The worker's side
175
+ /**
176
+ * A worker's own globals over its bridge: `self`, `postMessage`, `onmessage`
177
+ * and `onerror` as properties the script assigns, `addEventListener`,
178
+ * `close`, and `__sbWorkerReceive`, which the host calls with each message.
179
+ */
180
+ export function installWorkerScope(native, target) {
181
+ const installed = [];
182
+ const define = (name, value) => {
183
+ Object.defineProperty(target, name, { value, writable: true, configurable: true, enumerable: false });
184
+ installed.push(name);
185
+ };
186
+ const messageListeners = new Set();
187
+ const errorListeners = new Set();
188
+ define('self', target);
189
+ define('postMessage', (data) => native.workerPostMessage(encodeMessage(data)));
190
+ define('addEventListener', (type, listener) => {
191
+ if (type === 'message')
192
+ messageListeners.add(listener);
193
+ else if (type === 'error')
194
+ errorListeners.add(listener);
195
+ });
196
+ define('removeEventListener', (type, listener) => {
197
+ if (type === 'message')
198
+ messageListeners.delete(listener);
199
+ else if (type === 'error')
200
+ errorListeners.delete(listener);
201
+ });
202
+ define('close', () => native.log('warn', 'close(): a worker ends when the app terminates it'));
203
+ define('__sbWorkerReceive', (text) => {
204
+ const event = { type: 'message', data: decodeMessage(text) };
205
+ const handler = target['onmessage'];
206
+ if (typeof handler === 'function')
207
+ handler(event);
208
+ for (const listener of messageListeners)
209
+ listener(event);
210
+ });
211
+ return installed;
212
+ }
213
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swiftbrowser/native",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "Run a @swiftbrowser/react app natively: the runtime a JavaScriptCore-hosted app needs (timers, console, fetch and the other web globals over the SwiftBrowser Native bridge), the `run` entry that renders into the native SwiftUI renderer, and the bundler that builds app.js for the Xcode target.",
6
6
  "repository": {
@@ -59,8 +59,8 @@
59
59
  },
60
60
  "dependencies": {
61
61
  "@babel/core": "^7.29.7",
62
- "@swiftbrowser/react": "0.11.0",
63
- "@swiftbrowser/web": "0.11.0",
62
+ "@swiftbrowser/react": "0.13.0",
63
+ "@swiftbrowser/web": "0.13.0",
64
64
  "esbuild": "^0.28.2",
65
65
  "react-refresh": "^0.17.0"
66
66
  },
package/src/bridge.ts CHANGED
@@ -26,6 +26,18 @@ export interface NativeBridge {
26
26
  locale(): string;
27
27
  /** The process's launch arguments (`-screen list` from an Xcode scheme or `simctl launch`). */
28
28
  launchArguments?(): string[];
29
+
30
+ // Workers (worker.ts): a second context on its own thread, from a bundle
31
+ // `swiftbrowser-native build --worker <name>=<entry>` wrote next to app.js
32
+ // (`<name>.worker.js`). The app's context has the first three; a worker's
33
+ // context has the last and no `applyOps`.
34
+ /** Starts worker `id` from `<name>.worker.js`; returns an error message, or nothing. Its messages arrive at `__sbWorkerMessage(id, text)`, its errors at `__sbWorkerError(id, message)`. */
35
+ workerStart?(id: number, name: string): string | null | undefined;
36
+ /** Delivers an encoded message (worker.ts) to worker `id`'s `__sbWorkerReceive(text)`. */
37
+ workerPost?(id: number, text: string): void;
38
+ workerTerminate?(id: number): void;
39
+ /** In a worker's context: an encoded message to the app's context. */
40
+ workerPostMessage?(text: string): void;
29
41
  }
30
42
 
31
43
  /** The app's launch arguments, or none outside the native app. */
package/src/build.ts CHANGED
@@ -23,14 +23,31 @@ export interface BuildOptions {
23
23
  sourcemap?: boolean;
24
24
  /** `process.env.NODE_ENV`; `production` when minifying, else `development`. */
25
25
  mode?: 'development' | 'production';
26
+ /**
27
+ * Worker bundles by name (`{ render: 'src/render.worker.ts' }`): each a
28
+ * script of its own, the runtime first, written as `<name>.worker.js` next
29
+ * to the app bundle, which `createWorker(name)` runs on a thread of its own
30
+ * (worker.ts).
31
+ */
32
+ workers?: Record<string, string>;
33
+ /**
34
+ * Where the worker bundles are published when the app is drawn in a page
35
+ * (`/native/bundle/App/`), as `createWorker` finds them there; absent in an
36
+ * Xcode bundle, whose host loads them by name.
37
+ */
38
+ workerBase?: string;
26
39
  }
27
40
 
28
41
  export interface BuildResult {
29
42
  code: string;
30
43
  bytes: number;
31
44
  warnings: string[];
45
+ /** The worker bundles, by name. */
46
+ workers: Record<string, { code: string; bytes: number }>;
32
47
  }
33
48
 
49
+ const WORKER_NAME = /^[A-Za-z0-9_-]+$/;
50
+
34
51
  const here = dirname(fileURLToPath(import.meta.url));
35
52
  /** The runtime's source; the package ships `src` so this resolves from `dist` too. */
36
53
  const runtimeEntry = fileURLToPath(new URL('../src/runtime.ts', import.meta.url));
@@ -68,34 +85,49 @@ function common(options: BuildOptions): esbuild.BuildOptions {
68
85
  target: ['safari16'],
69
86
  minify: options.minify ?? false,
70
87
  sourcemap: options.sourcemap ? 'inline' : false,
71
- define: { 'process.env.NODE_ENV': JSON.stringify(mode), 'import.meta.env.DEV': JSON.stringify(mode === 'development') },
88
+ define: {
89
+ 'process.env.NODE_ENV': JSON.stringify(mode),
90
+ 'import.meta.env.DEV': JSON.stringify(mode === 'development'),
91
+ __SB_WORKER_BASE__: options.workerBase === undefined ? 'undefined' : JSON.stringify(options.workerBase),
92
+ },
72
93
  jsx: 'automatic',
73
94
  logLevel: 'silent',
74
95
  absWorkingDir: here,
75
96
  };
76
97
  }
77
98
 
78
- /** Builds the two halves and joins them. */
99
+ /** Builds the two halves and joins them, and each worker the same way. */
79
100
  export async function bundle(options: BuildOptions): Promise<BuildResult> {
80
101
  const runtime = await esbuild.build({ ...common(options), entryPoints: [runtimeEntry] });
81
102
  const app = await esbuild.build({ ...common(options), entryPoints: [options.entry], alias: aliases(options.entry) });
82
103
  const code = `${text(runtime)}\n${text(app)}`;
83
- return {
84
- code,
85
- bytes: Buffer.byteLength(code),
86
- warnings: [...runtime.warnings, ...app.warnings].map((warning) => warning.text),
87
- };
104
+ const warnings = [...runtime.warnings, ...app.warnings].map((warning) => warning.text);
105
+ const workers: BuildResult['workers'] = {};
106
+ for (const [name, entry] of Object.entries(options.workers ?? {})) {
107
+ if (!WORKER_NAME.test(name)) throw new Error(`worker names are letters, digits, "_" and "-": ${name}`);
108
+ const worker = await esbuild.build({ ...common(options), entryPoints: [resolve(entry)], alias: aliases(entry) });
109
+ const workerCode = `${text(runtime)}\n${text(worker)}`;
110
+ workers[name] = { code: workerCode, bytes: Buffer.byteLength(workerCode) };
111
+ warnings.push(...worker.warnings.map((warning) => warning.text));
112
+ }
113
+ return { code, bytes: Buffer.byteLength(code), warnings, workers };
114
+ }
115
+
116
+ /** Where a worker bundle goes: `<name>.worker.js` beside the app bundle. */
117
+ export function workerOutfile(outfile: string, name: string): string {
118
+ return resolve(dirname(outfile), `${name}.worker.js`);
88
119
  }
89
120
 
90
121
  function text(result: esbuild.BuildResult): string {
91
122
  return result.outputFiles?.map((file) => file.text).join('\n') ?? '';
92
123
  }
93
124
 
94
- /** Builds and writes the bundle. */
125
+ /** Builds and writes the bundle, and the worker bundles beside it. */
95
126
  export async function buildApp(options: BuildOptions): Promise<BuildResult> {
96
127
  const result = await bundle(options);
97
128
  await mkdir(dirname(options.outfile), { recursive: true });
98
129
  await writeFile(options.outfile, result.code);
130
+ for (const [name, worker] of Object.entries(result.workers)) await writeFile(workerOutfile(options.outfile, name), worker.code);
99
131
  return result;
100
132
  }
101
133
 
package/src/index.ts CHANGED
@@ -18,3 +18,4 @@ export { nativeBridge, launchArguments, type NativeBridge, type NativeFetchReque
18
18
  export { SimpleHeaders, SimpleRequest, SimpleResponse, SimpleAbortController, SimpleAbortSignal, createFetch } from './fetch';
19
19
  export { SimpleURL, SimpleURLSearchParams } from './url';
20
20
  export { SimpleTextEncoder, SimpleTextDecoder, base64Encode, base64Decode, utf8Encode, utf8Decode } from './encoding';
21
+ export { createWorker, NativeWorker, encodeMessage, decodeMessage, workerName, type WorkerLike, type WorkerMessageEvent, type WorkerErrorEvent } from './worker';
package/src/runtime.ts CHANGED
@@ -11,15 +11,20 @@
11
11
  * `atob`, `btoa`, `fetch` with `Headers`, `Request`, `Response`,
12
12
  * `AbortController` and `AbortSignal`, `URL`, `URLSearchParams`, `crypto`
13
13
  * (`getRandomValues`, `randomUUID`), `localStorage`, `structuredClone`,
14
- * `navigator`, and the bridge's callbacks `__sbTimerFired` and
15
- * `__sbFetchResult`. `window` and `document` are not defined: code that
16
- * checks for them takes its headless path, as it should here.
14
+ * `navigator`, `Worker` (worker.ts) with the bridge's `__sbWorkerMessage`
15
+ * and `__sbWorkerError`, and the bridge's callbacks `__sbTimerFired` and
16
+ * `__sbFetchResult`. In a worker's own context (its bridge has
17
+ * `workerPostMessage`), `self`, `postMessage`, `addEventListener` and
18
+ * `__sbWorkerReceive` instead of `Worker`. `window` and `document` are not
19
+ * defined: code that checks for them takes its headless path, as it should
20
+ * here.
17
21
  */
18
22
 
19
23
  import { nativeBridge, type NativeBridge } from './bridge';
20
24
  import { atobPolyfill, base64Decode, btoaPolyfill, SimpleTextDecoder, SimpleTextEncoder } from './encoding';
21
25
  import { createFetch } from './fetch';
22
26
  import { SimpleURL, SimpleURLSearchParams } from './url';
27
+ import { installWorkerScope, NativeWorker } from './worker';
23
28
 
24
29
  type AnyGlobal = Record<string, unknown>;
25
30
 
@@ -199,6 +204,14 @@ export function installRuntime(native: NativeBridge, target: AnyGlobal = globalT
199
204
  hardwareConcurrency: 1,
200
205
  });
201
206
 
207
+ if (native.workerPostMessage) {
208
+ installed.push(...installWorkerScope(native, target));
209
+ } else if (native.workerStart) {
210
+ define('Worker', NativeWorker);
211
+ define('__sbWorkerMessage', NativeWorker.receive);
212
+ define('__sbWorkerError', NativeWorker.fail);
213
+ }
214
+
202
215
  return installed;
203
216
  }
204
217
 
package/src/site.ts CHANGED
@@ -36,6 +36,12 @@ export interface SiteOptions extends Omit<BuildOptions, 'outfile'> {
36
36
  module?: string;
37
37
  /** The path the site is served under when it is not a host's root (`/native/`); `/` by default. */
38
38
  base?: string;
39
+ /**
40
+ * Asset catalogs (`*.xcassets` directories) the app's named images and
41
+ * colors come from, served as the site's catalog; the Xcode target's, or
42
+ * one kept for the site (a `.symbolset` holding any SVG draws as a symbol).
43
+ */
44
+ catalogs?: string[];
39
45
  /** Vite's log level while building the site; `warn` by default. */
40
46
  logLevel?: 'info' | 'warn' | 'error' | 'silent';
41
47
  }
@@ -46,6 +52,8 @@ export interface SiteResult {
46
52
  module: string;
47
53
  moduleBytes: number;
48
54
  bundleBytes: number;
55
+ /** The worker bundles' sizes, by name. */
56
+ workers: Record<string, number>;
49
57
  warnings: string[];
50
58
  }
51
59
 
@@ -74,28 +82,46 @@ export async function buildNativeSite(options: SiteOptions): Promise<SiteResult>
74
82
  );
75
83
  }
76
84
  if (options.base !== undefined && !/^\/([^/].*\/)?$/.test(options.base)) throw new Error(`site: --base must start and end with "/": ${options.base}`);
77
- const { outDir, name: _name, displayName, module: _module, base, logLevel, ...bundleOptions } = options;
85
+ const { outDir, name: _name, displayName, module: _module, base, logLevel, catalogs, ...bundleOptions } = options;
86
+ for (const catalog of catalogs ?? []) {
87
+ if (!existsSync(catalog)) throw new Error(`site: no asset catalog at ${catalog}`);
88
+ }
78
89
  // The bundle is the app's one resource: published at `/bundle/<name>/app.js`
79
90
  // and mounted at `/bundle/app.js`, where the module's launch reads it.
80
91
  const resources = mkdtempSync(path.join(tmpdir(), 'swiftbrowser-native-site-'));
81
92
  try {
82
- const built = await bundle({ ...bundleOptions, outfile: path.join(resources, 'app.js') });
93
+ // The worker bundles are resources beside it, where the page's `Worker` loads them from.
94
+ const built = await bundle({ ...bundleOptions, outfile: path.join(resources, 'app.js'), workerBase: `${base ?? '/'}bundle/${name}/` });
83
95
  writeFileSync(path.join(resources, 'app.js'), built.code);
96
+ const files = [path.join(resources, 'app.js')];
97
+ for (const [workerName, worker] of Object.entries(built.workers)) {
98
+ const file = path.join(resources, `${workerName}.worker.js`);
99
+ writeFileSync(file, worker.code);
100
+ files.push(file);
101
+ }
84
102
  await buildSite({
85
103
  apps: [{
86
104
  name,
87
105
  displayName: displayName ?? name,
88
106
  description: 'A SwiftBrowser Native app in the browser',
89
107
  wasm: module,
90
- catalogs: [],
91
- resources: [path.join(resources, 'app.js')],
108
+ catalogs: (catalogs ?? []).map((catalog) => path.resolve(catalog)),
109
+ resources: files,
92
110
  }],
93
111
  layout: 'app',
94
112
  outDir: path.resolve(outDir),
95
113
  ...(base !== undefined ? { base } : {}),
96
114
  logLevel: logLevel ?? 'warn',
97
115
  });
98
- return { name, outDir: path.resolve(outDir), module, moduleBytes: statSync(module).size, bundleBytes: built.bytes, warnings: built.warnings };
116
+ return {
117
+ name,
118
+ outDir: path.resolve(outDir),
119
+ module,
120
+ moduleBytes: statSync(module).size,
121
+ bundleBytes: built.bytes,
122
+ workers: Object.fromEntries(Object.entries(built.workers).map(([workerName, worker]) => [workerName, worker.bytes])),
123
+ warnings: built.warnings,
124
+ };
99
125
  } finally {
100
126
  rmSync(resources, { recursive: true, force: true });
101
127
  }