@swiftbrowser/native 0.0.0-stage → 0.10.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 (107) hide show
  1. package/README.md +143 -2
  2. package/bin/swiftbrowser-native.mjs +49 -0
  3. package/dist/bridge.d.ts +50 -0
  4. package/dist/bridge.d.ts.map +1 -0
  5. package/dist/bridge.js +10 -0
  6. package/dist/bridge.js.map +1 -0
  7. package/dist/build.d.ts +47 -0
  8. package/dist/build.d.ts.map +1 -0
  9. package/dist/build.js +156 -0
  10. package/dist/build.js.map +1 -0
  11. package/dist/devBuild.d.ts +34 -0
  12. package/dist/devBuild.d.ts.map +1 -0
  13. package/dist/devBuild.js +133 -0
  14. package/dist/devBuild.js.map +1 -0
  15. package/dist/encoding.d.ts +32 -0
  16. package/dist/encoding.d.ts.map +1 -0
  17. package/dist/encoding.js +136 -0
  18. package/dist/encoding.js.map +1 -0
  19. package/dist/fetch.d.ts +117 -0
  20. package/dist/fetch.d.ts.map +1 -0
  21. package/dist/fetch.js +272 -0
  22. package/dist/fetch.js.map +1 -0
  23. package/dist/index.d.ts +20 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +20 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/native.d.ts +19 -0
  28. package/dist/native.d.ts.map +1 -0
  29. package/dist/native.js +17 -0
  30. package/dist/native.js.map +1 -0
  31. package/dist/refresh.d.ts +18 -0
  32. package/dist/refresh.d.ts.map +1 -0
  33. package/dist/refresh.js +18 -0
  34. package/dist/refresh.js.map +1 -0
  35. package/dist/run.d.ts +27 -0
  36. package/dist/run.d.ts.map +1 -0
  37. package/dist/run.js +76 -0
  38. package/dist/run.js.map +1 -0
  39. package/dist/runtime.d.ts +30 -0
  40. package/dist/runtime.d.ts.map +1 -0
  41. package/dist/runtime.js +193 -0
  42. package/dist/runtime.js.map +1 -0
  43. package/dist/stubs/react-dom-client.d.ts +8 -0
  44. package/dist/stubs/react-dom-client.d.ts.map +1 -0
  45. package/dist/stubs/react-dom-client.js +11 -0
  46. package/dist/stubs/react-dom-client.js.map +1 -0
  47. package/dist/stubs/react-dom.d.ts +9 -0
  48. package/dist/stubs/react-dom.d.ts.map +1 -0
  49. package/dist/stubs/react-dom.js +13 -0
  50. package/dist/stubs/react-dom.js.map +1 -0
  51. package/dist/url.d.ts +48 -0
  52. package/dist/url.d.ts.map +1 -0
  53. package/dist/url.js +213 -0
  54. package/dist/url.js.map +1 -0
  55. package/dist/vendor.d.ts +11 -0
  56. package/dist/vendor.d.ts.map +1 -0
  57. package/dist/vendor.js +27 -0
  58. package/dist/vendor.js.map +1 -0
  59. package/dist/wasmHost.d.ts +35 -0
  60. package/dist/wasmHost.d.ts.map +1 -0
  61. package/dist/wasmHost.js +120 -0
  62. package/dist/wasmHost.js.map +1 -0
  63. package/package.json +73 -4
  64. package/src/bridge.ts +58 -0
  65. package/src/build.ts +185 -0
  66. package/src/devBuild.ts +152 -0
  67. package/src/encoding.ts +130 -0
  68. package/src/fetch.ts +305 -0
  69. package/src/index.ts +20 -0
  70. package/src/native.ts +35 -0
  71. package/src/refresh.ts +29 -0
  72. package/src/run.ts +98 -0
  73. package/src/runtime.ts +208 -0
  74. package/src/stubs/react-dom-client.ts +17 -0
  75. package/src/stubs/react-dom.ts +15 -0
  76. package/src/types/react-refresh.d.ts +38 -0
  77. package/src/url.ts +202 -0
  78. package/src/vendor.ts +30 -0
  79. package/src/wasmHost.ts +147 -0
  80. package/swift/Package.swift +31 -0
  81. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +228 -0
  82. package/swift/Sources/SwiftBrowserNative/Host/JSHost.swift +42 -0
  83. package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +102 -0
  84. package/swift/Sources/SwiftBrowserNative/Host/ShimAliases.swift +25 -0
  85. package/swift/Sources/SwiftBrowserNative/Host/WebHost.swift +138 -0
  86. package/swift/Sources/SwiftBrowserNative/Runtime/NativeApp.swift +251 -0
  87. package/swift/Sources/SwiftBrowserNative/Runtime/NativeRegistry.swift +53 -0
  88. package/swift/Sources/SwiftBrowserNative/SwiftBrowserNative.swift +6 -0
  89. package/swift/Sources/SwiftBrowserNative/Views/Conversions.swift +361 -0
  90. package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +169 -0
  91. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +268 -0
  92. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Lists.swift +175 -0
  93. package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +196 -0
  94. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +246 -0
  95. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Shapes.swift +108 -0
  96. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +189 -0
  97. package/swift/Sources/SwiftBrowserNative/Views/Kinds/TextAndImages.swift +102 -0
  98. package/swift/Sources/SwiftBrowserNative/Views/NativeRootView.swift +190 -0
  99. package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +182 -0
  100. package/swift/Sources/SwiftBrowserProtocol/JSONValue.swift +268 -0
  101. package/swift/Sources/SwiftBrowserProtocol/Op.swift +100 -0
  102. package/swift/Sources/SwiftBrowserProtocol/Props.swift +469 -0
  103. package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +117 -0
  104. package/swift/Sources/SwiftBrowserProtocol/Style.swift +148 -0
  105. package/swift/Sources/SwiftBrowserProtocol/Values.swift +279 -0
  106. package/swift/Sources/SwiftBrowserTree/ElementNode.swift +65 -0
  107. package/swift/Sources/SwiftBrowserTree/OpTree.swift +168 -0
package/src/runtime.ts ADDED
@@ -0,0 +1,208 @@
1
+ /**
2
+ * The web globals a JavaScriptCore app expects, built on the bridge's
3
+ * primitives (`__sbNative`, see bridge.ts). React's scheduler reads
4
+ * `setTimeout` and `performance.now` when its module loads, so this module
5
+ * installs everything at import time and the bundler (build.ts) puts it
6
+ * first in `app.js`.
7
+ *
8
+ * What is installed when missing: `setTimeout`, `setInterval`, `clearTimeout`,
9
+ * `clearInterval`, `requestAnimationFrame`, `cancelAnimationFrame`,
10
+ * `queueMicrotask`, `console`, `performance`, `TextEncoder`, `TextDecoder`,
11
+ * `atob`, `btoa`, `fetch` with `Headers`, `Request`, `Response`,
12
+ * `AbortController` and `AbortSignal`, `URL`, `URLSearchParams`, `crypto`
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.
17
+ */
18
+
19
+ import { nativeBridge, type NativeBridge } from './bridge';
20
+ import { atobPolyfill, base64Decode, btoaPolyfill, SimpleTextDecoder, SimpleTextEncoder } from './encoding';
21
+ import { createFetch } from './fetch';
22
+ import { SimpleURL, SimpleURLSearchParams } from './url';
23
+
24
+ type AnyGlobal = Record<string, unknown>;
25
+
26
+ interface TimerEntry {
27
+ callback: (...args: unknown[]) => void;
28
+ args: unknown[];
29
+ repeats: boolean;
30
+ }
31
+
32
+ export interface InstallOptions {
33
+ /** Install over globals that exist already (tests). */
34
+ force?: boolean;
35
+ }
36
+
37
+ /**
38
+ * Installs the runtime on `target` (the global object) over `native`.
39
+ * Returns what it installed, by name.
40
+ */
41
+ export function installRuntime(native: NativeBridge, target: AnyGlobal = globalThis as unknown as AnyGlobal, options: InstallOptions = {}): string[] {
42
+ const installed: string[] = [];
43
+ const define = (name: string, value: unknown): void => {
44
+ if (!options.force && target[name] !== undefined) return;
45
+ Object.defineProperty(target, name, { value, writable: true, configurable: true, enumerable: false });
46
+ installed.push(name);
47
+ };
48
+
49
+ // Timers: one native timer per JavaScript timer, by id.
50
+ const timers = new Map<number, TimerEntry>();
51
+ let nextTimer = 1;
52
+ const schedule = (callback: unknown, delay: unknown, args: unknown[], repeats: boolean): number => {
53
+ const id = nextTimer++;
54
+ const fn = typeof callback === 'function' ? (callback as TimerEntry['callback']) : () => {};
55
+ timers.set(id, { callback: fn, args, repeats });
56
+ native.setTimer(id, Math.max(0, Number(delay) || 0), repeats);
57
+ return id;
58
+ };
59
+ const cancel = (id: unknown): void => {
60
+ const key = Number(id);
61
+ if (!timers.delete(key)) return;
62
+ native.clearTimer(key);
63
+ };
64
+ const fired = (id: number): void => {
65
+ const entry = timers.get(id);
66
+ if (!entry) return;
67
+ if (!entry.repeats) timers.delete(id);
68
+ entry.callback(...entry.args);
69
+ };
70
+ define('setTimeout', (callback: unknown, delay?: unknown, ...args: unknown[]) => schedule(callback, delay, args, false));
71
+ define('setInterval', (callback: unknown, delay?: unknown, ...args: unknown[]) => schedule(callback, delay, args, true));
72
+ define('clearTimeout', cancel);
73
+ define('clearInterval', cancel);
74
+ define('__sbTimerFired', fired);
75
+
76
+ // Animation frames: a 60 Hz timer; the callback gets the time.
77
+ const now = (): number => native.now();
78
+ define('requestAnimationFrame', (callback: (time: number) => void) => schedule(() => callback(now()), 16, [], false));
79
+ define('cancelAnimationFrame', cancel);
80
+
81
+ define('queueMicrotask', (callback: () => void) => {
82
+ Promise.resolve().then(callback);
83
+ });
84
+
85
+ // The console: one line per call, formatted here.
86
+ const format = (value: unknown): string => {
87
+ if (typeof value === 'string') return value;
88
+ if (value instanceof Error) return value.stack ? String(value.stack) : `${value.name}: ${value.message}`;
89
+ if (typeof value === 'function') return `[Function ${value.name || 'anonymous'}]`;
90
+ if (typeof value === 'symbol' || typeof value === 'bigint') return value.toString();
91
+ try {
92
+ const json = JSON.stringify(value, (_key, inner: unknown) => (inner instanceof Error ? `${inner.name}: ${inner.message}` : inner), 2);
93
+ return json === undefined ? String(value) : json;
94
+ } catch {
95
+ return String(value);
96
+ }
97
+ };
98
+ const log = (level: string) => (...args: unknown[]) => native.log(level, args.map(format).join(' '));
99
+ const timersByLabel = new Map<string, number>();
100
+ const counts = new Map<string, number>();
101
+ define('console', {
102
+ log: log('log'),
103
+ info: log('info'),
104
+ warn: log('warn'),
105
+ error: log('error'),
106
+ debug: log('debug'),
107
+ trace: log('debug'),
108
+ dir: log('log'),
109
+ table: log('log'),
110
+ group: log('log'),
111
+ groupCollapsed: log('log'),
112
+ groupEnd: () => {},
113
+ assert: (condition: unknown, ...args: unknown[]) => {
114
+ if (!condition) native.log('error', `Assertion failed${args.length ? `: ${args.map(format).join(' ')}` : ''}`);
115
+ },
116
+ count: (label = 'default') => {
117
+ const count = (counts.get(label) ?? 0) + 1;
118
+ counts.set(label, count);
119
+ native.log('log', `${label}: ${count}`);
120
+ },
121
+ countReset: (label = 'default') => counts.delete(label),
122
+ time: (label = 'default') => timersByLabel.set(label, now()),
123
+ timeEnd: (label = 'default') => {
124
+ const start = timersByLabel.get(label);
125
+ if (start !== undefined) native.log('log', `${label}: ${(now() - start).toFixed(3)} ms`);
126
+ timersByLabel.delete(label);
127
+ },
128
+ timeLog: (label = 'default') => {
129
+ const start = timersByLabel.get(label);
130
+ if (start !== undefined) native.log('log', `${label}: ${(now() - start).toFixed(3)} ms`);
131
+ },
132
+ });
133
+
134
+ const origin = Date.now() - now();
135
+ define('performance', {
136
+ now,
137
+ timeOrigin: origin,
138
+ mark: () => {},
139
+ measure: () => {},
140
+ getEntries: () => [],
141
+ getEntriesByName: () => [],
142
+ getEntriesByType: () => [],
143
+ clearMarks: () => {},
144
+ clearMeasures: () => {},
145
+ });
146
+
147
+ define('TextEncoder', SimpleTextEncoder);
148
+ define('TextDecoder', SimpleTextDecoder);
149
+ define('atob', atobPolyfill);
150
+ define('btoa', btoaPolyfill);
151
+
152
+ const fetchGlobals = createFetch(native);
153
+ define('fetch', fetchGlobals.fetch);
154
+ define('Headers', fetchGlobals.Headers);
155
+ define('Request', fetchGlobals.Request);
156
+ define('Response', fetchGlobals.Response);
157
+ define('AbortController', fetchGlobals.AbortController);
158
+ define('AbortSignal', fetchGlobals.AbortSignal);
159
+ define('__sbFetchResult', fetchGlobals.__sbFetchResult);
160
+
161
+ define('URL', SimpleURL);
162
+ define('URLSearchParams', SimpleURLSearchParams);
163
+
164
+ define('crypto', {
165
+ getRandomValues: <T extends ArrayBufferView>(array: T): T => {
166
+ const bytes = base64Decode(native.randomBytes(array.byteLength));
167
+ new Uint8Array(array.buffer, array.byteOffset, array.byteLength).set(bytes.subarray(0, array.byteLength));
168
+ return array;
169
+ },
170
+ randomUUID: (): string => {
171
+ const bytes = base64Decode(native.randomBytes(16));
172
+ bytes[6] = (bytes[6]! & 0x0f) | 0x40;
173
+ bytes[8] = (bytes[8]! & 0x3f) | 0x80;
174
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
175
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
176
+ },
177
+ });
178
+
179
+ const storage = {
180
+ getItem: (key: string): string | null => native.storageGet(String(key)),
181
+ setItem: (key: string, value: string): void => native.storageSet(String(key), String(value)),
182
+ removeItem: (key: string): void => native.storageRemove(String(key)),
183
+ clear: (): void => native.storageClear(),
184
+ key: (index: number): string | null => native.storageKeys()[index] ?? null,
185
+ get length(): number {
186
+ return native.storageKeys().length;
187
+ },
188
+ };
189
+ define('localStorage', storage);
190
+
191
+ define('structuredClone', <T>(value: T): T => (value === undefined ? value : (JSON.parse(JSON.stringify(value)) as T)));
192
+
193
+ define('navigator', {
194
+ userAgent: 'SwiftBrowserNative',
195
+ language: native.locale().replace('_', '-'),
196
+ languages: [native.locale().replace('_', '-')],
197
+ onLine: true,
198
+ platform: 'iOS',
199
+ hardwareConcurrency: 1,
200
+ });
201
+
202
+ return installed;
203
+ }
204
+
205
+ // Installed at import, over the bridge the app runs in. Outside it (a test,
206
+ // a bundler's dry run) nothing happens until `installRuntime` is called.
207
+ const bridge = nativeBridge();
208
+ if (bridge) installRuntime(bridge);
@@ -0,0 +1,17 @@
1
+ /** `react-dom/client` for the native bundle: no DOM, so no roots (see react-dom.ts). */
2
+
3
+ export interface Root {
4
+ render(children: unknown): void;
5
+ unmount(): void;
6
+ }
7
+
8
+ export function createRoot(): Root {
9
+ return {
10
+ render() {},
11
+ unmount() {},
12
+ };
13
+ }
14
+
15
+ export function hydrateRoot(): Root {
16
+ return createRoot();
17
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `react-dom` as the native bundle sees it. @swiftbrowser/react's `Host`
3
+ * component (React DOM inside a SwiftUI tree) imports it; there is no DOM
4
+ * here, so a `Host` renders nothing and `flushSync` just runs its function.
5
+ */
6
+
7
+ export function flushSync<R>(fn?: () => R): R | undefined {
8
+ return fn?.();
9
+ }
10
+
11
+ export function createPortal(): null {
12
+ return null;
13
+ }
14
+
15
+ export const version = '0.0.0-swiftbrowser-native';
@@ -0,0 +1,38 @@
1
+ // The parts of react-refresh's runtime and Babel plugin this package uses
2
+ // (the package ships no types; @types/react-refresh would be one more dependency).
3
+ declare module 'react-refresh/runtime' {
4
+ export interface Family {
5
+ current: unknown;
6
+ }
7
+ export type SignatureFunction = (type: unknown, key: string, forceReset?: boolean, getCustomHooks?: () => unknown[]) => unknown;
8
+ const RefreshRuntime: {
9
+ injectIntoGlobalHook(globalObject: Window): void;
10
+ register(type: unknown, id: string): void;
11
+ createSignatureFunctionForTransform(): SignatureFunction;
12
+ performReactRefresh(): unknown;
13
+ getFamilyByType(type: unknown): Family | undefined;
14
+ isLikelyComponentType(type: unknown): boolean;
15
+ hasUnrecoverableErrors(): boolean;
16
+ };
17
+ export default RefreshRuntime;
18
+ }
19
+
20
+ declare module 'react-refresh/babel' {
21
+ const plugin: unknown;
22
+ export default plugin;
23
+ }
24
+
25
+ declare module '@babel/core' {
26
+ export interface TransformOptions {
27
+ filename?: string;
28
+ configFile?: boolean;
29
+ babelrc?: boolean;
30
+ sourceType?: 'module' | 'script';
31
+ plugins?: unknown[];
32
+ sourceMaps?: boolean | 'inline';
33
+ }
34
+ export interface BabelFileResult {
35
+ code?: string | null;
36
+ }
37
+ export function transformAsync(code: string, options?: TransformOptions): Promise<BabelFileResult | null>;
38
+ }
package/src/url.ts ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * `URL` and `URLSearchParams` for an engine without them. Enough for what
3
+ * apps and libraries do with them (build and read absolute URLs, query
4
+ * strings); not the whole WHATWG parser: no IDNA, no percent-encoding
5
+ * normalization beyond `encodeURIComponent` in the search params.
6
+ */
7
+
8
+ export class SimpleURLSearchParams {
9
+ private entries: [string, string][] = [];
10
+ /** Called when the params change, so an owning URL updates its `search`. */
11
+ onChange: (() => void) | null = null;
12
+
13
+ constructor(init: string | Record<string, string> | [string, string][] | SimpleURLSearchParams = '') {
14
+ if (typeof init === 'string') {
15
+ const text = init.startsWith('?') ? init.slice(1) : init;
16
+ for (const part of text.split('&')) {
17
+ if (!part) continue;
18
+ const at = part.indexOf('=');
19
+ const key = at < 0 ? part : part.slice(0, at);
20
+ const value = at < 0 ? '' : part.slice(at + 1);
21
+ this.entries.push([decodeComponent(key), decodeComponent(value)]);
22
+ }
23
+ } else if (init instanceof SimpleURLSearchParams) {
24
+ this.entries = init.entries.map(([k, v]) => [k, v]);
25
+ } else if (Array.isArray(init)) {
26
+ this.entries = init.map(([k, v]) => [String(k), String(v)]);
27
+ } else {
28
+ this.entries = Object.entries(init).map(([k, v]) => [k, String(v)]);
29
+ }
30
+ }
31
+
32
+ get size(): number {
33
+ return this.entries.length;
34
+ }
35
+ append(name: string, value: string): void {
36
+ this.entries.push([name, String(value)]);
37
+ this.onChange?.();
38
+ }
39
+ delete(name: string): void {
40
+ this.entries = this.entries.filter(([k]) => k !== name);
41
+ this.onChange?.();
42
+ }
43
+ get(name: string): string | null {
44
+ return this.entries.find(([k]) => k === name)?.[1] ?? null;
45
+ }
46
+ getAll(name: string): string[] {
47
+ return this.entries.filter(([k]) => k === name).map(([, v]) => v);
48
+ }
49
+ has(name: string): boolean {
50
+ return this.entries.some(([k]) => k === name);
51
+ }
52
+ set(name: string, value: string): void {
53
+ const index = this.entries.findIndex(([k]) => k === name);
54
+ if (index < 0) this.entries.push([name, String(value)]);
55
+ else {
56
+ this.entries[index] = [name, String(value)];
57
+ this.entries = this.entries.filter(([k], i) => k !== name || i === index);
58
+ }
59
+ this.onChange?.();
60
+ }
61
+ sort(): void {
62
+ this.entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
63
+ this.onChange?.();
64
+ }
65
+ forEach(callback: (value: string, key: string, parent: SimpleURLSearchParams) => void): void {
66
+ for (const [k, v] of this.entries) callback(v, k, this);
67
+ }
68
+ keys(): IterableIterator<string> {
69
+ return this.entries.map(([k]) => k)[Symbol.iterator]();
70
+ }
71
+ values(): IterableIterator<string> {
72
+ return this.entries.map(([, v]) => v)[Symbol.iterator]();
73
+ }
74
+ [Symbol.iterator](): IterableIterator<[string, string]> {
75
+ return this.entries.map(([k, v]) => [k, v] as [string, string])[Symbol.iterator]();
76
+ }
77
+ toString(): string {
78
+ return this.entries.map(([k, v]) => `${encodeComponent(k)}=${encodeComponent(v)}`).join('&');
79
+ }
80
+ }
81
+
82
+ function encodeComponent(value: string): string {
83
+ return encodeURIComponent(value).replace(/%20/g, '+');
84
+ }
85
+ function decodeComponent(value: string): string {
86
+ try {
87
+ return decodeURIComponent(value.replace(/\+/g, ' '));
88
+ } catch {
89
+ return value;
90
+ }
91
+ }
92
+
93
+ const URL_PATTERN = /^([a-zA-Z][a-zA-Z0-9+.-]*):(?:\/\/(?:([^@/?#]*)@)?([^:/?#]*)(?::(\d+))?)?([^?#]*)(\?[^#]*)?(#.*)?$/;
94
+ const DEFAULT_PORTS: Record<string, string> = { 'http:': '80', 'https:': '443', 'ws:': '80', 'wss:': '443', 'ftp:': '21' };
95
+
96
+ export class SimpleURL {
97
+ protocol = '';
98
+ username = '';
99
+ password = '';
100
+ hostname = '';
101
+ port = '';
102
+ pathname = '';
103
+ hash = '';
104
+ readonly searchParams: SimpleURLSearchParams;
105
+ private _search = '';
106
+
107
+ constructor(url: string, base?: string | SimpleURL) {
108
+ let text = String(url);
109
+ if (!URL_PATTERN.test(text) || /^[a-zA-Z][a-zA-Z0-9+.-]*:(?!\/\/)/.test(text) === false && !/^[a-zA-Z]/.test(text)) {
110
+ if (base === undefined) throw new TypeError(`Invalid URL: ${text}`);
111
+ text = resolve(text, base instanceof SimpleURL ? base.href : String(base));
112
+ } else if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(text)) {
113
+ if (base === undefined) throw new TypeError(`Invalid URL: ${text}`);
114
+ text = resolve(text, base instanceof SimpleURL ? base.href : String(base));
115
+ }
116
+ const match = URL_PATTERN.exec(text);
117
+ if (!match) throw new TypeError(`Invalid URL: ${text}`);
118
+ const [, scheme, auth, host, port, path, search, hash] = match;
119
+ this.protocol = `${scheme!.toLowerCase()}:`;
120
+ if (auth !== undefined) {
121
+ const at = auth.indexOf(':');
122
+ this.username = at < 0 ? auth : auth.slice(0, at);
123
+ this.password = at < 0 ? '' : auth.slice(at + 1);
124
+ }
125
+ this.hostname = (host ?? '').toLowerCase();
126
+ this.port = port !== undefined && DEFAULT_PORTS[this.protocol] !== port ? port : '';
127
+ this.pathname = path || (host !== undefined ? '/' : '');
128
+ this._search = search && search !== '?' ? search : '';
129
+ this.hash = hash && hash !== '#' ? hash : '';
130
+ this.searchParams = new SimpleURLSearchParams(this._search);
131
+ this.searchParams.onChange = () => {
132
+ const text = this.searchParams.toString();
133
+ this._search = text ? `?${text}` : '';
134
+ };
135
+ }
136
+
137
+ get search(): string {
138
+ return this._search;
139
+ }
140
+ set search(value: string) {
141
+ const text = value.startsWith('?') ? value : value ? `?${value}` : '';
142
+ this._search = text === '?' ? '' : text;
143
+ const next = new SimpleURLSearchParams(this._search);
144
+ // Replace the entries in place so the same object keeps working.
145
+ (this.searchParams as unknown as { entries: [string, string][] }).entries = [...next];
146
+ }
147
+ get host(): string {
148
+ return this.port ? `${this.hostname}:${this.port}` : this.hostname;
149
+ }
150
+ set host(value: string) {
151
+ const at = value.indexOf(':');
152
+ this.hostname = at < 0 ? value : value.slice(0, at);
153
+ this.port = at < 0 ? '' : value.slice(at + 1);
154
+ }
155
+ get origin(): string {
156
+ return this.hostname ? `${this.protocol}//${this.host}` : 'null';
157
+ }
158
+ get href(): string {
159
+ const auth = this.username ? `${this.username}${this.password ? `:${this.password}` : ''}@` : '';
160
+ const authority = this.hostname || this.protocol === 'file:' ? `//${auth}${this.host}` : '';
161
+ return `${this.protocol}${authority}${this.pathname}${this._search}${this.hash}`;
162
+ }
163
+ set href(value: string) {
164
+ const next = new SimpleURL(value);
165
+ Object.assign(this, { protocol: next.protocol, username: next.username, password: next.password, hostname: next.hostname, port: next.port, pathname: next.pathname, hash: next.hash });
166
+ this.search = next.search;
167
+ }
168
+ toString(): string {
169
+ return this.href;
170
+ }
171
+ toJSON(): string {
172
+ return this.href;
173
+ }
174
+
175
+ static canParse(url: string, base?: string): boolean {
176
+ try {
177
+ new SimpleURL(url, base);
178
+ return true;
179
+ } catch {
180
+ return false;
181
+ }
182
+ }
183
+ }
184
+
185
+ /** Resolves a relative reference against a base (RFC 3986 section 5.2, the common cases). */
186
+ function resolve(reference: string, base: string): string {
187
+ const b = new SimpleURL(base);
188
+ if (reference.startsWith('//')) return `${b.protocol}${reference}`;
189
+ if (reference.startsWith('/')) return `${b.origin}${reference}`;
190
+ if (reference.startsWith('?')) return `${b.origin}${b.pathname}${reference}`;
191
+ if (reference.startsWith('#')) return `${b.origin}${b.pathname}${b.search}${reference}`;
192
+ if (reference === '') return `${b.origin}${b.pathname}${b.search}`;
193
+ const directory = b.pathname.slice(0, b.pathname.lastIndexOf('/') + 1);
194
+ const segments: string[] = [];
195
+ for (const segment of `${directory}${reference}`.split('/')) {
196
+ if (segment === '..') segments.pop();
197
+ else if (segment !== '.') segments.push(segment);
198
+ }
199
+ let path = segments.join('/');
200
+ if (!path.startsWith('/')) path = `/${path}`;
201
+ return `${b.origin}${path}`;
202
+ }
package/src/vendor.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The vendor half of the dev bundle (devBuild.ts): the runtime, React Fast
3
+ * Refresh's runtime (before React), React, `@swiftbrowser/react` and this
4
+ * package, evaluated once. The app half is built against shims that read
5
+ * these from `__sbVendor`, so re-evaluating it on a save keeps one React and
6
+ * refreshes the mounted components in place.
7
+ */
8
+
9
+ import './runtime';
10
+ import './refresh';
11
+ import * as React from 'react';
12
+ import * as JSXDevRuntime from 'react/jsx-dev-runtime';
13
+ import * as JSXRuntime from 'react/jsx-runtime';
14
+ import * as SwiftBrowserReact from '@swiftbrowser/react';
15
+ import { injectIntoDevTools } from '@swiftbrowser/react';
16
+ import * as Native from './index';
17
+
18
+ // The reconciler tells the DevTools hook (which the refresh runtime installed)
19
+ // about itself, which is how Fast Refresh reaches the mounted roots.
20
+ injectIntoDevTools();
21
+
22
+ export const vendor: Record<string, unknown> = {
23
+ react: React,
24
+ 'react/jsx-runtime': JSXRuntime,
25
+ 'react/jsx-dev-runtime': JSXDevRuntime,
26
+ '@swiftbrowser/react': SwiftBrowserReact,
27
+ '@swiftbrowser/native': Native,
28
+ };
29
+
30
+ (globalThis as unknown as { __sbVendor: typeof vendor }).__sbVendor = vendor;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * The host's JavaScript as SwiftBrowser Native's engine (docs/native.md,
3
+ * "The round trip"): for a module built with the round-trip target
4
+ * (`Native/RoundTrip`, the native views compiled against the shim), this
5
+ * answers its one import, `sb_native.request`, and exposes the native
6
+ * functions Swift installs (`applyOps`, `log`, `now`, …) on `__sbNative`,
7
+ * each a call of the module's `sb_native_invoke` export.
8
+ *
9
+ * Node (scripts/run-wasi.mjs) and a page use it the same way: merge
10
+ * `imports` into the module's imports, instantiate, then `attach` the
11
+ * instance. Strings cross linear memory as UTF-8; an answer to Swift is a
12
+ * buffer from `sb_alloc` holding a little-endian 4-byte length and the
13
+ * bytes, which Swift frees, and a result from Swift is the same layout,
14
+ * freed with `sb_native_free` once read.
15
+ */
16
+
17
+ export interface WasmNativeHostOptions {
18
+ /** Runs a script at the global scope (default: an indirect `eval`). */
19
+ evaluate?: (script: string, name: string) => void;
20
+ /** The global object the bundle's globals live on (default `globalThis`). */
21
+ global?: Record<string, unknown>;
22
+ }
23
+
24
+ export interface WasmNativeHost {
25
+ /** Merge into the module's import object before instantiating. */
26
+ imports: {
27
+ sb_native: {
28
+ request: (kind: number, kindLength: number, a: number, aLength: number, b: number, bLength: number) => number;
29
+ };
30
+ };
31
+ /** The instance, once instantiated: its memory and the exports the host calls. */
32
+ attach(instance: WebAssembly.Instance): void;
33
+ /** The `__sbNative` object as it stands (replaced by a reset). */
34
+ readonly native: Record<string, (...args: unknown[]) => unknown>;
35
+ }
36
+
37
+ interface Exports {
38
+ memory: WebAssembly.Memory;
39
+ sb_alloc: (length: number) => number;
40
+ sb_native_invoke: (name: number, nameLength: number, args: number, argsLength: number) => number;
41
+ sb_native_free: (pointer: number) => void;
42
+ }
43
+
44
+ /** The globals a bundle defines, dropped by a reset (`JSHost.reset`). */
45
+ const BUNDLE_GLOBALS = ['__sbDispatch', '__sbTimerFired', '__sbFetchResult', '__sbRefresh', '__sbVendor'];
46
+
47
+ export function wasmNativeHost(options: WasmNativeHostOptions = {}): WasmNativeHost {
48
+ const g = options.global ?? (globalThis as unknown as Record<string, unknown>);
49
+ const evaluate = options.evaluate ?? ((script: string) => (0, eval)(script));
50
+ const encoder = new TextEncoder();
51
+ const decoder = new TextDecoder();
52
+ let exports: Exports | null = null;
53
+ let native: Record<string, (...args: unknown[]) => unknown> = {};
54
+
55
+ const need = (): Exports => {
56
+ if (!exports) throw new Error('wasmNativeHost: attach(instance) before the module runs');
57
+ return exports;
58
+ };
59
+ const read = (pointer: number, length: number): string =>
60
+ decoder.decode(new Uint8Array(need().memory.buffer, pointer, length));
61
+ /** A buffer from `sb_alloc` with the text's length and bytes; Swift frees it. */
62
+ const answer = (value: unknown): number => {
63
+ const bytes = encoder.encode(JSON.stringify(value));
64
+ const pointer = need().sb_alloc(bytes.length + 4);
65
+ // The allocation may have grown the memory: take the view after it.
66
+ const view = new DataView(need().memory.buffer);
67
+ view.setUint32(pointer, bytes.length, true);
68
+ new Uint8Array(need().memory.buffer, pointer + 4, bytes.length).set(bytes);
69
+ return pointer;
70
+ };
71
+ const describe = (error: unknown): string => (error instanceof Error ? (error.stack ?? error.message) : String(error));
72
+
73
+ /** A native function's call: into Swift and back with its JSON result. */
74
+ const invoke = (name: string, args: unknown[]): unknown => {
75
+ const nameBytes = encoder.encode(name);
76
+ const argsBytes = encoder.encode(JSON.stringify(args));
77
+ const namePointer = need().sb_alloc(nameBytes.length || 1);
78
+ new Uint8Array(need().memory.buffer, namePointer, nameBytes.length).set(nameBytes);
79
+ const argsPointer = need().sb_alloc(argsBytes.length || 1);
80
+ new Uint8Array(need().memory.buffer, argsPointer, argsBytes.length).set(argsBytes);
81
+ const result = need().sb_native_invoke(namePointer, nameBytes.length, argsPointer, argsBytes.length);
82
+ if (result === 0) return undefined;
83
+ const length = new DataView(need().memory.buffer).getUint32(result, true);
84
+ const text = read(result + 4, length);
85
+ need().sb_native_free(result);
86
+ const value: unknown = JSON.parse(text);
87
+ if (value && typeof value === 'object' && 'error' in value && Object.keys(value).length === 1) {
88
+ throw new Error(String((value as { error: unknown }).error));
89
+ }
90
+ return value;
91
+ };
92
+
93
+ const reset = (): void => {
94
+ for (const name of BUNDLE_GLOBALS) delete g[name];
95
+ native = {};
96
+ g['__sbNative'] = native;
97
+ };
98
+
99
+ const request = (kind: number, kindLength: number, a: number, aLength: number, b: number, bLength: number): number => {
100
+ const what = read(kind, kindLength);
101
+ const first = read(a, aLength);
102
+ const second = read(b, bLength);
103
+ switch (what) {
104
+ case 'reset':
105
+ reset();
106
+ return 0;
107
+ case 'install':
108
+ native[first] = (...args: unknown[]) => invoke(first, args);
109
+ return 0;
110
+ case 'eval':
111
+ try {
112
+ evaluate(first, second);
113
+ return 0;
114
+ } catch (error) {
115
+ return answer({ error: describe(error) });
116
+ }
117
+ case 'call': {
118
+ const fn = g[first];
119
+ if (typeof fn !== 'function') return answer({ error: `${first} is not defined` });
120
+ try {
121
+ const result: unknown = (fn as (...args: unknown[]) => unknown)(...(JSON.parse(second) as unknown[]));
122
+ return answer({ ok: result === undefined ? null : result });
123
+ } catch (error) {
124
+ return answer({ error: describe(error) });
125
+ }
126
+ }
127
+ case 'has':
128
+ return answer({ ok: first in g });
129
+ default:
130
+ return answer({ error: `unknown request ${what}` });
131
+ }
132
+ };
133
+
134
+ reset();
135
+ return {
136
+ imports: { sb_native: { request } },
137
+ attach(instance) {
138
+ for (const name of ['memory', 'sb_alloc', 'sb_native_invoke', 'sb_native_free']) {
139
+ if (!(name in instance.exports)) throw new Error(`wasmNativeHost: the module does not export ${name}`);
140
+ }
141
+ exports = instance.exports as unknown as Exports;
142
+ },
143
+ get native() {
144
+ return native;
145
+ },
146
+ };
147
+ }