@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/build.ts ADDED
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Builds `app.js` for an Xcode target: the runtime first (./runtime, so the
3
+ * web globals exist before React's scheduler reads them), then the app, as
4
+ * one script JavaScriptCore evaluates with `NativeApp.load`. `react-dom` is
5
+ * aliased to stubs (no DOM here); `@swiftbrowser/react` and React are
6
+ * bundled in. Node only (the CLI in ../bin and build scripts use it).
7
+ */
8
+
9
+ import * as esbuild from 'esbuild';
10
+ import { createHash } from 'node:crypto';
11
+ import { existsSync } from 'node:fs';
12
+ import { createServer } from 'node:http';
13
+ import { mkdir, writeFile } from 'node:fs/promises';
14
+ import { dirname } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ export interface BuildOptions {
18
+ /** The app's entry module, which calls `run(<App />)`. */
19
+ entry: string;
20
+ /** Where to write the bundle (`<App>/app.js` in the Xcode target's folder). */
21
+ outfile: string;
22
+ minify?: boolean;
23
+ sourcemap?: boolean;
24
+ /** `process.env.NODE_ENV`; `production` when minifying, else `development`. */
25
+ mode?: 'development' | 'production';
26
+ }
27
+
28
+ export interface BuildResult {
29
+ code: string;
30
+ bytes: number;
31
+ warnings: string[];
32
+ }
33
+
34
+ const here = dirname(fileURLToPath(import.meta.url));
35
+ /** The runtime's source; the package ships `src` so this resolves from `dist` too. */
36
+ const runtimeEntry = fileURLToPath(new URL('../src/runtime.ts', import.meta.url));
37
+ const reactSources = fileURLToPath(new URL('../../react/src/index.ts', import.meta.url));
38
+ const stubs: Record<string, string> = {
39
+ 'react-dom/client': fileURLToPath(new URL('../src/stubs/react-dom-client.ts', import.meta.url)),
40
+ 'react-dom': fileURLToPath(new URL('../src/stubs/react-dom.ts', import.meta.url)),
41
+ // This package from its sources, so a bundle never needs its `dist` built first.
42
+ '@swiftbrowser/native/runtime': runtimeEntry,
43
+ '@swiftbrowser/native': fileURLToPath(new URL('../src/index.ts', import.meta.url)),
44
+ // In this repository, @swiftbrowser/react from its sources too (installed, it is a built package).
45
+ ...(existsSync(reactSources) ? { '@swiftbrowser/react': reactSources } : {}),
46
+ };
47
+
48
+ function common(options: BuildOptions): esbuild.BuildOptions {
49
+ const mode = options.mode ?? (options.minify ? 'production' : 'development');
50
+ return {
51
+ bundle: true,
52
+ write: false,
53
+ format: 'iife',
54
+ platform: 'browser',
55
+ // JavaScriptCore on iOS 17 has every ES2022 feature.
56
+ target: ['safari16'],
57
+ minify: options.minify ?? false,
58
+ sourcemap: options.sourcemap ? 'inline' : false,
59
+ define: { 'process.env.NODE_ENV': JSON.stringify(mode), 'import.meta.env.DEV': JSON.stringify(mode === 'development') },
60
+ jsx: 'automatic',
61
+ logLevel: 'silent',
62
+ absWorkingDir: here,
63
+ };
64
+ }
65
+
66
+ /** Builds the two halves and joins them. */
67
+ export async function bundle(options: BuildOptions): Promise<BuildResult> {
68
+ const runtime = await esbuild.build({ ...common(options), entryPoints: [runtimeEntry] });
69
+ const app = await esbuild.build({ ...common(options), entryPoints: [options.entry], alias: stubs });
70
+ const code = `${text(runtime)}\n${text(app)}`;
71
+ return {
72
+ code,
73
+ bytes: Buffer.byteLength(code),
74
+ warnings: [...runtime.warnings, ...app.warnings].map((warning) => warning.text),
75
+ };
76
+ }
77
+
78
+ function text(result: esbuild.BuildResult): string {
79
+ return result.outputFiles?.map((file) => file.text).join('\n') ?? '';
80
+ }
81
+
82
+ /** Builds and writes the bundle. */
83
+ export async function buildApp(options: BuildOptions): Promise<BuildResult> {
84
+ const result = await bundle(options);
85
+ await mkdir(dirname(options.outfile), { recursive: true });
86
+ await writeFile(options.outfile, result.code);
87
+ return result;
88
+ }
89
+
90
+ export interface ServeOptions extends BuildOptions {
91
+ port?: number;
92
+ /** Called after every build (a log line). */
93
+ onBuild?: (result: BuildResult | Error) => void;
94
+ }
95
+
96
+ /**
97
+ * The dev loop: serves the dev bundle (devBuild.ts) for `NativeApp.watch(url:)`:
98
+ *
99
+ * | path | what |
100
+ * |---|---|
101
+ * | `/manifest.json` | `{ "vendor": <hash>, "app": <hash> }`, polled; `Cache-Control: no-store` |
102
+ * | `/vendor.js` | the vendor half, evaluated once (again when its hash changes) |
103
+ * | `/app.js` | the app half, evaluated again on every change: Fast Refresh |
104
+ *
105
+ * Rebuilds when the sources change (esbuild's watcher over the app's
106
+ * inputs), and writes the plain single bundle to `outfile` too, so an app
107
+ * built from Xcode without the server gets the latest. Returns a function
108
+ * that stops everything.
109
+ */
110
+ export async function serve(options: ServeOptions): Promise<() => Promise<void>> {
111
+ const { buildDev } = await import('./devBuild.js');
112
+ let vendor = { code: '', hash: '' };
113
+ let app = { code: '', hash: '' };
114
+ const rebuild = async (): Promise<void> => {
115
+ try {
116
+ const dev = await buildDev({ entry: options.entry, sourcemap: options.sourcemap ?? false });
117
+ vendor = { code: dev.vendor, hash: hash(dev.vendor) };
118
+ app = { code: dev.app, hash: hash(dev.app) };
119
+ const full = await buildApp(options);
120
+ options.onBuild?.({ ...full, warnings: [...full.warnings, ...dev.warnings] });
121
+ } catch (error) {
122
+ options.onBuild?.(error instanceof Error ? error : new Error(String(error)));
123
+ }
124
+ };
125
+ await rebuild();
126
+
127
+ // esbuild's watcher over the app build's inputs.
128
+ const context = await esbuild.context({
129
+ ...common(options),
130
+ entryPoints: [options.entry],
131
+ alias: stubs,
132
+ plugins: [
133
+ {
134
+ name: 'swiftbrowser-native-rebuild',
135
+ setup(build) {
136
+ let first = true;
137
+ build.onEnd(() => {
138
+ if (first) {
139
+ first = false;
140
+ return;
141
+ }
142
+ void rebuild();
143
+ });
144
+ },
145
+ },
146
+ ],
147
+ });
148
+ await context.watch();
149
+
150
+ const port = options.port ?? 5173;
151
+ const server = createServer((request, response) => {
152
+ const url = request.url ?? '/';
153
+ const send = (body: string, type: string, etag: string): void => {
154
+ response.setHeader('Content-Type', type);
155
+ response.setHeader('Cache-Control', 'no-store');
156
+ response.setHeader('ETag', `"${etag}"`);
157
+ if (request.headers['if-none-match'] === `"${etag}"`) {
158
+ response.statusCode = 304;
159
+ response.end();
160
+ return;
161
+ }
162
+ response.end(request.method === 'HEAD' ? undefined : body);
163
+ };
164
+ if (url === '/manifest.json' || url === '/') {
165
+ send(JSON.stringify({ vendor: vendor.hash, app: app.hash }), 'application/json', `${vendor.hash}-${app.hash}`);
166
+ } else if (url === '/vendor.js') {
167
+ send(vendor.code, 'text/javascript; charset=utf-8', vendor.hash);
168
+ } else if (url === '/app.js') {
169
+ send(app.code, 'text/javascript; charset=utf-8', app.hash);
170
+ } else {
171
+ response.statusCode = 404;
172
+ response.end('not found');
173
+ }
174
+ });
175
+ await new Promise<void>((resolve) => server.listen(port, resolve));
176
+
177
+ return async () => {
178
+ await context.dispose();
179
+ await new Promise<void>((resolve) => server.close(() => resolve()));
180
+ };
181
+ }
182
+
183
+ function hash(text: string): string {
184
+ return createHash('sha1').update(text).digest('hex').slice(0, 12);
185
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * The dev bundle in two halves, for Fast Refresh (docs/native.md):
3
+ *
4
+ * - `vendor.js`: vendor.ts (the runtime, the refresh runtime, React,
5
+ * `@swiftbrowser/react`, this package), evaluated once per vendor change.
6
+ * - `app.js`: the app's own modules, with `react`, `react/jsx-runtime`,
7
+ * `@swiftbrowser/react` and `@swiftbrowser/native` resolved to shims that
8
+ * read `__sbVendor`, and every module run through `react-refresh/babel`
9
+ * so its components register with the refresh runtime. Evaluated again
10
+ * on every save; `run` then refreshes the mounted components in place.
11
+ *
12
+ * Node only (serve in build.ts uses it).
13
+ */
14
+
15
+ import { transformAsync } from '@babel/core';
16
+ import * as esbuild from 'esbuild';
17
+ import { createRequire } from 'node:module';
18
+ import { dirname, extname } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const require = createRequire(import.meta.url);
22
+ const here = dirname(fileURLToPath(import.meta.url));
23
+ const vendorEntry = fileURLToPath(new URL('../src/vendor.ts', import.meta.url));
24
+ const ownIndex = fileURLToPath(new URL('../src/index.ts', import.meta.url));
25
+ const reactSources = fileURLToPath(new URL('../../react/src/index.ts', import.meta.url));
26
+
27
+ /** The modules the app half takes from the vendor half. */
28
+ const VENDORED = ['react', 'react/jsx-runtime', 'react/jsx-dev-runtime', '@swiftbrowser/react', '@swiftbrowser/native'];
29
+
30
+ export interface DevBuildOptions {
31
+ entry: string;
32
+ sourcemap?: boolean;
33
+ }
34
+
35
+ export interface DevBundle {
36
+ vendor: string;
37
+ app: string;
38
+ warnings: string[];
39
+ }
40
+
41
+ function common(options: DevBuildOptions): esbuild.BuildOptions {
42
+ return {
43
+ bundle: true,
44
+ write: false,
45
+ format: 'iife',
46
+ platform: 'browser',
47
+ target: ['safari16'],
48
+ sourcemap: options.sourcemap ? 'inline' : false,
49
+ define: { 'process.env.NODE_ENV': '"development"', 'import.meta.env.DEV': 'true' },
50
+ jsx: 'automatic',
51
+ logLevel: 'silent',
52
+ absWorkingDir: here,
53
+ };
54
+ }
55
+
56
+ /** The export names of a module, for a shim with static exports. */
57
+ async function exportNames(name: string): Promise<string[]> {
58
+ if (name === '@swiftbrowser/native') return names(ownIndex);
59
+ if (name === '@swiftbrowser/react') return names(reactSources);
60
+ // CommonJS (React): its exports are enumerable once required.
61
+ return Object.keys(require(name) as object).filter((key) => key !== 'default' && key !== '__esModule');
62
+ }
63
+
64
+ /** The export names of an ES module (its sources), from esbuild's metafile. */
65
+ async function names(entry: string): Promise<string[]> {
66
+ const result = await esbuild.build({
67
+ entryPoints: [entry],
68
+ bundle: true,
69
+ write: false,
70
+ format: 'esm',
71
+ metafile: true,
72
+ platform: 'browser',
73
+ logLevel: 'silent',
74
+ absWorkingDir: here,
75
+ external: ['react', 'react-dom', 'react-dom/client', 'react-reconciler', 'react-reconciler/constants', 'react-refresh/runtime', '@babel/core', 'esbuild', 'node:*'],
76
+ alias: { '@swiftbrowser/react': reactSources },
77
+ });
78
+ return Object.values(result.metafile!.outputs).flatMap((output) => output.exports).filter((key) => key !== 'default');
79
+ }
80
+
81
+ /** The vendor half: vendor.ts with its dependencies. */
82
+ export async function buildVendor(options: DevBuildOptions): Promise<{ code: string; warnings: string[] }> {
83
+ const result = await esbuild.build({
84
+ ...common(options),
85
+ entryPoints: [vendorEntry],
86
+ alias: {
87
+ 'react-dom/client': fileURLToPath(new URL('../src/stubs/react-dom-client.ts', import.meta.url)),
88
+ 'react-dom': fileURLToPath(new URL('../src/stubs/react-dom.ts', import.meta.url)),
89
+ '@swiftbrowser/react': reactSources,
90
+ },
91
+ });
92
+ return { code: result.outputFiles!.map((file) => file.text).join('\n'), warnings: result.warnings.map((w) => w.text) };
93
+ }
94
+
95
+ /** The app half: the entry's own modules over the vendor shims, with the refresh transform. */
96
+ export async function buildApp(options: DevBuildOptions): Promise<{ code: string; warnings: string[] }> {
97
+ const shims = new Map<string, string>();
98
+ for (const name of VENDORED) {
99
+ const exports = await exportNames(name);
100
+ shims.set(
101
+ name,
102
+ [
103
+ `const m = globalThis.__sbVendor[${JSON.stringify(name)}];`,
104
+ `if (!m) throw new Error(${JSON.stringify(`vendor.js was not loaded before app.js (no ${name})`)});`,
105
+ 'export default m.default ?? m;',
106
+ ...exports.map((key) => `export const ${key} = m[${JSON.stringify(key)}];`),
107
+ ].join('\n'),
108
+ );
109
+ }
110
+ const refreshPlugin = require.resolve('react-refresh/babel');
111
+ const result = await esbuild.build({
112
+ ...common(options),
113
+ entryPoints: [options.entry],
114
+ plugins: [
115
+ {
116
+ name: 'swiftbrowser-native-dev',
117
+ setup(build) {
118
+ const filter = new RegExp(`^(${VENDORED.map((name) => name.replace(/[/]/g, '\\/')).join('|')})$`);
119
+ build.onResolve({ filter }, (args) => ({ path: args.path, namespace: 'sb-vendor' }));
120
+ build.onLoad({ filter: /.*/, namespace: 'sb-vendor' }, (args) => ({ contents: shims.get(args.path)!, loader: 'js' }));
121
+ // The app's own modules: TypeScript and JSX compiled by esbuild, then
122
+ // the refresh transform, with the registration functions in scope.
123
+ build.onLoad({ filter: /\.[jt]sx?$/ }, async (args) => {
124
+ if (args.path.includes('/node_modules/')) return null;
125
+ const { readFile } = await import('node:fs/promises');
126
+ const source = await readFile(args.path, 'utf8');
127
+ const loader = extname(args.path).slice(1) as esbuild.Loader;
128
+ const compiled = await esbuild.transform(source, { loader, jsx: 'automatic', format: 'esm', sourcefile: args.path, target: 'es2022' });
129
+ const refreshed = await transformAsync(compiled.code, {
130
+ filename: args.path,
131
+ configFile: false,
132
+ babelrc: false,
133
+ sourceType: 'module',
134
+ plugins: [[refreshPlugin, { skipEnvCheck: true }]],
135
+ });
136
+ const prelude = [
137
+ `const $RefreshReg$ = (type, id) => globalThis.__sbRefresh.register(type, ${JSON.stringify(args.path)} + ' ' + id);`,
138
+ 'const $RefreshSig$ = globalThis.__sbRefresh.createSignatureFunctionForTransform;',
139
+ ].join('\n');
140
+ return { contents: `${prelude}\n${refreshed?.code ?? compiled.code}`, loader: 'js' };
141
+ });
142
+ },
143
+ },
144
+ ],
145
+ });
146
+ return { code: result.outputFiles!.map((file) => file.text).join('\n'), warnings: result.warnings.map((w) => w.text) };
147
+ }
148
+
149
+ export async function buildDev(options: DevBuildOptions): Promise<DevBundle> {
150
+ const [vendor, app] = await Promise.all([buildVendor(options), buildApp(options)]);
151
+ return { vendor: vendor.code, app: app.code, warnings: [...vendor.warnings, ...app.warnings] };
152
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * UTF-8 and base64, for the globals bare JavaScriptCore lacks (`TextEncoder`,
3
+ * `TextDecoder`, `atob`, `btoa`) and for the bytes that cross the bridge as
4
+ * base64 (fetch bodies). No dependencies, so the runtime can carry them.
5
+ */
6
+
7
+ export function utf8Encode(text: string): Uint8Array {
8
+ const out: number[] = [];
9
+ for (let i = 0; i < text.length; i++) {
10
+ let code = text.charCodeAt(i);
11
+ if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length) {
12
+ const low = text.charCodeAt(i + 1);
13
+ if (low >= 0xdc00 && low <= 0xdfff) {
14
+ code = 0x10000 + ((code - 0xd800) << 10) + (low - 0xdc00);
15
+ i++;
16
+ }
17
+ }
18
+ if (code < 0x80) out.push(code);
19
+ else if (code < 0x800) out.push(0xc0 | (code >> 6), 0x80 | (code & 63));
20
+ else if (code < 0x10000) out.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 63), 0x80 | (code & 63));
21
+ else out.push(0xf0 | (code >> 18), 0x80 | ((code >> 12) & 63), 0x80 | ((code >> 6) & 63), 0x80 | (code & 63));
22
+ }
23
+ return Uint8Array.from(out);
24
+ }
25
+
26
+ export function utf8Decode(bytes: Uint8Array): string {
27
+ let out = '';
28
+ for (let i = 0; i < bytes.length; ) {
29
+ const a = bytes[i]!;
30
+ let code: number;
31
+ if (a < 0x80) {
32
+ code = a;
33
+ i += 1;
34
+ } else if ((a & 0xe0) === 0xc0) {
35
+ code = ((a & 31) << 6) | (bytes[i + 1]! & 63);
36
+ i += 2;
37
+ } else if ((a & 0xf0) === 0xe0) {
38
+ code = ((a & 15) << 12) | ((bytes[i + 1]! & 63) << 6) | (bytes[i + 2]! & 63);
39
+ i += 3;
40
+ } else {
41
+ code = ((a & 7) << 18) | ((bytes[i + 1]! & 63) << 12) | ((bytes[i + 2]! & 63) << 6) | (bytes[i + 3]! & 63);
42
+ i += 4;
43
+ }
44
+ if (Number.isNaN(code)) break;
45
+ out += String.fromCodePoint(code);
46
+ }
47
+ return out;
48
+ }
49
+
50
+ const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
51
+ const B64_INDEX: Record<string, number> = {};
52
+ for (let i = 0; i < B64.length; i++) B64_INDEX[B64[i]!] = i;
53
+
54
+ /** Standard base64 (RFC 4648, padded). */
55
+ export function base64Encode(bytes: Uint8Array): string {
56
+ let out = '';
57
+ for (let i = 0; i < bytes.length; i += 3) {
58
+ const a = bytes[i]!;
59
+ const b = i + 1 < bytes.length ? bytes[i + 1]! : 0;
60
+ const c = i + 2 < bytes.length ? bytes[i + 2]! : 0;
61
+ const n = (a << 16) | (b << 8) | c;
62
+ out += B64[(n >> 18) & 63]! + B64[(n >> 12) & 63]!;
63
+ out += i + 1 < bytes.length ? B64[(n >> 6) & 63]! : '=';
64
+ out += i + 2 < bytes.length ? B64[n & 63]! : '=';
65
+ }
66
+ return out;
67
+ }
68
+
69
+ export function base64Decode(text: string): Uint8Array {
70
+ const clean = text.replace(/[^A-Za-z0-9+/]/g, '');
71
+ const out: number[] = [];
72
+ for (let i = 0; i < clean.length; i += 4) {
73
+ const a = B64_INDEX[clean[i]!] ?? 0;
74
+ const b = B64_INDEX[clean[i + 1]!] ?? 0;
75
+ const c = i + 2 < clean.length ? B64_INDEX[clean[i + 2]!]! : -1;
76
+ const d = i + 3 < clean.length ? B64_INDEX[clean[i + 3]!]! : -1;
77
+ out.push((a << 2) | (b >> 4));
78
+ if (c >= 0) out.push(((b & 15) << 4) | (c >> 2));
79
+ if (d >= 0) out.push(((c & 3) << 6) | d);
80
+ }
81
+ return Uint8Array.from(out);
82
+ }
83
+
84
+ /** `TextEncoder` (UTF-8 only), as the runtime installs it. */
85
+ export class SimpleTextEncoder {
86
+ readonly encoding = 'utf-8';
87
+ encode(input = ''): Uint8Array {
88
+ return utf8Encode(String(input));
89
+ }
90
+ encodeInto(input: string, destination: Uint8Array): { read: number; written: number } {
91
+ const bytes = utf8Encode(input);
92
+ const written = Math.min(bytes.length, destination.length);
93
+ destination.set(bytes.subarray(0, written));
94
+ return { read: input.length, written };
95
+ }
96
+ }
97
+
98
+ /** `TextDecoder` (UTF-8 only), as the runtime installs it. */
99
+ export class SimpleTextDecoder {
100
+ readonly encoding: string;
101
+ readonly fatal = false;
102
+ readonly ignoreBOM = false;
103
+ constructor(label = 'utf-8') {
104
+ this.encoding = label.toLowerCase();
105
+ }
106
+ decode(input?: ArrayBuffer | ArrayBufferView | null): string {
107
+ if (!input) return '';
108
+ const bytes = input instanceof ArrayBuffer ? new Uint8Array(input) : new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
109
+ return utf8Decode(bytes);
110
+ }
111
+ }
112
+
113
+ /** `btoa`: a binary string (char codes 0...255) to base64. */
114
+ export function btoaPolyfill(binary: string): string {
115
+ const bytes = new Uint8Array(binary.length);
116
+ for (let i = 0; i < binary.length; i++) {
117
+ const code = binary.charCodeAt(i);
118
+ if (code > 255) throw new Error('btoa: the string contains a character outside the Latin1 range');
119
+ bytes[i] = code;
120
+ }
121
+ return base64Encode(bytes);
122
+ }
123
+
124
+ /** `atob`: base64 to a binary string. */
125
+ export function atobPolyfill(text: string): string {
126
+ const bytes = base64Decode(text);
127
+ let out = '';
128
+ for (const byte of bytes) out += String.fromCharCode(byte);
129
+ return out;
130
+ }