@swiftbrowser/native 0.12.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.
- package/README.md +36 -1
- package/bin/swiftbrowser-native.mjs +23 -7
- package/dist/bridge.d.ts +7 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js.map +1 -1
- package/dist/build.d.ts +22 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +25 -8
- package/dist/build.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +7 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +16 -3
- package/dist/runtime.js.map +1 -1
- package/dist/site.d.ts +2 -0
- package/dist/site.d.ts.map +1 -1
- package/dist/site.js +18 -3
- package/dist/site.js.map +1 -1
- package/dist/worker.d.ts +87 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +213 -0
- package/dist/worker.js.map +1 -0
- package/package.json +3 -3
- package/src/bridge.ts +12 -0
- package/src/build.ts +40 -8
- package/src/index.ts +1 -0
- package/src/runtime.ts +16 -3
- package/src/site.ts +20 -3
- package/src/worker.ts +236 -0
- package/swift/Package.swift +1 -0
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +46 -2
- package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +2 -1
- package/swift/Sources/SwiftBrowserNative/Host/WorkerHost.swift +279 -0
- package/wasm/SwiftBrowserNative.wasm +2 -2
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.
|
|
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.
|
|
63
|
-
"@swiftbrowser/web": "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: {
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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`,
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
@@ -52,6 +52,8 @@ export interface SiteResult {
|
|
|
52
52
|
module: string;
|
|
53
53
|
moduleBytes: number;
|
|
54
54
|
bundleBytes: number;
|
|
55
|
+
/** The worker bundles' sizes, by name. */
|
|
56
|
+
workers: Record<string, number>;
|
|
55
57
|
warnings: string[];
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -88,8 +90,15 @@ export async function buildNativeSite(options: SiteOptions): Promise<SiteResult>
|
|
|
88
90
|
// and mounted at `/bundle/app.js`, where the module's launch reads it.
|
|
89
91
|
const resources = mkdtempSync(path.join(tmpdir(), 'swiftbrowser-native-site-'));
|
|
90
92
|
try {
|
|
91
|
-
|
|
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}/` });
|
|
92
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
|
+
}
|
|
93
102
|
await buildSite({
|
|
94
103
|
apps: [{
|
|
95
104
|
name,
|
|
@@ -97,14 +106,22 @@ export async function buildNativeSite(options: SiteOptions): Promise<SiteResult>
|
|
|
97
106
|
description: 'A SwiftBrowser Native app in the browser',
|
|
98
107
|
wasm: module,
|
|
99
108
|
catalogs: (catalogs ?? []).map((catalog) => path.resolve(catalog)),
|
|
100
|
-
resources:
|
|
109
|
+
resources: files,
|
|
101
110
|
}],
|
|
102
111
|
layout: 'app',
|
|
103
112
|
outDir: path.resolve(outDir),
|
|
104
113
|
...(base !== undefined ? { base } : {}),
|
|
105
114
|
logLevel: logLevel ?? 'warn',
|
|
106
115
|
});
|
|
107
|
-
return {
|
|
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
|
+
};
|
|
108
125
|
} finally {
|
|
109
126
|
rmSync(resources, { recursive: true, force: true });
|
|
110
127
|
}
|