@swiftbrowser/native 0.10.4 → 0.11.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 +24 -0
- package/bin/swiftbrowser-native.mjs +21 -4
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +15 -4
- package/dist/build.js.map +1 -1
- package/dist/site.d.ts +46 -0
- package/dist/site.d.ts.map +1 -0
- package/dist/site.js +70 -0
- package/dist/site.js.map +1 -0
- package/dist/wasmHost.d.ts +6 -32
- package/dist/wasmHost.d.ts.map +1 -1
- package/dist/wasmHost.js +6 -117
- package/dist/wasmHost.js.map +1 -1
- package/package.json +8 -3
- package/src/build.ts +16 -4
- package/src/site.ts +102 -0
- package/src/wasmHost.ts +6 -145
- package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +1 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +70 -13
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +6 -0
- package/wasm/SwiftBrowserNative.wasm +4 -0
package/README.md
CHANGED
|
@@ -116,6 +116,30 @@ JavaScriptCore lacks. `test/fastRefresh.test.ts` runs the loop in Node.
|
|
|
116
116
|
Xcode scheme or `simctl launch`), as the Fidelity example does to open one
|
|
117
117
|
screen.
|
|
118
118
|
|
|
119
|
+
## In the browser
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
npx swiftbrowser-native site src/index.tsx --out dist # the app as a static site
|
|
123
|
+
npx swiftbrowser-native site src/index.tsx --out ../web/dist/native --base /native/
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The same bundle, drawn by the native views in a browser: the package ships
|
|
127
|
+
the SwiftBrowser Native Swift package compiled for WebAssembly against
|
|
128
|
+
SwiftBrowser's SwiftUI shim (`wasm/SwiftBrowserNative.wasm`, the round trip
|
|
129
|
+
of [docs/native.md](../../docs/native.md)), and `site` writes it, the bundle
|
|
130
|
+
and the web renderer's page as one static directory, the layout
|
|
131
|
+
`swiftbrowser build` writes for a Swift app. The page runs the module, the
|
|
132
|
+
module evaluates the bundle at the page's global scope (so `fetch`, timers
|
|
133
|
+
and `localStorage` are the browser's own, same origin and cookies included),
|
|
134
|
+
the native views draw the app's ops with the shim's SwiftUI, and the web
|
|
135
|
+
renderer draws what the shim emits. One app, three renderers: the browser's
|
|
136
|
+
DOM through `@swiftbrowser/react` directly, real SwiftUI in the iOS app, and
|
|
137
|
+
this: the iOS app's own views, in a page. `--base /path/` when the site is
|
|
138
|
+
served under a path of another site, `--module <file>` for a module built
|
|
139
|
+
from a checkout (`swift build --swift-sdk <wasm sdk> -c release --product
|
|
140
|
+
NativeRoundTrip`), `--name <App>` for the site's app name (the folder
|
|
141
|
+
holding `js/` by default).
|
|
142
|
+
|
|
119
143
|
## The escape hatch
|
|
120
144
|
|
|
121
145
|
```tsx
|
|
@@ -3,22 +3,26 @@
|
|
|
3
3
|
// same as `swiftbrowser native build`, from @swiftbrowser/cli, which also
|
|
4
4
|
// writes a new project with `swiftbrowser native init`);
|
|
5
5
|
// `swiftbrowser-native dev <entry> --out <file> [--port 5173]` serves it with
|
|
6
|
-
// rebuild on save for `NativeApp.watch(url:)
|
|
6
|
+
// rebuild on save for `NativeApp.watch(url:)`; `swiftbrowser-native site
|
|
7
|
+
// <entry> --out <dir>` writes the app as a static site the web renderer runs
|
|
8
|
+
// (the native views built for WebAssembly, `--module <file>` for one other
|
|
9
|
+
// than the package's; `--base /path/` when the site is not a host's root).
|
|
7
10
|
|
|
8
|
-
import { resolve } from 'node:path';
|
|
11
|
+
import { basename, resolve } from 'node:path';
|
|
9
12
|
import { buildApp, serve } from '../dist/build.js';
|
|
10
13
|
|
|
11
14
|
const [command, entry, ...rest] = process.argv.slice(2);
|
|
12
15
|
const flags = {};
|
|
13
16
|
for (let i = 0; i < rest.length; i++) {
|
|
14
17
|
const arg = rest[i];
|
|
15
|
-
if (arg === '--out' || arg === '--port') flags[arg.slice(2)] = rest[++i];
|
|
18
|
+
if (arg === '--out' || arg === '--port' || arg === '--module' || arg === '--base' || arg === '--name') flags[arg.slice(2)] = rest[++i];
|
|
16
19
|
else if (arg.startsWith('--')) flags[arg.slice(2)] = true;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
if (!command || !entry || !flags.out || !['build', 'dev'].includes(command)) {
|
|
22
|
+
if (!command || !entry || !flags.out || !['build', 'dev', 'site'].includes(command)) {
|
|
20
23
|
console.error('usage: swiftbrowser-native build <entry.tsx> --out <App/app.js> [--minify] [--sourcemap]');
|
|
21
24
|
console.error(' swiftbrowser-native dev <entry.tsx> --out <App/app.js> [--port 5173] [--sourcemap]');
|
|
25
|
+
console.error(' swiftbrowser-native site <entry.tsx> --out <dir> [--name <App>] [--module <file.wasm>] [--base /path/] [--minify]');
|
|
22
26
|
process.exit(2);
|
|
23
27
|
}
|
|
24
28
|
|
|
@@ -35,6 +39,19 @@ if (command === 'build') {
|
|
|
35
39
|
const result = await buildApp(options);
|
|
36
40
|
for (const warning of result.warnings) console.warn(warning);
|
|
37
41
|
console.log(`${options.outfile}: ${kb(result.bytes)}`);
|
|
42
|
+
} else if (command === 'site') {
|
|
43
|
+
const { buildNativeSite } = await import('../dist/site.js');
|
|
44
|
+
const { outfile, ...bundleOptions } = options;
|
|
45
|
+
const result = await buildNativeSite({
|
|
46
|
+
...bundleOptions,
|
|
47
|
+
outDir: outfile,
|
|
48
|
+
...(flags.name ? { name: flags.name } : {}),
|
|
49
|
+
...(flags.module ? { module: resolve(flags.module) } : {}),
|
|
50
|
+
...(flags.base ? { base: flags.base } : {}),
|
|
51
|
+
});
|
|
52
|
+
for (const warning of result.warnings) console.warn(warning);
|
|
53
|
+
console.log(`${result.name}: app.js ${kb(result.bundleBytes)}, ${basename(result.module)} ${kb(result.moduleBytes)}`);
|
|
54
|
+
console.log(`site written to ${result.outDir}`);
|
|
38
55
|
} else {
|
|
39
56
|
const port = flags.port ? Number(flags.port) : 5173;
|
|
40
57
|
await serve({
|
package/dist/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,aAAa,GAAG,YAAY,CAAC;CACrC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,aAAa,GAAG,YAAY,CAAC;CACrC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA8CD,4CAA4C;AAC5C,wBAAsB,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CASxE;AAMD,oCAAoC;AACpC,wBAAsB,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAK1E;AAED,MAAM,WAAW,YAAa,SAAQ,YAAY;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK,KAAK,IAAI,CAAC;CACjD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAuE/E"}
|
package/dist/build.js
CHANGED
|
@@ -10,21 +10,32 @@ import { createHash } from 'node:crypto';
|
|
|
10
10
|
import { existsSync } from 'node:fs';
|
|
11
11
|
import { createServer } from 'node:http';
|
|
12
12
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
13
|
-
import { dirname } from 'node:path';
|
|
13
|
+
import { dirname, resolve, sep } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
/** The runtime's source; the package ships `src` so this resolves from `dist` too. */
|
|
17
17
|
const runtimeEntry = fileURLToPath(new URL('../src/runtime.ts', import.meta.url));
|
|
18
18
|
const reactSources = fileURLToPath(new URL('../../react/src/index.ts', import.meta.url));
|
|
19
|
+
/** The repository, when this runs from a checkout (installed, the package has no `packages/react` beside it). */
|
|
20
|
+
const repository = existsSync(reactSources) ? resolve(here, '../../..') : null;
|
|
19
21
|
const stubs = {
|
|
20
22
|
'react-dom/client': fileURLToPath(new URL('../src/stubs/react-dom-client.ts', import.meta.url)),
|
|
21
23
|
'react-dom': fileURLToPath(new URL('../src/stubs/react-dom.ts', import.meta.url)),
|
|
22
24
|
// This package from its sources, so a bundle never needs its `dist` built first.
|
|
23
25
|
'@swiftbrowser/native/runtime': runtimeEntry,
|
|
24
26
|
'@swiftbrowser/native': fileURLToPath(new URL('../src/index.ts', import.meta.url)),
|
|
25
|
-
// In this repository, @swiftbrowser/react from its sources too (installed, it is a built package).
|
|
26
|
-
...(existsSync(reactSources) ? { '@swiftbrowser/react': reactSources } : {}),
|
|
27
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* The aliases for an entry: in this repository, an app of the repository's
|
|
30
|
+
* (the examples) gets @swiftbrowser/react from its sources too (installed,
|
|
31
|
+
* it is a built package). An app outside it (a project built with the
|
|
32
|
+
* checkout's bundler) keeps its own installed copy: the sources would bring
|
|
33
|
+
* the repository's React along, a second copy beside the app's.
|
|
34
|
+
*/
|
|
35
|
+
function aliases(entry) {
|
|
36
|
+
const inRepository = repository !== null && resolve(entry).startsWith(repository + sep);
|
|
37
|
+
return { ...stubs, ...(inRepository ? { '@swiftbrowser/react': reactSources } : {}) };
|
|
38
|
+
}
|
|
28
39
|
function common(options) {
|
|
29
40
|
const mode = options.mode ?? (options.minify ? 'production' : 'development');
|
|
30
41
|
return {
|
|
@@ -45,7 +56,7 @@ function common(options) {
|
|
|
45
56
|
/** Builds the two halves and joins them. */
|
|
46
57
|
export async function bundle(options) {
|
|
47
58
|
const runtime = await esbuild.build({ ...common(options), entryPoints: [runtimeEntry] });
|
|
48
|
-
const app = await esbuild.build({ ...common(options), entryPoints: [options.entry], alias:
|
|
59
|
+
const app = await esbuild.build({ ...common(options), entryPoints: [options.entry], alias: aliases(options.entry) });
|
|
49
60
|
const code = `${text(runtime)}\n${text(app)}`;
|
|
50
61
|
return {
|
|
51
62
|
code,
|
package/dist/build.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"}
|
package/dist/site.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native app as a static site (`swiftbrowser-native site`): the same
|
|
3
|
+
* bundle `build` writes for the Xcode target, run in the browser by the
|
|
4
|
+
* SwiftBrowser Native module built for WebAssembly (docs/native.md, "The
|
|
5
|
+
* round trip"): the native views compiled against the SwiftUI shim, with the
|
|
6
|
+
* page's JavaScript as the engine. The web renderer draws what the shim
|
|
7
|
+
* emits, as it draws any Swift app. The site is what `swiftbrowser build`
|
|
8
|
+
* writes for a Swift app: the page at `/`, the module, the bundle as the
|
|
9
|
+
* app's one resource (`/bundle/app.js`, which the module evaluates at
|
|
10
|
+
* launch) and the manifest. Node only.
|
|
11
|
+
*/
|
|
12
|
+
import { type BuildOptions } from './build.js';
|
|
13
|
+
/** The module as the package ships it (publish.yml builds it; `wasm/` is gitignored in the repository). */
|
|
14
|
+
export declare const PACKAGED_MODULE: string;
|
|
15
|
+
export interface SiteOptions extends Omit<BuildOptions, 'outfile'> {
|
|
16
|
+
/** The directory to write the site into (emptied first). */
|
|
17
|
+
outDir: string;
|
|
18
|
+
/**
|
|
19
|
+
* The site's app name (letters, digits, `_` and `-`): the module's file
|
|
20
|
+
* name and the document title. The entry's project folder's name by default
|
|
21
|
+
* (`js/src/index.tsx` → the folder holding `js`, as `native init` lays an
|
|
22
|
+
* app out), else `App`.
|
|
23
|
+
*/
|
|
24
|
+
name?: string;
|
|
25
|
+
/** Shown as the document title and in the web manifest; `name` by default. */
|
|
26
|
+
displayName?: string;
|
|
27
|
+
/** The SwiftBrowser Native module built for WebAssembly; `PACKAGED_MODULE` by default. */
|
|
28
|
+
module?: string;
|
|
29
|
+
/** The path the site is served under when it is not a host's root (`/native/`); `/` by default. */
|
|
30
|
+
base?: string;
|
|
31
|
+
/** Vite's log level while building the site; `warn` by default. */
|
|
32
|
+
logLevel?: 'info' | 'warn' | 'error' | 'silent';
|
|
33
|
+
}
|
|
34
|
+
export interface SiteResult {
|
|
35
|
+
name: string;
|
|
36
|
+
outDir: string;
|
|
37
|
+
module: string;
|
|
38
|
+
moduleBytes: number;
|
|
39
|
+
bundleBytes: number;
|
|
40
|
+
warnings: string[];
|
|
41
|
+
}
|
|
42
|
+
/** The app name an entry suggests: the folder holding its `js/` project, else the entry's own folder. */
|
|
43
|
+
export declare function siteNameFor(entry: string): string;
|
|
44
|
+
/** Bundles the app and writes the site around the module. */
|
|
45
|
+
export declare function buildNativeSite(options: SiteOptions): Promise<SiteResult>;
|
|
46
|
+
//# sourceMappingURL=site.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site.d.ts","sourceRoot":"","sources":["../src/site.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,OAAO,EAAU,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAEvD,2GAA2G;AAC3G,eAAO,MAAM,eAAe,QAA6E,CAAC;AAE1G,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC;IAChE,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0FAA0F;IAC1F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mGAAmG;IACnG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;CACjD;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAID,yGAAyG;AACzG,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED,6DAA6D;AAC7D,wBAAsB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAqC/E"}
|
package/dist/site.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native app as a static site (`swiftbrowser-native site`): the same
|
|
3
|
+
* bundle `build` writes for the Xcode target, run in the browser by the
|
|
4
|
+
* SwiftBrowser Native module built for WebAssembly (docs/native.md, "The
|
|
5
|
+
* round trip"): the native views compiled against the SwiftUI shim, with the
|
|
6
|
+
* page's JavaScript as the engine. The web renderer draws what the shim
|
|
7
|
+
* emits, as it draws any Swift app. The site is what `swiftbrowser build`
|
|
8
|
+
* writes for a Swift app: the page at `/`, the module, the bundle as the
|
|
9
|
+
* app's one resource (`/bundle/app.js`, which the module evaluates at
|
|
10
|
+
* launch) and the manifest. Node only.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdtempSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { buildSite } from '@swiftbrowser/web';
|
|
17
|
+
import { bundle } from './build.js';
|
|
18
|
+
/** The module as the package ships it (publish.yml builds it; `wasm/` is gitignored in the repository). */
|
|
19
|
+
export const PACKAGED_MODULE = fileURLToPath(new URL('../wasm/SwiftBrowserNative.wasm', import.meta.url));
|
|
20
|
+
const APP_NAME = /^[A-Za-z0-9_-]+$/;
|
|
21
|
+
/** The app name an entry suggests: the folder holding its `js/` project, else the entry's own folder. */
|
|
22
|
+
export function siteNameFor(entry) {
|
|
23
|
+
const dir = path.dirname(path.resolve(entry));
|
|
24
|
+
const parts = dir.split(path.sep);
|
|
25
|
+
const js = parts.lastIndexOf('js');
|
|
26
|
+
const candidate = js > 0 ? parts[js - 1] : path.basename(dir) === 'src' ? parts[parts.length - 2] : path.basename(dir);
|
|
27
|
+
const cleaned = (candidate ?? '').replace(/[^A-Za-z0-9_-]/g, '');
|
|
28
|
+
return cleaned && APP_NAME.test(cleaned) ? cleaned : 'App';
|
|
29
|
+
}
|
|
30
|
+
/** Bundles the app and writes the site around the module. */
|
|
31
|
+
export async function buildNativeSite(options) {
|
|
32
|
+
const name = options.name ?? siteNameFor(options.entry);
|
|
33
|
+
if (!APP_NAME.test(name))
|
|
34
|
+
throw new Error(`site: the app name must be letters, digits, "_" and "-": ${name}`);
|
|
35
|
+
const module = path.resolve(options.module ?? PACKAGED_MODULE);
|
|
36
|
+
if (!existsSync(module)) {
|
|
37
|
+
throw new Error(module === PACKAGED_MODULE
|
|
38
|
+
? `site: this @swiftbrowser/native has no module at ${module}; pass one with --module (swift build --swift-sdk <wasm sdk> -c release --product NativeRoundTrip in a SwiftBrowser checkout)`
|
|
39
|
+
: `site: no module at ${module}`);
|
|
40
|
+
}
|
|
41
|
+
if (options.base !== undefined && !/^\/([^/].*\/)?$/.test(options.base))
|
|
42
|
+
throw new Error(`site: --base must start and end with "/": ${options.base}`);
|
|
43
|
+
const { outDir, name: _name, displayName, module: _module, base, logLevel, ...bundleOptions } = options;
|
|
44
|
+
// The bundle is the app's one resource: published at `/bundle/<name>/app.js`
|
|
45
|
+
// and mounted at `/bundle/app.js`, where the module's launch reads it.
|
|
46
|
+
const resources = mkdtempSync(path.join(tmpdir(), 'swiftbrowser-native-site-'));
|
|
47
|
+
try {
|
|
48
|
+
const built = await bundle({ ...bundleOptions, outfile: path.join(resources, 'app.js') });
|
|
49
|
+
writeFileSync(path.join(resources, 'app.js'), built.code);
|
|
50
|
+
await buildSite({
|
|
51
|
+
apps: [{
|
|
52
|
+
name,
|
|
53
|
+
displayName: displayName ?? name,
|
|
54
|
+
description: 'A SwiftBrowser Native app in the browser',
|
|
55
|
+
wasm: module,
|
|
56
|
+
catalogs: [],
|
|
57
|
+
resources: [path.join(resources, 'app.js')],
|
|
58
|
+
}],
|
|
59
|
+
layout: 'app',
|
|
60
|
+
outDir: path.resolve(outDir),
|
|
61
|
+
...(base !== undefined ? { base } : {}),
|
|
62
|
+
logLevel: logLevel ?? 'warn',
|
|
63
|
+
});
|
|
64
|
+
return { name, outDir: path.resolve(outDir), module, moduleBytes: statSync(module).size, bundleBytes: built.bytes, warnings: built.warnings };
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
rmSync(resources, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=site.js.map
|
package/dist/site.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site.js","sourceRoot":"","sources":["../src/site.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnF,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAqB,MAAM,YAAY,CAAC;AAEvD,2GAA2G;AAC3G,MAAM,CAAC,MAAM,eAAe,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,iCAAiC,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AA+B1G,MAAM,QAAQ,GAAG,kBAAkB,CAAC;AAEpC,yGAAyG;AACzG,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAClC,MAAM,EAAE,GAAG,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,SAAS,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IACvH,MAAM,OAAO,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;IACjE,OAAO,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;AAC7D,CAAC;AAED,6DAA6D;AAC7D,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAoB;IACxD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACxD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,4DAA4D,IAAI,EAAE,CAAC,CAAC;IAC9G,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,eAAe,CAAC,CAAC;IAC/D,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,MAAM,KAAK,eAAe;YACxB,CAAC,CAAC,oDAAoD,MAAM,+HAA+H;YAC3L,CAAC,CAAC,sBAAsB,MAAM,EAAE,CACnC,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IACtJ,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,aAAa,EAAE,GAAG,OAAO,CAAC;IACxG,6EAA6E;IAC7E,uEAAuE;IACvE,MAAM,SAAS,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,2BAA2B,CAAC,CAAC,CAAC;IAChF,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,EAAE,GAAG,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC;QAC1F,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAC1D,MAAM,SAAS,CAAC;YACd,IAAI,EAAE,CAAC;oBACL,IAAI;oBACJ,WAAW,EAAE,WAAW,IAAI,IAAI;oBAChC,WAAW,EAAE,0CAA0C;oBACvD,IAAI,EAAE,MAAM;oBACZ,QAAQ,EAAE,EAAE;oBACZ,SAAS,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;iBAC5C,CAAC;YACF,MAAM,EAAE,KAAK;YACb,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;YAC5B,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACvC,QAAQ,EAAE,QAAQ,IAAI,MAAM;SAC7B,CAAC,CAAC;QACH,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;IAChJ,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;AACH,CAAC"}
|
package/dist/wasmHost.d.ts
CHANGED
|
@@ -1,35 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
2
|
+
* `@swiftbrowser/native/wasm-host`: the host's JavaScript as SwiftBrowser
|
|
3
|
+
* Native's engine for a module built with the round-trip target (docs/native.md,
|
|
4
|
+
* "The round trip"). The implementation lives in the web renderer
|
|
5
|
+
* (Web/src/nativeHost.ts), which attaches it to any module that imports
|
|
6
|
+
* `sb_native`; this entry is for Node (scripts/run-wasi.mjs) and tests.
|
|
15
7
|
*/
|
|
16
|
-
export
|
|
17
|
-
/** Runs a script at the global scope (default: an indirect `eval`). */
|
|
18
|
-
evaluate?: (script: string, name: string) => void;
|
|
19
|
-
/** The global object the bundle's globals live on (default `globalThis`). */
|
|
20
|
-
global?: Record<string, unknown>;
|
|
21
|
-
}
|
|
22
|
-
export interface WasmNativeHost {
|
|
23
|
-
/** Merge into the module's import object before instantiating. */
|
|
24
|
-
imports: {
|
|
25
|
-
sb_native: {
|
|
26
|
-
request: (kind: number, kindLength: number, a: number, aLength: number, b: number, bLength: number) => number;
|
|
27
|
-
};
|
|
28
|
-
};
|
|
29
|
-
/** The instance, once instantiated: its memory and the exports the host calls. */
|
|
30
|
-
attach(instance: WebAssembly.Instance): void;
|
|
31
|
-
/** The `__sbNative` object as it stands (replaced by a reset). */
|
|
32
|
-
readonly native: Record<string, (...args: unknown[]) => unknown>;
|
|
33
|
-
}
|
|
34
|
-
export declare function wasmNativeHost(options?: WasmNativeHostOptions): WasmNativeHost;
|
|
8
|
+
export { wasmNativeHost, type WasmNativeHost, type WasmNativeHostOptions } from '@swiftbrowser/web';
|
|
35
9
|
//# sourceMappingURL=wasmHost.d.ts.map
|
package/dist/wasmHost.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wasmHost.d.ts","sourceRoot":"","sources":["../src/wasmHost.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"wasmHost.d.ts","sourceRoot":"","sources":["../src/wasmHost.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/wasmHost.js
CHANGED
|
@@ -1,120 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
2
|
+
* `@swiftbrowser/native/wasm-host`: the host's JavaScript as SwiftBrowser
|
|
3
|
+
* Native's engine for a module built with the round-trip target (docs/native.md,
|
|
4
|
+
* "The round trip"). The implementation lives in the web renderer
|
|
5
|
+
* (Web/src/nativeHost.ts), which attaches it to any module that imports
|
|
6
|
+
* `sb_native`; this entry is for Node (scripts/run-wasi.mjs) and tests.
|
|
15
7
|
*/
|
|
16
|
-
|
|
17
|
-
const BUNDLE_GLOBALS = ['__sbDispatch', '__sbTimerFired', '__sbFetchResult', '__sbRefresh', '__sbVendor'];
|
|
18
|
-
export function wasmNativeHost(options = {}) {
|
|
19
|
-
const g = options.global ?? globalThis;
|
|
20
|
-
const evaluate = options.evaluate ?? ((script) => (0, eval)(script));
|
|
21
|
-
const encoder = new TextEncoder();
|
|
22
|
-
const decoder = new TextDecoder();
|
|
23
|
-
let exports = null;
|
|
24
|
-
let native = {};
|
|
25
|
-
const need = () => {
|
|
26
|
-
if (!exports)
|
|
27
|
-
throw new Error('wasmNativeHost: attach(instance) before the module runs');
|
|
28
|
-
return exports;
|
|
29
|
-
};
|
|
30
|
-
const read = (pointer, length) => decoder.decode(new Uint8Array(need().memory.buffer, pointer, length));
|
|
31
|
-
/** A buffer from `sb_alloc` with the text's length and bytes; Swift frees it. */
|
|
32
|
-
const answer = (value) => {
|
|
33
|
-
const bytes = encoder.encode(JSON.stringify(value));
|
|
34
|
-
const pointer = need().sb_alloc(bytes.length + 4);
|
|
35
|
-
// The allocation may have grown the memory: take the view after it.
|
|
36
|
-
const view = new DataView(need().memory.buffer);
|
|
37
|
-
view.setUint32(pointer, bytes.length, true);
|
|
38
|
-
new Uint8Array(need().memory.buffer, pointer + 4, bytes.length).set(bytes);
|
|
39
|
-
return pointer;
|
|
40
|
-
};
|
|
41
|
-
const describe = (error) => (error instanceof Error ? (error.stack ?? error.message) : String(error));
|
|
42
|
-
/** A native function's call: into Swift and back with its JSON result. */
|
|
43
|
-
const invoke = (name, args) => {
|
|
44
|
-
const nameBytes = encoder.encode(name);
|
|
45
|
-
const argsBytes = encoder.encode(JSON.stringify(args));
|
|
46
|
-
const namePointer = need().sb_alloc(nameBytes.length || 1);
|
|
47
|
-
new Uint8Array(need().memory.buffer, namePointer, nameBytes.length).set(nameBytes);
|
|
48
|
-
const argsPointer = need().sb_alloc(argsBytes.length || 1);
|
|
49
|
-
new Uint8Array(need().memory.buffer, argsPointer, argsBytes.length).set(argsBytes);
|
|
50
|
-
const result = need().sb_native_invoke(namePointer, nameBytes.length, argsPointer, argsBytes.length);
|
|
51
|
-
if (result === 0)
|
|
52
|
-
return undefined;
|
|
53
|
-
const length = new DataView(need().memory.buffer).getUint32(result, true);
|
|
54
|
-
const text = read(result + 4, length);
|
|
55
|
-
need().sb_native_free(result);
|
|
56
|
-
const value = JSON.parse(text);
|
|
57
|
-
if (value && typeof value === 'object' && 'error' in value && Object.keys(value).length === 1) {
|
|
58
|
-
throw new Error(String(value.error));
|
|
59
|
-
}
|
|
60
|
-
return value;
|
|
61
|
-
};
|
|
62
|
-
const reset = () => {
|
|
63
|
-
for (const name of BUNDLE_GLOBALS)
|
|
64
|
-
delete g[name];
|
|
65
|
-
native = {};
|
|
66
|
-
g['__sbNative'] = native;
|
|
67
|
-
};
|
|
68
|
-
const request = (kind, kindLength, a, aLength, b, bLength) => {
|
|
69
|
-
const what = read(kind, kindLength);
|
|
70
|
-
const first = read(a, aLength);
|
|
71
|
-
const second = read(b, bLength);
|
|
72
|
-
switch (what) {
|
|
73
|
-
case 'reset':
|
|
74
|
-
reset();
|
|
75
|
-
return 0;
|
|
76
|
-
case 'install':
|
|
77
|
-
native[first] = (...args) => invoke(first, args);
|
|
78
|
-
return 0;
|
|
79
|
-
case 'eval':
|
|
80
|
-
try {
|
|
81
|
-
evaluate(first, second);
|
|
82
|
-
return 0;
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
return answer({ error: describe(error) });
|
|
86
|
-
}
|
|
87
|
-
case 'call': {
|
|
88
|
-
const fn = g[first];
|
|
89
|
-
if (typeof fn !== 'function')
|
|
90
|
-
return answer({ error: `${first} is not defined` });
|
|
91
|
-
try {
|
|
92
|
-
const result = fn(...JSON.parse(second));
|
|
93
|
-
return answer({ ok: result === undefined ? null : result });
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
return answer({ error: describe(error) });
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
case 'has':
|
|
100
|
-
return answer({ ok: first in g });
|
|
101
|
-
default:
|
|
102
|
-
return answer({ error: `unknown request ${what}` });
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
reset();
|
|
106
|
-
return {
|
|
107
|
-
imports: { sb_native: { request } },
|
|
108
|
-
attach(instance) {
|
|
109
|
-
for (const name of ['memory', 'sb_alloc', 'sb_native_invoke', 'sb_native_free']) {
|
|
110
|
-
if (!(name in instance.exports))
|
|
111
|
-
throw new Error(`wasmNativeHost: the module does not export ${name}`);
|
|
112
|
-
}
|
|
113
|
-
exports = instance.exports;
|
|
114
|
-
},
|
|
115
|
-
get native() {
|
|
116
|
-
return native;
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
}
|
|
8
|
+
export { wasmNativeHost } from '@swiftbrowser/web';
|
|
120
9
|
//# sourceMappingURL=wasmHost.js.map
|
package/dist/wasmHost.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wasmHost.js","sourceRoot":"","sources":["../src/wasmHost.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"wasmHost.js","sourceRoot":"","sources":["../src/wasmHost.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,cAAc,EAAmD,MAAM,mBAAmB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swiftbrowser/native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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": {
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"types": "./dist/build.d.ts",
|
|
24
24
|
"default": "./dist/build.js"
|
|
25
25
|
},
|
|
26
|
+
"./site": {
|
|
27
|
+
"types": "./dist/site.d.ts",
|
|
28
|
+
"default": "./dist/site.js"
|
|
29
|
+
},
|
|
26
30
|
"./wasm-host": {
|
|
27
31
|
"types": "./dist/wasmHost.d.ts",
|
|
28
32
|
"default": "./dist/wasmHost.js"
|
|
@@ -36,6 +40,7 @@
|
|
|
36
40
|
"src",
|
|
37
41
|
"bin",
|
|
38
42
|
"swift",
|
|
43
|
+
"wasm",
|
|
39
44
|
"README.md"
|
|
40
45
|
],
|
|
41
46
|
"sideEffects": [
|
|
@@ -54,8 +59,8 @@
|
|
|
54
59
|
},
|
|
55
60
|
"dependencies": {
|
|
56
61
|
"@babel/core": "^7.29.7",
|
|
57
|
-
"@swiftbrowser/react": "0.
|
|
58
|
-
"@swiftbrowser/web": "0.
|
|
62
|
+
"@swiftbrowser/react": "0.11.0",
|
|
63
|
+
"@swiftbrowser/web": "0.11.0",
|
|
59
64
|
"esbuild": "^0.28.2",
|
|
60
65
|
"react-refresh": "^0.17.0"
|
|
61
66
|
},
|
package/src/build.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { createHash } from 'node:crypto';
|
|
|
11
11
|
import { existsSync } from 'node:fs';
|
|
12
12
|
import { createServer } from 'node:http';
|
|
13
13
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
14
|
-
import { dirname } from 'node:path';
|
|
14
|
+
import { dirname, resolve, sep } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
|
|
17
17
|
export interface BuildOptions {
|
|
@@ -35,16 +35,28 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
35
35
|
/** The runtime's source; the package ships `src` so this resolves from `dist` too. */
|
|
36
36
|
const runtimeEntry = fileURLToPath(new URL('../src/runtime.ts', import.meta.url));
|
|
37
37
|
const reactSources = fileURLToPath(new URL('../../react/src/index.ts', import.meta.url));
|
|
38
|
+
/** The repository, when this runs from a checkout (installed, the package has no `packages/react` beside it). */
|
|
39
|
+
const repository = existsSync(reactSources) ? resolve(here, '../../..') : null;
|
|
38
40
|
const stubs: Record<string, string> = {
|
|
39
41
|
'react-dom/client': fileURLToPath(new URL('../src/stubs/react-dom-client.ts', import.meta.url)),
|
|
40
42
|
'react-dom': fileURLToPath(new URL('../src/stubs/react-dom.ts', import.meta.url)),
|
|
41
43
|
// This package from its sources, so a bundle never needs its `dist` built first.
|
|
42
44
|
'@swiftbrowser/native/runtime': runtimeEntry,
|
|
43
45
|
'@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
46
|
};
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The aliases for an entry: in this repository, an app of the repository's
|
|
50
|
+
* (the examples) gets @swiftbrowser/react from its sources too (installed,
|
|
51
|
+
* it is a built package). An app outside it (a project built with the
|
|
52
|
+
* checkout's bundler) keeps its own installed copy: the sources would bring
|
|
53
|
+
* the repository's React along, a second copy beside the app's.
|
|
54
|
+
*/
|
|
55
|
+
function aliases(entry: string): Record<string, string> {
|
|
56
|
+
const inRepository = repository !== null && resolve(entry).startsWith(repository + sep);
|
|
57
|
+
return { ...stubs, ...(inRepository ? { '@swiftbrowser/react': reactSources } : {}) };
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
function common(options: BuildOptions): esbuild.BuildOptions {
|
|
49
61
|
const mode = options.mode ?? (options.minify ? 'production' : 'development');
|
|
50
62
|
return {
|
|
@@ -66,7 +78,7 @@ function common(options: BuildOptions): esbuild.BuildOptions {
|
|
|
66
78
|
/** Builds the two halves and joins them. */
|
|
67
79
|
export async function bundle(options: BuildOptions): Promise<BuildResult> {
|
|
68
80
|
const runtime = await esbuild.build({ ...common(options), entryPoints: [runtimeEntry] });
|
|
69
|
-
const app = await esbuild.build({ ...common(options), entryPoints: [options.entry], alias:
|
|
81
|
+
const app = await esbuild.build({ ...common(options), entryPoints: [options.entry], alias: aliases(options.entry) });
|
|
70
82
|
const code = `${text(runtime)}\n${text(app)}`;
|
|
71
83
|
return {
|
|
72
84
|
code,
|
package/src/site.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native app as a static site (`swiftbrowser-native site`): the same
|
|
3
|
+
* bundle `build` writes for the Xcode target, run in the browser by the
|
|
4
|
+
* SwiftBrowser Native module built for WebAssembly (docs/native.md, "The
|
|
5
|
+
* round trip"): the native views compiled against the SwiftUI shim, with the
|
|
6
|
+
* page's JavaScript as the engine. The web renderer draws what the shim
|
|
7
|
+
* emits, as it draws any Swift app. The site is what `swiftbrowser build`
|
|
8
|
+
* writes for a Swift app: the page at `/`, the module, the bundle as the
|
|
9
|
+
* app's one resource (`/bundle/app.js`, which the module evaluates at
|
|
10
|
+
* launch) and the manifest. Node only.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, mkdtempSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { buildSite } from '@swiftbrowser/web';
|
|
18
|
+
import { bundle, type BuildOptions } from './build.js';
|
|
19
|
+
|
|
20
|
+
/** The module as the package ships it (publish.yml builds it; `wasm/` is gitignored in the repository). */
|
|
21
|
+
export const PACKAGED_MODULE = fileURLToPath(new URL('../wasm/SwiftBrowserNative.wasm', import.meta.url));
|
|
22
|
+
|
|
23
|
+
export interface SiteOptions extends Omit<BuildOptions, 'outfile'> {
|
|
24
|
+
/** The directory to write the site into (emptied first). */
|
|
25
|
+
outDir: string;
|
|
26
|
+
/**
|
|
27
|
+
* The site's app name (letters, digits, `_` and `-`): the module's file
|
|
28
|
+
* name and the document title. The entry's project folder's name by default
|
|
29
|
+
* (`js/src/index.tsx` → the folder holding `js`, as `native init` lays an
|
|
30
|
+
* app out), else `App`.
|
|
31
|
+
*/
|
|
32
|
+
name?: string;
|
|
33
|
+
/** Shown as the document title and in the web manifest; `name` by default. */
|
|
34
|
+
displayName?: string;
|
|
35
|
+
/** The SwiftBrowser Native module built for WebAssembly; `PACKAGED_MODULE` by default. */
|
|
36
|
+
module?: string;
|
|
37
|
+
/** The path the site is served under when it is not a host's root (`/native/`); `/` by default. */
|
|
38
|
+
base?: string;
|
|
39
|
+
/** Vite's log level while building the site; `warn` by default. */
|
|
40
|
+
logLevel?: 'info' | 'warn' | 'error' | 'silent';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SiteResult {
|
|
44
|
+
name: string;
|
|
45
|
+
outDir: string;
|
|
46
|
+
module: string;
|
|
47
|
+
moduleBytes: number;
|
|
48
|
+
bundleBytes: number;
|
|
49
|
+
warnings: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const APP_NAME = /^[A-Za-z0-9_-]+$/;
|
|
53
|
+
|
|
54
|
+
/** The app name an entry suggests: the folder holding its `js/` project, else the entry's own folder. */
|
|
55
|
+
export function siteNameFor(entry: string): string {
|
|
56
|
+
const dir = path.dirname(path.resolve(entry));
|
|
57
|
+
const parts = dir.split(path.sep);
|
|
58
|
+
const js = parts.lastIndexOf('js');
|
|
59
|
+
const candidate = js > 0 ? parts[js - 1] : path.basename(dir) === 'src' ? parts[parts.length - 2] : path.basename(dir);
|
|
60
|
+
const cleaned = (candidate ?? '').replace(/[^A-Za-z0-9_-]/g, '');
|
|
61
|
+
return cleaned && APP_NAME.test(cleaned) ? cleaned : 'App';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Bundles the app and writes the site around the module. */
|
|
65
|
+
export async function buildNativeSite(options: SiteOptions): Promise<SiteResult> {
|
|
66
|
+
const name = options.name ?? siteNameFor(options.entry);
|
|
67
|
+
if (!APP_NAME.test(name)) throw new Error(`site: the app name must be letters, digits, "_" and "-": ${name}`);
|
|
68
|
+
const module = path.resolve(options.module ?? PACKAGED_MODULE);
|
|
69
|
+
if (!existsSync(module)) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
module === PACKAGED_MODULE
|
|
72
|
+
? `site: this @swiftbrowser/native has no module at ${module}; pass one with --module (swift build --swift-sdk <wasm sdk> -c release --product NativeRoundTrip in a SwiftBrowser checkout)`
|
|
73
|
+
: `site: no module at ${module}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
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;
|
|
78
|
+
// The bundle is the app's one resource: published at `/bundle/<name>/app.js`
|
|
79
|
+
// and mounted at `/bundle/app.js`, where the module's launch reads it.
|
|
80
|
+
const resources = mkdtempSync(path.join(tmpdir(), 'swiftbrowser-native-site-'));
|
|
81
|
+
try {
|
|
82
|
+
const built = await bundle({ ...bundleOptions, outfile: path.join(resources, 'app.js') });
|
|
83
|
+
writeFileSync(path.join(resources, 'app.js'), built.code);
|
|
84
|
+
await buildSite({
|
|
85
|
+
apps: [{
|
|
86
|
+
name,
|
|
87
|
+
displayName: displayName ?? name,
|
|
88
|
+
description: 'A SwiftBrowser Native app in the browser',
|
|
89
|
+
wasm: module,
|
|
90
|
+
catalogs: [],
|
|
91
|
+
resources: [path.join(resources, 'app.js')],
|
|
92
|
+
}],
|
|
93
|
+
layout: 'app',
|
|
94
|
+
outDir: path.resolve(outDir),
|
|
95
|
+
...(base !== undefined ? { base } : {}),
|
|
96
|
+
logLevel: logLevel ?? 'warn',
|
|
97
|
+
});
|
|
98
|
+
return { name, outDir: path.resolve(outDir), module, moduleBytes: statSync(module).size, bundleBytes: built.bytes, warnings: built.warnings };
|
|
99
|
+
} finally {
|
|
100
|
+
rmSync(resources, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/wasmHost.ts
CHANGED
|
@@ -1,147 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
2
|
+
* `@swiftbrowser/native/wasm-host`: the host's JavaScript as SwiftBrowser
|
|
3
|
+
* Native's engine for a module built with the round-trip target (docs/native.md,
|
|
4
|
+
* "The round trip"). The implementation lives in the web renderer
|
|
5
|
+
* (Web/src/nativeHost.ts), which attaches it to any module that imports
|
|
6
|
+
* `sb_native`; this entry is for Node (scripts/run-wasi.mjs) and tests.
|
|
15
7
|
*/
|
|
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
|
-
}
|
|
8
|
+
export { wasmNativeHost, type WasmNativeHost, type WasmNativeHostOptions } from '@swiftbrowser/web';
|
|
@@ -57,6 +57,7 @@ struct ElementView: View {
|
|
|
57
57
|
case "swipeactions": return AnyView(SwipeActionsElement(node: node))
|
|
58
58
|
// Navigation and presentation
|
|
59
59
|
case "navstack": return AnyView(NavigationStackElement(node: node))
|
|
60
|
+
case "navsplit": return AnyView(NavigationSplitElement(node: node))
|
|
60
61
|
case "navscreen": return AnyView(NavigationScreenElement(node: node))
|
|
61
62
|
case "navlink": return AnyView(NavigationLinkElement(node: node))
|
|
62
63
|
case "tabview": return AnyView(TabViewElement(node: node))
|
|
@@ -15,10 +15,19 @@ struct NavigationStackElement: View {
|
|
|
15
15
|
@Environment(\.nativeApp) private var app
|
|
16
16
|
|
|
17
17
|
var body: some View {
|
|
18
|
-
|
|
18
|
+
Self.stack(for: node, app: app)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// The stack itself, with its root screen and the pushed ones as its
|
|
22
|
+
/// path. A split view's detail column is one of these directly (not a
|
|
23
|
+
/// view around one): the shim wraps a detail that is not a
|
|
24
|
+
/// `NavigationStack` in a stack of its own, and a stack nested in a
|
|
25
|
+
/// stack is flattened, its path with it.
|
|
26
|
+
static func stack(for node: ElementNode?, app: NativeApp?) -> NavigationStack<[Int], AnyView> {
|
|
27
|
+
let screens = node?.children.filter { $0.kind == "navscreen" } ?? []
|
|
19
28
|
let pushed = screens.dropFirst().map(\.id)
|
|
20
29
|
let path = Binding<[Int]>(
|
|
21
|
-
get: { node
|
|
30
|
+
get: { node?.children.filter { $0.kind == "navscreen" }.dropFirst().map(\.id) ?? [] },
|
|
22
31
|
set: { next in
|
|
23
32
|
// Only pops come from here (pushes come from the app).
|
|
24
33
|
for id in pushed where !next.contains(id) {
|
|
@@ -26,21 +35,69 @@ struct NavigationStackElement: View {
|
|
|
26
35
|
}
|
|
27
36
|
}
|
|
28
37
|
)
|
|
29
|
-
NavigationStack(path: path) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
return NavigationStack(path: path) {
|
|
39
|
+
AnyView(
|
|
40
|
+
Group {
|
|
41
|
+
if let root = screens.first {
|
|
42
|
+
NavigationScreenElement(node: root)
|
|
43
|
+
}
|
|
33
44
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
.navigationDestination(for: Int.self) { id in
|
|
46
|
+
if let screen = node?.children.first(where: { $0.id == id }) {
|
|
47
|
+
NavigationScreenElement(node: screen)
|
|
48
|
+
} else {
|
|
49
|
+
// Popped by the app already; SwiftUI is on its way out.
|
|
50
|
+
Color.clear
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// `navsplit`: the sidebar's and the detail's stacks (its two `navstack`
|
|
59
|
+
/// children, in that order) as the columns of a split view. The app emits
|
|
60
|
+
/// it in a regular width only; in a compact width it emits one stack rooted
|
|
61
|
+
/// at the sidebar, as iOS shows. `sidebarVisible` is the column visibility
|
|
62
|
+
/// (`detailOnly` hides the sidebar); the sidebar toggle sends `tap` with the
|
|
63
|
+
/// split's id, and the app flips the visibility, as the protocol says.
|
|
64
|
+
struct NavigationSplitElement: View {
|
|
65
|
+
let node: ElementNode
|
|
66
|
+
@Environment(\.nativeApp) private var app
|
|
67
|
+
|
|
68
|
+
var body: some View {
|
|
69
|
+
let columns = node.children.filter { $0.kind == "navstack" }
|
|
70
|
+
let sidebar = columns.first
|
|
71
|
+
let detail = columns.count > 1 ? columns[1] : nil
|
|
72
|
+
let visibility = Binding<NavigationSplitViewVisibility>(
|
|
73
|
+
get: { NavSplitProps(props: node.props).sidebarVisible ? .all : .detailOnly },
|
|
74
|
+
set: { next in
|
|
75
|
+
if (next == .detailOnly) == NavSplitProps(props: node.props).sidebarVisible {
|
|
76
|
+
app?.dispatch(.tap(node.id))
|
|
41
77
|
}
|
|
42
78
|
}
|
|
79
|
+
)
|
|
80
|
+
NavigationSplitView(columnVisibility: visibility) {
|
|
81
|
+
sidebarColumn(sidebar)
|
|
82
|
+
} detail: {
|
|
83
|
+
NavigationStackElement.stack(for: detail, app: app)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// The sidebar column: natively its stack (the column pushes within
|
|
88
|
+
/// itself); against the shim its root screen alone, since the shim gives
|
|
89
|
+
/// the sidebar a stack of its own and would flatten this one into it.
|
|
90
|
+
@ViewBuilder
|
|
91
|
+
private func sidebarColumn(_ sidebar: ElementNode?) -> some View {
|
|
92
|
+
#if canImport(FoundationBridge)
|
|
93
|
+
if let root = sidebar?.children.first(where: { $0.kind == "navscreen" }) {
|
|
94
|
+
NavigationScreenElement(node: root)
|
|
43
95
|
}
|
|
96
|
+
#else
|
|
97
|
+
if let sidebar {
|
|
98
|
+
NavigationStackElement(node: sidebar)
|
|
99
|
+
}
|
|
100
|
+
#endif
|
|
44
101
|
}
|
|
45
102
|
}
|
|
46
103
|
|
|
@@ -183,6 +183,12 @@ public struct NavScreenProps: Equatable, Sendable {
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
public struct NavSplitProps: Equatable, Sendable {
|
|
187
|
+
/// Whether the sidebar column shows (`false` hides it).
|
|
188
|
+
public var sidebarVisible: Bool
|
|
189
|
+
public init(props: JSONObject) { sidebarVisible = props.bool("sidebarVisible") ?? true }
|
|
190
|
+
}
|
|
191
|
+
|
|
186
192
|
public struct NavLinkProps: Equatable, Sendable {
|
|
187
193
|
public var selected: Bool
|
|
188
194
|
public var plain: Bool
|