@swiftbrowser/web 0.6.6 → 0.7.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/node/dist/node/config.d.ts +6 -0
- package/node/dist/node/config.d.ts.map +1 -1
- package/node/dist/node/config.js +7 -0
- package/node/dist/node/config.js.map +1 -1
- package/node/dist/plugins/playground.d.ts +26 -0
- package/node/dist/plugins/playground.d.ts.map +1 -0
- package/node/dist/plugins/playground.js +46 -0
- package/node/dist/plugins/playground.js.map +1 -0
- package/node/dist/src/playground/files.d.ts +12 -0
- package/node/dist/src/playground/files.d.ts.map +1 -0
- package/node/dist/src/playground/files.js +7 -0
- package/node/dist/src/playground/files.js.map +1 -0
- package/package.json +4 -3
- package/plugins/playground.ts +56 -0
- package/src/main.ts +5 -1
- package/src/pinch.ts +324 -0
- package/src/playground/assets.ts +96 -0
- package/src/playground/build.ts +149 -0
- package/src/playground/files.ts +18 -0
- package/src/playground/main.ts +95 -0
- package/src/playground/pack.ts +57 -0
- package/src/playground/playground.css +30 -0
- package/src/playground/worker.ts +52 -0
- package/src/protocol.ts +32 -1
- package/src/renderer.ts +6 -0
- package/src/styles.css +16 -2
- package/src/vite-env.d.ts +4 -0
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { UserConfig } from 'vite';
|
|
16
16
|
import { type SiteLayout, type SitePageMode } from '../plugins/site-layout.js';
|
|
17
|
+
import { type PlaygroundOptions } from '../plugins/playground.js';
|
|
17
18
|
import { type AppSpec } from './apps.js';
|
|
18
19
|
export interface SiteOptions {
|
|
19
20
|
apps: AppSpec[];
|
|
@@ -40,6 +41,11 @@ export interface SiteOptions {
|
|
|
40
41
|
* Default `auto`; `buildSite` builds an `app` layout site with `device`.
|
|
41
42
|
*/
|
|
42
43
|
pageMode?: SitePageMode;
|
|
44
|
+
/**
|
|
45
|
+
* Adds the playground page (plugins/playground.ts), which compiles Swift in
|
|
46
|
+
* the browser. This repository's site only.
|
|
47
|
+
*/
|
|
48
|
+
playground?: PlaygroundOptions;
|
|
43
49
|
}
|
|
44
50
|
/** The layout `options` resolve to (see `SiteOptions.layout`). */
|
|
45
51
|
export declare function siteLayout(options: Pick<SiteOptions, 'apps' | 'sites' | 'layout'>): SiteLayout;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAKvC,OAAO,EAAmD,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAKvC,OAAO,EAAmD,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAChI,OAAO,EAA0B,KAAK,iBAAiB,EAAwD,MAAM,0BAA0B,CAAC;AAEhJ,OAAO,EAAmC,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1E,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,sGAAsG;IACtG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB;;;OAGG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAC;CAChC;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC,GAAG,UAAU,CAG9F;AAED,qGAAqG;AACrG,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oHAAoH;IACpH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,yGAAyG;IACzG,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,0GAA0G;IAC1G,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAOD;;;;;GAKG;AACH,wBAAgB,OAAO,IAAI,MAAM,CAoBhC;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,UAAU,EAAE,CAWnE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,CA4DjE"}
|
package/node/dist/node/config.js
CHANGED
|
@@ -20,6 +20,7 @@ import { assetCatalogs } from '../plugins/asset-catalogs.js';
|
|
|
20
20
|
import { bundleResources } from '../plugins/bundle-resources.js';
|
|
21
21
|
import { includedSites, readSiteApps } from '../plugins/included-sites.js';
|
|
22
22
|
import { ROOT_START_URL, siteLayoutPlugin } from '../plugins/site-layout.js';
|
|
23
|
+
import { PLAYGROUND_ASSETS_PATH, playgroundAliases, playgroundAssets, playgroundFiles } from '../plugins/playground.js';
|
|
23
24
|
import { swiftWasm } from '../plugins/swift-wasm.js';
|
|
24
25
|
import { displayNameOf, validateAppSpecs } from './apps.js';
|
|
25
26
|
/** The layout `options` resolve to (see `SiteOptions.layout`). */
|
|
@@ -110,6 +111,8 @@ export function createViteConfig(options) {
|
|
|
110
111
|
const pages = { app: path.resolve(root, 'app/index.html') };
|
|
111
112
|
if (layout === 'landing')
|
|
112
113
|
pages['landing'] = path.resolve(root, 'index.html');
|
|
114
|
+
if (options.playground)
|
|
115
|
+
pages['playground'] = path.resolve(root, 'playground/index.html');
|
|
113
116
|
return {
|
|
114
117
|
root,
|
|
115
118
|
plugins: [
|
|
@@ -119,7 +122,9 @@ export function createViteConfig(options) {
|
|
|
119
122
|
bundleResources(apps),
|
|
120
123
|
includedSites(landing, sites),
|
|
121
124
|
siteLayoutPlugin(site, main),
|
|
125
|
+
...(options.playground ? [playgroundAssets(options.playground)] : []),
|
|
122
126
|
],
|
|
127
|
+
...(options.playground ? { resolve: { alias: playgroundAliases(path.resolve(root, '..')) }, worker: { format: 'es' } } : {}),
|
|
123
128
|
// The landing page at `/` and the app at `/app/` (or the app alone). `mpa`
|
|
124
129
|
// keeps the dev server from answering unknown URLs with a page.
|
|
125
130
|
appType: 'mpa',
|
|
@@ -130,6 +135,8 @@ export function createViteConfig(options) {
|
|
|
130
135
|
__SB_COMMIT__: JSON.stringify(options.commit ?? 'dev'),
|
|
131
136
|
__SB_BRANCH__: JSON.stringify(options.branch ?? ''),
|
|
132
137
|
__SB_BUILT_AT__: JSON.stringify(new Date().toISOString()),
|
|
138
|
+
__SB_PLAYGROUND_ASSETS__: JSON.stringify(options.playground?.assetsUrl ?? PLAYGROUND_ASSETS_PATH),
|
|
139
|
+
__SB_PLAYGROUND_FILES__: JSON.stringify(options.playground ? playgroundFiles(options.playground) : null),
|
|
133
140
|
},
|
|
134
141
|
server: {
|
|
135
142
|
port: 5173,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC3E,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAqD,MAAM,2BAA2B,CAAC;AAChI,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAgB,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The playground page (playground/index.html): Swift compiled in the browser
|
|
3
|
+
* by the WebAssembly compiler (docs/in-browser-compiler.md, Milestone 3).
|
|
4
|
+
* Only this repository's site has it: its code imports
|
|
5
|
+
* @swiftbrowser/wasm-compiler and the CLI's browser entry from their sources
|
|
6
|
+
* (the aliases below). playground/build-assets.ts writes the assets and
|
|
7
|
+
* their manifest to a directory; the build reads the manifest, and the page
|
|
8
|
+
* fetches the files from /playground-assets/, where the dev server serves
|
|
9
|
+
* that directory, or, deployed, from where they were uploaded
|
|
10
|
+
* (playground/publish-assets.mjs).
|
|
11
|
+
*/
|
|
12
|
+
import type { Plugin } from 'vite';
|
|
13
|
+
import { type PlaygroundFiles } from '../src/playground/files.js';
|
|
14
|
+
export interface PlaygroundOptions {
|
|
15
|
+
/** The directory build-assets wrote: its manifest, and the files the dev server serves at /playground-assets/. */
|
|
16
|
+
assetsDir: string;
|
|
17
|
+
/** Where the page fetches the assets from; default /playground-assets/. */
|
|
18
|
+
assetsUrl?: string;
|
|
19
|
+
}
|
|
20
|
+
/** The downloads the assets' manifest names. */
|
|
21
|
+
export declare function playgroundFiles(options: PlaygroundOptions): PlaygroundFiles;
|
|
22
|
+
export declare const PLAYGROUND_ASSETS_PATH = "/playground-assets/";
|
|
23
|
+
/** The source aliases of the playground's imports, from the repository root. */
|
|
24
|
+
export declare function playgroundAliases(repoRoot: string): Record<string, string>;
|
|
25
|
+
export declare function playgroundAssets(options: PlaygroundOptions): Plugin;
|
|
26
|
+
//# sourceMappingURL=playground.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playground.d.ts","sourceRoot":"","sources":["../../../plugins/playground.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAEvF,MAAM,WAAW,iBAAiB;IAChC,kHAAkH;IAClH,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,gDAAgD;AAChD,wBAAgB,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG,eAAe,CAI3E;AAED,eAAO,MAAM,sBAAsB,wBAAwB,CAAC;AAE5D,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAK1E;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAcnE"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The playground page (playground/index.html): Swift compiled in the browser
|
|
3
|
+
* by the WebAssembly compiler (docs/in-browser-compiler.md, Milestone 3).
|
|
4
|
+
* Only this repository's site has it: its code imports
|
|
5
|
+
* @swiftbrowser/wasm-compiler and the CLI's browser entry from their sources
|
|
6
|
+
* (the aliases below). playground/build-assets.ts writes the assets and
|
|
7
|
+
* their manifest to a directory; the build reads the manifest, and the page
|
|
8
|
+
* fetches the files from /playground-assets/, where the dev server serves
|
|
9
|
+
* that directory, or, deployed, from where they were uploaded
|
|
10
|
+
* (playground/publish-assets.mjs).
|
|
11
|
+
*/
|
|
12
|
+
import { createReadStream, existsSync, readFileSync, statSync } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { PLAYGROUND_MANIFEST } from '../src/playground/files.js';
|
|
15
|
+
/** The downloads the assets' manifest names. */
|
|
16
|
+
export function playgroundFiles(options) {
|
|
17
|
+
const manifest = path.join(options.assetsDir, PLAYGROUND_MANIFEST);
|
|
18
|
+
if (!existsSync(manifest))
|
|
19
|
+
throw new Error(`${manifest} is missing: build the playground's assets (playground/build-assets.ts)`);
|
|
20
|
+
return JSON.parse(readFileSync(manifest, 'utf8'));
|
|
21
|
+
}
|
|
22
|
+
export const PLAYGROUND_ASSETS_PATH = '/playground-assets/';
|
|
23
|
+
/** The source aliases of the playground's imports, from the repository root. */
|
|
24
|
+
export function playgroundAliases(repoRoot) {
|
|
25
|
+
return {
|
|
26
|
+
'@swiftbrowser/wasm-compiler': path.join(repoRoot, 'packages/wasm-compiler/src/index.ts'),
|
|
27
|
+
'@swiftbrowser/cli/browser': path.join(repoRoot, 'packages/cli/src/browser.ts'),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function playgroundAssets(options) {
|
|
31
|
+
return {
|
|
32
|
+
name: 'swiftbrowser-playground-assets',
|
|
33
|
+
configureServer(server) {
|
|
34
|
+
const dir = path.resolve(options.assetsDir);
|
|
35
|
+
server.middlewares.use(PLAYGROUND_ASSETS_PATH, (req, res, next) => {
|
|
36
|
+
const file = path.join(dir, decodeURIComponent(new URL(req.url ?? '/', 'http://x').pathname));
|
|
37
|
+
if (!file.startsWith(dir) || !existsSync(file) || statSync(file).isDirectory())
|
|
38
|
+
return next();
|
|
39
|
+
res.setHeader('Content-Type', file.endsWith('.gz') ? 'application/gzip' : 'application/octet-stream');
|
|
40
|
+
res.setHeader('Content-Length', statSync(file).size);
|
|
41
|
+
createReadStream(file).pipe(res);
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=playground.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playground.js","sourceRoot":"","sources":["../../../plugins/playground.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/E,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,mBAAmB,EAAwB,MAAM,4BAA4B,CAAC;AASvF,gDAAgD;AAChD,MAAM,UAAU,eAAe,CAAC,OAA0B;IACxD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;IACnE,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ,yEAAyE,CAAC,CAAC;IACjI,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAoB,CAAC;AACvE,CAAC;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAG,qBAAqB,CAAC;AAE5D,gFAAgF;AAChF,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,OAAO;QACL,6BAA6B,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,qCAAqC,CAAC;QACzF,2BAA2B,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,6BAA6B,CAAC;KAChF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAA0B;IACzD,OAAO;QACL,IAAI,EAAE,gCAAgC;QACtC,eAAe,CAAC,MAAM;YACpB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAC5C,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,sBAAsB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;gBAChE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAkB,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAC9F,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE;oBAAE,OAAO,IAAI,EAAE,CAAC;gBAC9F,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC;gBACtG,GAAG,CAAC,SAAS,CAAC,gBAAgB,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;gBACrD,gBAAgB,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACnC,CAAC,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface PlaygroundFiles {
|
|
2
|
+
/** The compiler (`swift-frontend-<hash>.wasm.gz`). */
|
|
3
|
+
frontend: string;
|
|
4
|
+
/** The linker (`wasm-ld-<hash>.wasm.gz`). */
|
|
5
|
+
linker: string;
|
|
6
|
+
/** The SDK and kit files, ICU data and module cache (`playground-<hash>.pack.gz`). */
|
|
7
|
+
pack: string;
|
|
8
|
+
/** The macro plugins by the module whose macros they expand. */
|
|
9
|
+
plugins: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export declare const PLAYGROUND_MANIFEST = "assets.json";
|
|
12
|
+
//# sourceMappingURL=files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../../../src/playground/files.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,eAAe;IAC9B,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,eAAO,MAAM,mBAAmB,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The playground's downloads by role, as playground/build-assets.ts names
|
|
2
|
+
// them in assets.json: gzip files named by a hash of their contents, so any
|
|
3
|
+
// cache may keep them forever and a new build is new names. The site's
|
|
4
|
+
// build reads the manifest (plugins/playground.ts) and the page fetches
|
|
5
|
+
// these names from the assets' base URL.
|
|
6
|
+
export const PLAYGROUND_MANIFEST = 'assets.json';
|
|
7
|
+
//# sourceMappingURL=files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../../../src/playground/files.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,4EAA4E;AAC5E,uEAAuE;AACvE,wEAAwE;AACxE,yCAAyC;AAazC,MAAM,CAAC,MAAM,mBAAmB,GAAG,aAAa,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swiftbrowser/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SwiftBrowser web renderer and dev server: applies render ops from a Swift/Wasm app to the DOM inside an iPhone frame.",
|
|
6
6
|
"repository": {
|
|
@@ -41,8 +41,9 @@
|
|
|
41
41
|
"build:node": "tsc -p tsconfig.node-build.json",
|
|
42
42
|
"preview": "vite preview",
|
|
43
43
|
"test:e2e": "playwright test",
|
|
44
|
-
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p e2e/tsconfig.json && tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.node-build.json",
|
|
45
|
-
"test:unit": "vitest run"
|
|
44
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p e2e/tsconfig.json && tsc --noEmit -p playground/tsconfig.json && tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.node-build.json",
|
|
45
|
+
"test:unit": "vitest run",
|
|
46
|
+
"playground:assets": "esbuild playground/build-assets.ts --bundle --platform=node --format=esm --packages=external --alias:@swiftbrowser/wasm-compiler=../packages/wasm-compiler/src/index.ts --alias:@swiftbrowser/cli/browser=../packages/cli/src/browser.ts --outfile=node_modules/.playground/build-assets.mjs --log-level=warning && node --stack-size=65500 node_modules/.playground/build-assets.mjs"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
49
|
"@bjorn3/browser_wasi_shim": "^0.4.2",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The playground page (playground/index.html): Swift compiled in the browser
|
|
3
|
+
* by the WebAssembly compiler (docs/in-browser-compiler.md, Milestone 3).
|
|
4
|
+
* Only this repository's site has it: its code imports
|
|
5
|
+
* @swiftbrowser/wasm-compiler and the CLI's browser entry from their sources
|
|
6
|
+
* (the aliases below). playground/build-assets.ts writes the assets and
|
|
7
|
+
* their manifest to a directory; the build reads the manifest, and the page
|
|
8
|
+
* fetches the files from /playground-assets/, where the dev server serves
|
|
9
|
+
* that directory, or, deployed, from where they were uploaded
|
|
10
|
+
* (playground/publish-assets.mjs).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createReadStream, existsSync, readFileSync, statSync } from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import type { Plugin } from 'vite';
|
|
16
|
+
import { PLAYGROUND_MANIFEST, type PlaygroundFiles } from '../src/playground/files.js';
|
|
17
|
+
|
|
18
|
+
export interface PlaygroundOptions {
|
|
19
|
+
/** The directory build-assets wrote: its manifest, and the files the dev server serves at /playground-assets/. */
|
|
20
|
+
assetsDir: string;
|
|
21
|
+
/** Where the page fetches the assets from; default /playground-assets/. */
|
|
22
|
+
assetsUrl?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The downloads the assets' manifest names. */
|
|
26
|
+
export function playgroundFiles(options: PlaygroundOptions): PlaygroundFiles {
|
|
27
|
+
const manifest = path.join(options.assetsDir, PLAYGROUND_MANIFEST);
|
|
28
|
+
if (!existsSync(manifest)) throw new Error(`${manifest} is missing: build the playground's assets (playground/build-assets.ts)`);
|
|
29
|
+
return JSON.parse(readFileSync(manifest, 'utf8')) as PlaygroundFiles;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const PLAYGROUND_ASSETS_PATH = '/playground-assets/';
|
|
33
|
+
|
|
34
|
+
/** The source aliases of the playground's imports, from the repository root. */
|
|
35
|
+
export function playgroundAliases(repoRoot: string): Record<string, string> {
|
|
36
|
+
return {
|
|
37
|
+
'@swiftbrowser/wasm-compiler': path.join(repoRoot, 'packages/wasm-compiler/src/index.ts'),
|
|
38
|
+
'@swiftbrowser/cli/browser': path.join(repoRoot, 'packages/cli/src/browser.ts'),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function playgroundAssets(options: PlaygroundOptions): Plugin {
|
|
43
|
+
return {
|
|
44
|
+
name: 'swiftbrowser-playground-assets',
|
|
45
|
+
configureServer(server) {
|
|
46
|
+
const dir = path.resolve(options.assetsDir);
|
|
47
|
+
server.middlewares.use(PLAYGROUND_ASSETS_PATH, (req, res, next) => {
|
|
48
|
+
const file = path.join(dir, decodeURIComponent(new URL(req.url ?? '/', 'http://x').pathname));
|
|
49
|
+
if (!file.startsWith(dir) || !existsSync(file) || statSync(file).isDirectory()) return next();
|
|
50
|
+
res.setHeader('Content-Type', file.endsWith('.gz') ? 'application/gzip' : 'application/octet-stream');
|
|
51
|
+
res.setHeader('Content-Length', statSync(file).size);
|
|
52
|
+
createReadStream(file).pipe(res);
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -567,7 +567,11 @@ async function boot(): Promise<void> {
|
|
|
567
567
|
return;
|
|
568
568
|
}
|
|
569
569
|
|
|
570
|
-
|
|
570
|
+
// The module the page runs: the app's, or one compiled in this browser
|
|
571
|
+
// (`?module=` a blob: URL, which only this origin can make; the playground
|
|
572
|
+
// page passes its compiler's output this way).
|
|
573
|
+
const compiled = params.get('module');
|
|
574
|
+
const url = compiled?.startsWith('blob:') ? compiled : `${import.meta.env.BASE_URL}${requestedApp}.wasm`;
|
|
571
575
|
appName.textContent = `Loading ${requestedApp}…`;
|
|
572
576
|
device.dataset['state'] = 'loading';
|
|
573
577
|
const env = launchEnvironment();
|
package/src/pinch.ts
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-finger gestures: `MagnifyGesture` and `RotateGesture` (and their iOS 13
|
|
3
|
+
* forms). A `gesture` element whose props set `magnify` or `rotate` gets
|
|
4
|
+
* `data-sb-pinch` (renderer.ts) and these pinch sources:
|
|
5
|
+
*
|
|
6
|
+
* - two touch (or pen) pointers on it: the scale is the distance between them
|
|
7
|
+
* over the distance when the second came down, the rotation the turn of the
|
|
8
|
+
* line between them;
|
|
9
|
+
* - a trackpad pinch, which Chrome, Edge and Firefox report as `wheel` events
|
|
10
|
+
* with `ctrlKey` (a scale only);
|
|
11
|
+
* - Safari's `gesturestart` / `gesturechange` / `gestureend` on a computer
|
|
12
|
+
* (scale and rotation). On a touch screen Safari sends them alongside the
|
|
13
|
+
* pointers, so there they only stop the page zooming.
|
|
14
|
+
*
|
|
15
|
+
* The gesture begins once the scale has moved `minimumScaleDelta` (with
|
|
16
|
+
* `magnify`) or the angle `minimumAngleDelta` degrees (with `rotate`), then
|
|
17
|
+
* sends `pinch` events: `began`, `changed` (one per animation frame) and
|
|
18
|
+
* `ended`, or `cancelled` when the browser takes a pointer away
|
|
19
|
+
* (docs/ops-protocol.md, "Pinch"). The single-pointer gestures of the same
|
|
20
|
+
* element (a drag) carry on beside it, as SwiftUI's simultaneous gestures do.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { DragPhase, ElementId, PinchEvent } from './protocol';
|
|
24
|
+
|
|
25
|
+
export interface PinchHost {
|
|
26
|
+
readonly root: HTMLElement;
|
|
27
|
+
send(event: PinchEvent): void;
|
|
28
|
+
nodeOf(id: ElementId): { props: Record<string, unknown> } | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The element a pinch is on and its thresholds. */
|
|
32
|
+
interface Target {
|
|
33
|
+
el: HTMLElement;
|
|
34
|
+
id: ElementId;
|
|
35
|
+
magnify: boolean;
|
|
36
|
+
rotate: boolean;
|
|
37
|
+
minScale: number;
|
|
38
|
+
minAngle: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A pinch in progress, from whichever source. */
|
|
42
|
+
interface Pinch {
|
|
43
|
+
target: Target;
|
|
44
|
+
began: boolean;
|
|
45
|
+
scale: number;
|
|
46
|
+
/** Degrees, unwrapped (a full turn and more keeps counting). */
|
|
47
|
+
rotation: number;
|
|
48
|
+
startLocation: { x: number; y: number };
|
|
49
|
+
startAnchor: { x: number; y: number };
|
|
50
|
+
samples: { t: number; scale: number; rotation: number }[];
|
|
51
|
+
raf: number | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Two pointers on one element, and the pinch they drive once both are down. */
|
|
55
|
+
interface PointerPinch {
|
|
56
|
+
target: Target;
|
|
57
|
+
points: Map<number, { x: number; y: number }>;
|
|
58
|
+
/** The two pointers' ids, distance and angle when the second came down. */
|
|
59
|
+
base: { a: number; b: number; distance: number; angle: number } | null;
|
|
60
|
+
/** The last raw angle (degrees), for unwrapping. */
|
|
61
|
+
lastAngle: number;
|
|
62
|
+
pinch: Pinch | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const WHEEL_END_MS = 150;
|
|
66
|
+
const VELOCITY_WINDOW_MS = 100;
|
|
67
|
+
/** How much one pixel of a ctrl+wheel pinch scales (Chrome sends small deltas per frame). */
|
|
68
|
+
const WHEEL_SCALE_PER_PIXEL = 0.01;
|
|
69
|
+
|
|
70
|
+
const round = (n: number, places = 4): number => {
|
|
71
|
+
const f = 10 ** places;
|
|
72
|
+
return Math.round(n * f) / f;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
interface SafariGestureEvent extends UIEvent {
|
|
76
|
+
scale: number;
|
|
77
|
+
rotation: number;
|
|
78
|
+
clientX: number;
|
|
79
|
+
clientY: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class PinchController {
|
|
83
|
+
private pointers: PointerPinch | null = null;
|
|
84
|
+
private wheel: { pinch: Pinch; timer: number } | null = null;
|
|
85
|
+
private safari: Pinch | null = null;
|
|
86
|
+
/** Safari on a computer sends gesture events for the trackpad; a touch screen sends them beside the pointers. */
|
|
87
|
+
private readonly safariGestures = typeof navigator !== 'undefined' && navigator.maxTouchPoints === 0;
|
|
88
|
+
|
|
89
|
+
constructor(private readonly host: PinchHost) {
|
|
90
|
+
const root = host.root;
|
|
91
|
+
root.addEventListener('pointerdown', this.onPointerDown, true);
|
|
92
|
+
root.addEventListener('wheel', this.onWheel, { capture: true, passive: false });
|
|
93
|
+
root.addEventListener('gesturestart', this.onGestureStart, true);
|
|
94
|
+
root.addEventListener('gesturechange', this.onGestureChange, true);
|
|
95
|
+
root.addEventListener('gestureend', this.onGestureEnd, true);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
dispose(): void {
|
|
99
|
+
const root = this.host.root;
|
|
100
|
+
root.removeEventListener('pointerdown', this.onPointerDown, true);
|
|
101
|
+
root.removeEventListener('wheel', this.onWheel, true);
|
|
102
|
+
root.removeEventListener('gesturestart', this.onGestureStart, true);
|
|
103
|
+
root.removeEventListener('gesturechange', this.onGestureChange, true);
|
|
104
|
+
root.removeEventListener('gestureend', this.onGestureEnd, true);
|
|
105
|
+
this.stopPointers();
|
|
106
|
+
if (this.wheel) window.clearTimeout(this.wheel.timer);
|
|
107
|
+
this.wheel = null;
|
|
108
|
+
this.safari = null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The innermost pinch gesture element at or above `node`, with its props. */
|
|
112
|
+
private targetAt(node: EventTarget | null): Target | null {
|
|
113
|
+
const start = node instanceof Element ? node : null;
|
|
114
|
+
const el = start?.closest<HTMLElement>('.sb-gesture[data-sb-pinch]') ?? null;
|
|
115
|
+
if (!el || !this.host.root.contains(el)) return null;
|
|
116
|
+
const id = Number(el.dataset['sbId']);
|
|
117
|
+
if (!Number.isInteger(id)) return null;
|
|
118
|
+
const props = this.host.nodeOf(id)?.props ?? {};
|
|
119
|
+
const magnify = props['magnify'] === true;
|
|
120
|
+
const rotate = props['rotate'] === true;
|
|
121
|
+
if (!magnify && !rotate) return null;
|
|
122
|
+
const minScale = typeof props['minimumScaleDelta'] === 'number' ? Math.max(0, props['minimumScaleDelta']) : 0.01;
|
|
123
|
+
const minAngle = typeof props['minimumAngleDelta'] === 'number' ? Math.max(0, props['minimumAngleDelta']) : 1;
|
|
124
|
+
return { el, id, magnify, rotate, minScale, minAngle };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private newPinch(target: Target, clientX: number, clientY: number): Pinch {
|
|
128
|
+
const rect = target.el.getBoundingClientRect();
|
|
129
|
+
const x = clientX - rect.left;
|
|
130
|
+
const y = clientY - rect.top;
|
|
131
|
+
return {
|
|
132
|
+
target,
|
|
133
|
+
began: false,
|
|
134
|
+
scale: 1,
|
|
135
|
+
rotation: 0,
|
|
136
|
+
startLocation: { x: round(x, 2), y: round(y, 2) },
|
|
137
|
+
startAnchor: { x: round(rect.width > 0 ? x / rect.width : 0.5), y: round(rect.height > 0 ? y / rect.height : 0.5) },
|
|
138
|
+
samples: [],
|
|
139
|
+
raf: null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Records a new scale and rotation; begins the gesture past its thresholds, else queues a `changed`. */
|
|
144
|
+
private update(pinch: Pinch, scale: number, rotation: number, time: number): void {
|
|
145
|
+
pinch.scale = scale;
|
|
146
|
+
pinch.rotation = rotation;
|
|
147
|
+
pinch.samples.push({ t: time, scale, rotation });
|
|
148
|
+
while (pinch.samples.length > 2 && time - pinch.samples[0]!.t > VELOCITY_WINDOW_MS) pinch.samples.shift();
|
|
149
|
+
const { target } = pinch;
|
|
150
|
+
if (!pinch.began) {
|
|
151
|
+
const scaled = target.magnify && Math.abs(scale - 1) >= target.minScale;
|
|
152
|
+
const turned = target.rotate && Math.abs(rotation) >= target.minAngle;
|
|
153
|
+
if (!scaled && !turned) return;
|
|
154
|
+
pinch.began = true;
|
|
155
|
+
this.send(pinch, 'began');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (pinch.raf !== null) return;
|
|
159
|
+
pinch.raf = requestAnimationFrame(() => {
|
|
160
|
+
pinch.raf = null;
|
|
161
|
+
this.send(pinch, 'changed');
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Ends a pinch that began (a pinch below its thresholds sends nothing). */
|
|
166
|
+
private finish(pinch: Pinch, phase: 'ended' | 'cancelled'): void {
|
|
167
|
+
if (pinch.raf !== null) cancelAnimationFrame(pinch.raf);
|
|
168
|
+
pinch.raf = null;
|
|
169
|
+
if (pinch.began) this.send(pinch, phase);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private send(pinch: Pinch, phase: DragPhase): void {
|
|
173
|
+
const first = pinch.samples[0];
|
|
174
|
+
const last = pinch.samples[pinch.samples.length - 1];
|
|
175
|
+
const dt = first && last ? (last.t - first.t) / 1000 : 0;
|
|
176
|
+
const scaleVelocity = first && last && dt > 0 ? (last.scale - first.scale) / dt : 0;
|
|
177
|
+
const rotationVelocity = first && last && dt > 0 ? (last.rotation - first.rotation) / dt : 0;
|
|
178
|
+
this.host.send({
|
|
179
|
+
type: 'pinch',
|
|
180
|
+
id: pinch.target.id,
|
|
181
|
+
phase,
|
|
182
|
+
scale: round(pinch.scale),
|
|
183
|
+
rotation: round(pinch.rotation, 2),
|
|
184
|
+
scaleVelocity: round(scaleVelocity),
|
|
185
|
+
rotationVelocity: round(rotationVelocity, 2),
|
|
186
|
+
startLocation: pinch.startLocation,
|
|
187
|
+
startAnchor: pinch.startAnchor,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Two pointers
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
private readonly onPointerDown = (event: PointerEvent): void => {
|
|
196
|
+
if (event.pointerType === 'mouse') return;
|
|
197
|
+
const target = this.targetAt(event.target);
|
|
198
|
+
if (!target) return;
|
|
199
|
+
let state = this.pointers;
|
|
200
|
+
if (state && state.target.el !== target.el) {
|
|
201
|
+
// A finger on another pinch element: only one pinch at a time.
|
|
202
|
+
if (state.points.size > 0) return;
|
|
203
|
+
this.stopPointers();
|
|
204
|
+
state = null;
|
|
205
|
+
}
|
|
206
|
+
if (!state) {
|
|
207
|
+
state = { target, points: new Map(), base: null, lastAngle: 0, pinch: null };
|
|
208
|
+
this.pointers = state;
|
|
209
|
+
window.addEventListener('pointermove', this.onPointerMove, true);
|
|
210
|
+
window.addEventListener('pointerup', this.onPointerUp, true);
|
|
211
|
+
window.addEventListener('pointercancel', this.onPointerCancel, true);
|
|
212
|
+
}
|
|
213
|
+
if (state.points.size >= 2) return;
|
|
214
|
+
state.points.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
215
|
+
if (state.points.size === 2) {
|
|
216
|
+
const [[a, pa], [b, pb]] = [...state.points.entries()] as [[number, { x: number; y: number }], [number, { x: number; y: number }]];
|
|
217
|
+
const distance = Math.hypot(pb.x - pa.x, pb.y - pa.y);
|
|
218
|
+
const angle = (Math.atan2(pb.y - pa.y, pb.x - pa.x) * 180) / Math.PI;
|
|
219
|
+
state.base = { a, b, distance: Math.max(distance, 1), angle };
|
|
220
|
+
state.lastAngle = angle;
|
|
221
|
+
state.pinch = this.newPinch(target, (pa.x + pb.x) / 2, (pa.y + pb.y) / 2);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
private readonly onPointerMove = (event: PointerEvent): void => {
|
|
226
|
+
const state = this.pointers;
|
|
227
|
+
if (!state || !state.points.has(event.pointerId)) return;
|
|
228
|
+
state.points.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
229
|
+
const { base, pinch } = state;
|
|
230
|
+
if (!base || !pinch) return;
|
|
231
|
+
const pa = state.points.get(base.a);
|
|
232
|
+
const pb = state.points.get(base.b);
|
|
233
|
+
if (!pa || !pb) return;
|
|
234
|
+
const angle = (Math.atan2(pb.y - pa.y, pb.x - pa.x) * 180) / Math.PI;
|
|
235
|
+
// Unwrap across ±180° so a turn keeps counting.
|
|
236
|
+
let step = angle - state.lastAngle;
|
|
237
|
+
if (step > 180) step -= 360;
|
|
238
|
+
else if (step < -180) step += 360;
|
|
239
|
+
state.lastAngle = angle;
|
|
240
|
+
const rotation = pinch.rotation + step;
|
|
241
|
+
const scale = Math.hypot(pb.x - pa.x, pb.y - pa.y) / base.distance;
|
|
242
|
+
this.update(pinch, scale, rotation, event.timeStamp);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
private readonly onPointerUp = (event: PointerEvent): void => this.liftPointer(event, 'ended');
|
|
246
|
+
private readonly onPointerCancel = (event: PointerEvent): void => this.liftPointer(event, 'cancelled');
|
|
247
|
+
|
|
248
|
+
/** One of the fingers lifted: the pinch ends; a new second finger starts another. */
|
|
249
|
+
private liftPointer(event: PointerEvent, phase: 'ended' | 'cancelled'): void {
|
|
250
|
+
const state = this.pointers;
|
|
251
|
+
if (!state || !state.points.has(event.pointerId)) return;
|
|
252
|
+
state.points.delete(event.pointerId);
|
|
253
|
+
if (state.pinch) this.finish(state.pinch, phase);
|
|
254
|
+
state.pinch = null;
|
|
255
|
+
state.base = null;
|
|
256
|
+
if (state.points.size === 0) this.stopPointers();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private stopPointers(): void {
|
|
260
|
+
if (this.pointers?.pinch) this.finish(this.pointers.pinch, 'cancelled');
|
|
261
|
+
this.pointers = null;
|
|
262
|
+
window.removeEventListener('pointermove', this.onPointerMove, true);
|
|
263
|
+
window.removeEventListener('pointerup', this.onPointerUp, true);
|
|
264
|
+
window.removeEventListener('pointercancel', this.onPointerCancel, true);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Trackpads
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
/** A trackpad pinch in Chrome, Edge and Firefox: a wheel with ctrl. */
|
|
272
|
+
private readonly onWheel = (event: WheelEvent): void => {
|
|
273
|
+
if (!event.ctrlKey || this.safari) return;
|
|
274
|
+
const current = this.wheel;
|
|
275
|
+
const target = current?.pinch.target ?? this.targetAt(event.target);
|
|
276
|
+
if (!target || !target.magnify) return;
|
|
277
|
+
// The page would zoom.
|
|
278
|
+
event.preventDefault();
|
|
279
|
+
let state = current;
|
|
280
|
+
if (!state) {
|
|
281
|
+
state = { pinch: this.newPinch(target, event.clientX, event.clientY), timer: 0 };
|
|
282
|
+
this.wheel = state;
|
|
283
|
+
} else {
|
|
284
|
+
window.clearTimeout(state.timer);
|
|
285
|
+
}
|
|
286
|
+
const pixels = event.deltaMode === 1 ? event.deltaY * 16 : event.deltaY;
|
|
287
|
+
const scale = state.pinch.scale * Math.exp(-pixels * WHEEL_SCALE_PER_PIXEL);
|
|
288
|
+
this.update(state.pinch, scale, 0, event.timeStamp);
|
|
289
|
+
const ending = state;
|
|
290
|
+
state.timer = window.setTimeout(() => {
|
|
291
|
+
if (this.wheel !== ending) return;
|
|
292
|
+
this.wheel = null;
|
|
293
|
+
this.finish(ending.pinch, 'ended');
|
|
294
|
+
}, WHEEL_END_MS);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/** Safari's own pinch and rotation events. */
|
|
298
|
+
private readonly onGestureStart = (event: Event): void => {
|
|
299
|
+
const target = this.targetAt(event.target);
|
|
300
|
+
if (!target) return;
|
|
301
|
+
// The page would zoom (on a touch screen too, where the pointers drive the pinch).
|
|
302
|
+
event.preventDefault();
|
|
303
|
+
if (!this.safariGestures) return;
|
|
304
|
+
const gesture = event as SafariGestureEvent;
|
|
305
|
+
this.safari = this.newPinch(target, gesture.clientX, gesture.clientY);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
private readonly onGestureChange = (event: Event): void => {
|
|
309
|
+
if (this.targetAt(event.target)) event.preventDefault();
|
|
310
|
+
const pinch = this.safari;
|
|
311
|
+
if (!pinch) return;
|
|
312
|
+
event.preventDefault();
|
|
313
|
+
const gesture = event as SafariGestureEvent;
|
|
314
|
+
this.update(pinch, gesture.scale, gesture.rotation, event.timeStamp);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
private readonly onGestureEnd = (event: Event): void => {
|
|
318
|
+
if (this.targetAt(event.target)) event.preventDefault();
|
|
319
|
+
const pinch = this.safari;
|
|
320
|
+
if (!pinch) return;
|
|
321
|
+
this.safari = null;
|
|
322
|
+
this.finish(pinch, 'ended');
|
|
323
|
+
};
|
|
324
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// The playground's downloads (playground/build-assets.ts makes them): the
|
|
2
|
+
// compiler, the linker, the macro plugins and the pack of SDK and kit files,
|
|
3
|
+
// fetched from one base URL (a public bucket, or the dev server's copy) and
|
|
4
|
+
// kept in the Cache API, so a compile needs no server request after the
|
|
5
|
+
// first visit. Each file is gzipped, unpacked here as it arrives (no server
|
|
6
|
+
// or CDN has to set Content-Encoding), and named by its contents, so a new
|
|
7
|
+
// build is a new download.
|
|
8
|
+
|
|
9
|
+
import { memoryDirectory } from '@swiftbrowser/wasm-compiler';
|
|
10
|
+
import type { Toolchain } from './build';
|
|
11
|
+
import type { PlaygroundFiles } from './files';
|
|
12
|
+
import { readPack } from './pack';
|
|
13
|
+
|
|
14
|
+
/** What the pack's index records besides its files. */
|
|
15
|
+
export interface PlaygroundMeta {
|
|
16
|
+
/** The symbol Foundation's ICU reads its data through (`icudtNN_dat`). */
|
|
17
|
+
icuSymbol: string;
|
|
18
|
+
/** The apps whose compiles the pack was recorded from. */
|
|
19
|
+
apps: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CACHE = 'swiftbrowser-playground';
|
|
23
|
+
|
|
24
|
+
/** A response for `url`, from the cache when it has one. */
|
|
25
|
+
async function cached(url: string): Promise<Response> {
|
|
26
|
+
const cache = typeof caches === 'undefined' ? null : await caches.open(CACHE).catch(() => null);
|
|
27
|
+
const hit = await cache?.match(url);
|
|
28
|
+
if (hit) return hit;
|
|
29
|
+
const response = await fetch(url);
|
|
30
|
+
if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
|
|
31
|
+
await cache?.put(url, response.clone()).catch(() => {});
|
|
32
|
+
return response;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The file at `url`, unpacked when it is gzipped. */
|
|
36
|
+
async function body(url: string): Promise<Response> {
|
|
37
|
+
const response = await cached(url);
|
|
38
|
+
if (!url.endsWith('.gz') || !response.body) return response;
|
|
39
|
+
const type = url.endsWith('.wasm.gz') ? 'application/wasm' : 'application/octet-stream';
|
|
40
|
+
return new Response(response.body.pipeThrough(new DecompressionStream('gzip')), { headers: { 'content-type': type } });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function compileModule(url: string): Promise<WebAssembly.Module> {
|
|
44
|
+
const response = await body(url);
|
|
45
|
+
if (typeof WebAssembly.compileStreaming === 'function' && (response.headers.get('content-type') ?? '').includes('application/wasm')) {
|
|
46
|
+
return WebAssembly.compileStreaming(response);
|
|
47
|
+
}
|
|
48
|
+
return WebAssembly.compile(await response.arrayBuffer());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface LoadedToolchain {
|
|
52
|
+
/** The tools and files, without macro plugins until `plugins` settles. */
|
|
53
|
+
toolchain: Toolchain;
|
|
54
|
+
/**
|
|
55
|
+
* The macro plugins, downloading behind the first compile: most apps
|
|
56
|
+
* expand no macro, and the plugins are a fifth of the download.
|
|
57
|
+
*/
|
|
58
|
+
plugins: Promise<Toolchain['plugins']>;
|
|
59
|
+
/** The module cache built with the playground's flags. */
|
|
60
|
+
moduleCache: [string, Uint8Array][];
|
|
61
|
+
meta: PlaygroundMeta;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function loadToolchain(base: string, files: PlaygroundFiles, onProgress: (text: string) => void = () => {}): Promise<LoadedToolchain> {
|
|
65
|
+
const url = (file: string): string => new URL(file, base).href;
|
|
66
|
+
const started = performance.now();
|
|
67
|
+
const lap = (text: string): void => onProgress(`${text} (${((performance.now() - started) / 1000).toFixed(1)} s)`);
|
|
68
|
+
const [frontend, linker, packBytes] = await Promise.all([
|
|
69
|
+
compileModule(url(files.frontend)).then((module) => (lap('compiler ready'), module)),
|
|
70
|
+
compileModule(url(files.linker)).then((module) => (lap('linker ready'), module)),
|
|
71
|
+
body(url(files.pack)).then((response) => response.arrayBuffer()).then((buffer) => (lap('SDK ready'), new Uint8Array(buffer))),
|
|
72
|
+
]);
|
|
73
|
+
const pack = readPack<PlaygroundMeta>(packBytes);
|
|
74
|
+
const plugins = Promise.all(Object.entries(files.plugins).map(async ([name, file]) => {
|
|
75
|
+
const bytes = new Uint8Array(await (await body(url(file))).arrayBuffer());
|
|
76
|
+
return [name, { module: await WebAssembly.compile(bytes), bytes }] as const;
|
|
77
|
+
})).then(Object.fromEntries<Toolchain['plugins'][string]>);
|
|
78
|
+
// A failed download surfaces when a compile needs the plugins.
|
|
79
|
+
plugins.catch(() => {});
|
|
80
|
+
const kitJson = pack.file('kit.json');
|
|
81
|
+
const icu = pack.file('icu.dat');
|
|
82
|
+
if (!kitJson || !icu) throw new Error('playground.pack has no kit.json or icu.dat');
|
|
83
|
+
return {
|
|
84
|
+
toolchain: {
|
|
85
|
+
tools: { 'swift-frontend': frontend, 'wasm-ld': linker },
|
|
86
|
+
plugins: {},
|
|
87
|
+
sdk: memoryDirectory(pack.files('sdk')),
|
|
88
|
+
kit: memoryDirectory(pack.files('kit')),
|
|
89
|
+
kitManifest: JSON.parse(new TextDecoder().decode(kitJson)) as Toolchain['kitManifest'],
|
|
90
|
+
icu: { symbol: pack.meta.icuSymbol, data: icu },
|
|
91
|
+
},
|
|
92
|
+
plugins,
|
|
93
|
+
moduleCache: pack.files('module-cache'),
|
|
94
|
+
meta: pack.meta,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// An app compiled and linked in the browser: what the CLI does with
|
|
2
|
+
// SWIFTBROWSER_COMPILER=wasm (packages/cli/src/wasmCompiler.ts), on the
|
|
3
|
+
// in-memory host instead of the file system. The app's sources are staged
|
|
4
|
+
// for the shim with the CLI's own code (its browser entry) and compiled with
|
|
5
|
+
// the CLI's flags against the kit, so a module built here is the module the
|
|
6
|
+
// CLI builds. The playground's worker runs it, and so does the Node script
|
|
7
|
+
// that records which SDK and kit files a compile reads
|
|
8
|
+
// (playground/build-assets.ts).
|
|
9
|
+
|
|
10
|
+
import type { Directory } from '@bjorn3/browser_wasi_shim';
|
|
11
|
+
import {
|
|
12
|
+
FULL_FOUNDATION_FILE,
|
|
13
|
+
appFrontendFlags,
|
|
14
|
+
appLinkerFlags,
|
|
15
|
+
declaredTypeNames,
|
|
16
|
+
featuresOf,
|
|
17
|
+
fullFoundationSource,
|
|
18
|
+
kitTargets,
|
|
19
|
+
linkedProducts,
|
|
20
|
+
optionalRequirementNames,
|
|
21
|
+
transformSource,
|
|
22
|
+
} from '@swiftbrowser/cli/browser';
|
|
23
|
+
import { type CompileStep, compile, dataObject, memoryDirectory, memoryHost } from '@swiftbrowser/wasm-compiler';
|
|
24
|
+
|
|
25
|
+
/** Where the kit's modules and archives are, as the compiler sees them. */
|
|
26
|
+
const KIT_ROOT = '/kit/wasm32-unknown-wasip1';
|
|
27
|
+
|
|
28
|
+
/** What every compile shares, loaded once. */
|
|
29
|
+
export interface Toolchain {
|
|
30
|
+
tools: { 'swift-frontend': WebAssembly.Module; 'wasm-ld': WebAssembly.Module };
|
|
31
|
+
/** Macro plugins by the module whose macros they expand, with their bytes (the compiler checks the file). */
|
|
32
|
+
plugins: Record<string, { module: WebAssembly.Module; bytes: Uint8Array }>;
|
|
33
|
+
/** The Swift SDK's `wasm32-unknown-wasip1` directory. */
|
|
34
|
+
sdk: Directory;
|
|
35
|
+
/** The kit's directory (its `wasm32-unknown-wasip1/Modules` and `lib`). */
|
|
36
|
+
kit: Directory;
|
|
37
|
+
/** kit.json's target and product graph. */
|
|
38
|
+
kitManifest: { targets: Record<string, string[]>; products: Record<string, string[]> };
|
|
39
|
+
/** Foundation's ICU data, trimmed, and the symbol it is read through. */
|
|
40
|
+
icu: { symbol: string; data: Uint8Array };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AppSources {
|
|
44
|
+
/** The module name (a Swift identifier). */
|
|
45
|
+
name: string;
|
|
46
|
+
/** Path relative to the app's sources → text. */
|
|
47
|
+
files: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface BuildResult {
|
|
51
|
+
ok: boolean;
|
|
52
|
+
/** The compiler's and linker's diagnostics, with the app's file names. */
|
|
53
|
+
output: string;
|
|
54
|
+
/** The linked WASI command, when `ok`. */
|
|
55
|
+
wasm?: Uint8Array;
|
|
56
|
+
steps: CompileStep[];
|
|
57
|
+
/** Clang's module cache after the compile, for the next one to start from. */
|
|
58
|
+
moduleCache: [string, Uint8Array][];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface BuildOptions {
|
|
62
|
+
moduleCache?: Iterable<readonly [string, Uint8Array]>;
|
|
63
|
+
/** Every path the tools open, as a guest path. */
|
|
64
|
+
onOpen?: (path: string) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The app's sources as the CLI stages them (stage.ts): adjusted for the shim, with the generated Foundation aliases. */
|
|
68
|
+
export function stageSources(app: AppSources): Record<string, string> {
|
|
69
|
+
const texts = Object.values(app.files);
|
|
70
|
+
const optionalRequirements = optionalRequirementNames(texts);
|
|
71
|
+
const staged: Record<string, string> = {};
|
|
72
|
+
for (const [file, text] of Object.entries(app.files)) staged[file] = transformSource(text, { bridged: true, optionalRequirements }).text;
|
|
73
|
+
staged[FULL_FOUNDATION_FILE] = fullFoundationSource(featuresOf(texts), declaredTypeNames(texts));
|
|
74
|
+
return staged;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The compiler's output without the diagnostics in the file the staging
|
|
79
|
+
* generates (the user never sees it), nor the footnotes only they cited.
|
|
80
|
+
*/
|
|
81
|
+
export function appDiagnostics(output: string): string {
|
|
82
|
+
const blocks: string[][] = [[]];
|
|
83
|
+
for (const line of output.split('\n')) {
|
|
84
|
+
if (/^[^\s:][^:]*:\d+:\d+: (?:error|warning|note|remark):/.test(line)) blocks.push([]);
|
|
85
|
+
blocks.at(-1)!.push(line);
|
|
86
|
+
}
|
|
87
|
+
const kept = blocks.filter((block, index) => index === 0 || !block[0]!.startsWith(`${FULL_FOUNDATION_FILE}:`)).flat();
|
|
88
|
+
const text = kept.join('\n');
|
|
89
|
+
return kept
|
|
90
|
+
.filter((line) => {
|
|
91
|
+
const footnote = /^\[(#\w+)\]: </.exec(line);
|
|
92
|
+
return !footnote || text.split(`[${footnote[1]}]`).length > 2;
|
|
93
|
+
})
|
|
94
|
+
.join('\n')
|
|
95
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
96
|
+
.trim();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Whether a compile failed for want of a macro plugin it was not given. */
|
|
100
|
+
export function missingPlugin(output: string): boolean {
|
|
101
|
+
return /external macro implementation type '[^']+' could not be found/.test(output);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function buildApp(toolchain: Toolchain, app: AppSources, options: BuildOptions = {}): Promise<BuildResult> {
|
|
105
|
+
const staged = stageSources(app);
|
|
106
|
+
const encoder = new TextEncoder();
|
|
107
|
+
const sources = Object.keys(staged).sort().map((file) => `/app/${file}`);
|
|
108
|
+
const plugins = Object.entries(toolchain.plugins);
|
|
109
|
+
const host = memoryHost({
|
|
110
|
+
tools: toolchain.tools,
|
|
111
|
+
mounts: {
|
|
112
|
+
'/sdk': toolchain.sdk,
|
|
113
|
+
'/kit': toolchain.kit,
|
|
114
|
+
'/app': memoryDirectory(Object.entries(staged).map(([file, text]) => [file, encoder.encode(text)])),
|
|
115
|
+
'/work': memoryDirectory([['icudt.o', dataObject(toolchain.icu.symbol, toolchain.icu.data)]]),
|
|
116
|
+
'/plugins': memoryDirectory(plugins.map(([name, plugin]) => [`${name}.wasm`, plugin.bytes])),
|
|
117
|
+
},
|
|
118
|
+
sdkRoot: '/sdk',
|
|
119
|
+
plugins: Object.fromEntries(plugins.map(([name, plugin]) => [`/plugins/${name}.wasm`, plugin.module])),
|
|
120
|
+
...(options.moduleCache ? { moduleCache: options.moduleCache } : {}),
|
|
121
|
+
...(options.onOpen ? { onOpen: options.onOpen } : {}),
|
|
122
|
+
});
|
|
123
|
+
const features = featuresOf(Object.values(app.files));
|
|
124
|
+
const archives = kitTargets({ manifest: toolchain.kitManifest }, linkedProducts(features)).map((target) => `${KIT_ROOT}/lib/lib${target}.a`);
|
|
125
|
+
const result = await compile(host, {
|
|
126
|
+
moduleName: app.name,
|
|
127
|
+
sources,
|
|
128
|
+
objectDir: '/work',
|
|
129
|
+
output: `/work/${app.name}.wasm`,
|
|
130
|
+
wholeModule: true,
|
|
131
|
+
frontendFlags: [
|
|
132
|
+
...appFrontendFlags({ name: app.name, swiftLanguageMode: '5', defines: [] }, sources, `${KIT_ROOT}/Modules`),
|
|
133
|
+
...plugins.flatMap(([name]) => ['-load-plugin-executable', `/plugins/${name}.wasm#${name}`]),
|
|
134
|
+
],
|
|
135
|
+
objects: ['/work/icudt.o'],
|
|
136
|
+
linkerFlags: [
|
|
137
|
+
'--gc-sections',
|
|
138
|
+
...appLinkerFlags(),
|
|
139
|
+
...archives.flatMap((archive) => ['--whole-archive', archive, '--no-whole-archive']),
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
ok: result.ok,
|
|
144
|
+
output: appDiagnostics(result.output.split('/app/').join('')),
|
|
145
|
+
...(result.ok ? { wasm: await host.readFile(`/work/${app.name}.wasm`) } : {}),
|
|
146
|
+
steps: result.steps,
|
|
147
|
+
moduleCache: host.moduleCacheFiles(),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The playground's downloads by role, as playground/build-assets.ts names
|
|
2
|
+
// them in assets.json: gzip files named by a hash of their contents, so any
|
|
3
|
+
// cache may keep them forever and a new build is new names. The site's
|
|
4
|
+
// build reads the manifest (plugins/playground.ts) and the page fetches
|
|
5
|
+
// these names from the assets' base URL.
|
|
6
|
+
|
|
7
|
+
export interface PlaygroundFiles {
|
|
8
|
+
/** The compiler (`swift-frontend-<hash>.wasm.gz`). */
|
|
9
|
+
frontend: string;
|
|
10
|
+
/** The linker (`wasm-ld-<hash>.wasm.gz`). */
|
|
11
|
+
linker: string;
|
|
12
|
+
/** The SDK and kit files, ICU data and module cache (`playground-<hash>.pack.gz`). */
|
|
13
|
+
pack: string;
|
|
14
|
+
/** The macro plugins by the module whose macros they expand. */
|
|
15
|
+
plugins: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const PLAYGROUND_MANIFEST = 'assets.json';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/// <reference path="../vite-env.d.ts" />
|
|
2
|
+
// The playground page: an app's sources in an editor, compiled and linked in
|
|
3
|
+
// a worker by the WebAssembly Swift compiler (worker.ts), and run by the
|
|
4
|
+
// renderer's own page in the frame, which takes the module as a blob: URL
|
|
5
|
+
// (`?module=`, main.ts). Counter's sources come with the page, so after the
|
|
6
|
+
// first visit's downloads nothing reaches a server.
|
|
7
|
+
|
|
8
|
+
import type { FromWorker, ToWorker } from './worker';
|
|
9
|
+
|
|
10
|
+
const sources = import.meta.glob('../../../Examples/Counter/Sources/**/*.swift', { query: '?raw', import: 'default', eager: true }) as Record<string, string>;
|
|
11
|
+
const APP = 'Counter';
|
|
12
|
+
|
|
13
|
+
function $<T extends HTMLElement>(selector: string): T {
|
|
14
|
+
const element = document.querySelector<T>(selector);
|
|
15
|
+
if (!element) throw new Error(`missing ${selector}`);
|
|
16
|
+
return element;
|
|
17
|
+
}
|
|
18
|
+
const filesNav = $<HTMLElement>('#files');
|
|
19
|
+
const editor = $<HTMLTextAreaElement>('#source');
|
|
20
|
+
const run = $<HTMLButtonElement>('#run');
|
|
21
|
+
const status = $<HTMLElement>('#status');
|
|
22
|
+
const diagnostics = $<HTMLPreElement>('#diagnostics');
|
|
23
|
+
const frame = $<HTMLIFrameElement>('#app');
|
|
24
|
+
|
|
25
|
+
// Path relative to Sources/ → text, as edited.
|
|
26
|
+
const files: Record<string, string> = Object.fromEntries(
|
|
27
|
+
Object.entries(sources).map(([file, text]) => [file.replace(/^.*\/Sources\//, ''), text]),
|
|
28
|
+
);
|
|
29
|
+
let current = Object.keys(files).sort()[0]!;
|
|
30
|
+
|
|
31
|
+
function showFiles(): void {
|
|
32
|
+
filesNav.replaceChildren(...Object.keys(files).sort().map((file) => {
|
|
33
|
+
const button = document.createElement('button');
|
|
34
|
+
button.type = 'button';
|
|
35
|
+
button.textContent = file;
|
|
36
|
+
button.setAttribute('aria-current', String(file === current));
|
|
37
|
+
button.addEventListener('click', () => {
|
|
38
|
+
files[current] = editor.value;
|
|
39
|
+
current = file;
|
|
40
|
+
editor.value = files[file]!;
|
|
41
|
+
showFiles();
|
|
42
|
+
});
|
|
43
|
+
return button;
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
editor.value = files[current]!;
|
|
47
|
+
editor.addEventListener('input', () => (files[current] = editor.value));
|
|
48
|
+
showFiles();
|
|
49
|
+
|
|
50
|
+
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
|
|
51
|
+
const base = new URL(__SB_PLAYGROUND_ASSETS__, location.href).href;
|
|
52
|
+
let nextId = 1;
|
|
53
|
+
let moduleUrl: string | null = null;
|
|
54
|
+
const started = new Map<number, number>();
|
|
55
|
+
|
|
56
|
+
worker.onmessage = (event: MessageEvent<FromWorker>) => {
|
|
57
|
+
const message = event.data;
|
|
58
|
+
if (message.type === 'progress') {
|
|
59
|
+
status.textContent = message.text;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
run.disabled = false;
|
|
63
|
+
if (message.type === 'failure') {
|
|
64
|
+
status.textContent = 'The compiler failed.';
|
|
65
|
+
diagnostics.hidden = false;
|
|
66
|
+
diagnostics.textContent = message.error;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const seconds = (performance.now() - (started.get(message.id) ?? performance.now())) / 1000;
|
|
70
|
+
const steps = message.steps.map((step) => `${step.tool} ${step.seconds.toFixed(1)} s`).join(', ');
|
|
71
|
+
diagnostics.hidden = message.ok || message.output === '';
|
|
72
|
+
diagnostics.textContent = message.output;
|
|
73
|
+
if (!message.ok || !message.wasm) {
|
|
74
|
+
status.textContent = `Did not compile (${steps}).`;
|
|
75
|
+
diagnostics.hidden = false;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
status.textContent = `Compiled and linked in ${seconds.toFixed(1)} s (${steps}).`;
|
|
79
|
+
if (moduleUrl) URL.revokeObjectURL(moduleUrl);
|
|
80
|
+
moduleUrl = URL.createObjectURL(new Blob([message.wasm], { type: 'application/wasm' }));
|
|
81
|
+
frame.src = `${import.meta.env.BASE_URL}app/?app=${APP}&module=${encodeURIComponent(moduleUrl)}`;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function compile(): void {
|
|
85
|
+
run.disabled = true;
|
|
86
|
+
diagnostics.hidden = true;
|
|
87
|
+
status.textContent = 'Compiling…';
|
|
88
|
+
const id = nextId++;
|
|
89
|
+
started.set(id, performance.now());
|
|
90
|
+
const message: ToWorker = { type: 'compile', id, base, files: __SB_PLAYGROUND_FILES__, app: { name: APP, files: { ...files } } };
|
|
91
|
+
worker.postMessage(message);
|
|
92
|
+
}
|
|
93
|
+
run.addEventListener('click', compile);
|
|
94
|
+
// The first run loads the compiler and the SDK too.
|
|
95
|
+
compile();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// The playground's one download besides the tools: every file a compile
|
|
2
|
+
// reads (the Swift SDK's, the kit's, the ICU data, the module cache), packed
|
|
3
|
+
// as a JSON index followed by the files' bytes. Reading it is a view per
|
|
4
|
+
// file, no copies.
|
|
5
|
+
//
|
|
6
|
+
// [u32 LE index length][index JSON][file bytes...]
|
|
7
|
+
// index: { files: { "<path>": [offset, length] }, ...meta }, offsets from the end of the index
|
|
8
|
+
|
|
9
|
+
export interface PackIndex<Meta = Record<string, unknown>> {
|
|
10
|
+
files: Record<string, [number, number]>;
|
|
11
|
+
meta: Meta;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function writePack<Meta>(files: Iterable<readonly [string, Uint8Array]>, meta: Meta): Uint8Array {
|
|
15
|
+
const entries = [...files];
|
|
16
|
+
const index: PackIndex<Meta> = { files: {}, meta };
|
|
17
|
+
let offset = 0;
|
|
18
|
+
for (const [file, data] of entries) {
|
|
19
|
+
index.files[file] = [offset, data.length];
|
|
20
|
+
offset += data.length;
|
|
21
|
+
}
|
|
22
|
+
const json = new TextEncoder().encode(JSON.stringify(index));
|
|
23
|
+
const out = new Uint8Array(4 + json.length + offset);
|
|
24
|
+
new DataView(out.buffer).setUint32(0, json.length, true);
|
|
25
|
+
out.set(json, 4);
|
|
26
|
+
let at = 4 + json.length;
|
|
27
|
+
for (const [, data] of entries) {
|
|
28
|
+
out.set(data, at);
|
|
29
|
+
at += data.length;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Pack<Meta = Record<string, unknown>> {
|
|
35
|
+
meta: Meta;
|
|
36
|
+
/** The files under `prefix/` (the prefix removed), as views of the pack. */
|
|
37
|
+
files(prefix: string): [string, Uint8Array][];
|
|
38
|
+
file(path: string): Uint8Array | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function readPack<Meta>(bytes: Uint8Array): Pack<Meta> {
|
|
42
|
+
const length = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0, true);
|
|
43
|
+
const index = JSON.parse(new TextDecoder().decode(bytes.subarray(4, 4 + length))) as PackIndex<Meta>;
|
|
44
|
+
const data = bytes.subarray(4 + length);
|
|
45
|
+
const view = ([offset, size]: [number, number]): Uint8Array => data.subarray(offset, offset + size);
|
|
46
|
+
return {
|
|
47
|
+
meta: index.meta,
|
|
48
|
+
files(prefix: string) {
|
|
49
|
+
const start = `${prefix}/`;
|
|
50
|
+
return Object.entries(index.files).filter(([file]) => file.startsWith(start)).map(([file, range]) => [file.slice(start.length), view(range)]);
|
|
51
|
+
},
|
|
52
|
+
file(path: string) {
|
|
53
|
+
const range = index.files[path];
|
|
54
|
+
return range ? view(range) : undefined;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #f5f5f7;
|
|
3
|
+
--panel: #ffffff;
|
|
4
|
+
--text: #1d1d1f;
|
|
5
|
+
--muted: #6e6e73;
|
|
6
|
+
--accent: #0071e3;
|
|
7
|
+
--error: #c62828;
|
|
8
|
+
--border: #d2d2d7;
|
|
9
|
+
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif;
|
|
10
|
+
}
|
|
11
|
+
@media (prefers-color-scheme: dark) {
|
|
12
|
+
:root { --bg: #000; --panel: #1c1c1e; --text: #f5f5f7; --muted: #98989d; --accent: #2997ff; --error: #ff6b6b; --border: #38383a; }
|
|
13
|
+
}
|
|
14
|
+
body { margin: 0; background: var(--bg); color: var(--text); }
|
|
15
|
+
header { padding: 20px 24px 8px; }
|
|
16
|
+
header h1 { margin: 0; font-size: 22px; }
|
|
17
|
+
header p { margin: 4px 0 0; color: var(--muted); font-size: 14px; }
|
|
18
|
+
main { display: grid; grid-template-columns: minmax(0, 1fr) 420px; gap: 16px; padding: 16px 24px 24px; }
|
|
19
|
+
@media (max-width: 900px) { main { grid-template-columns: 1fr; } }
|
|
20
|
+
.editor { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
|
|
21
|
+
#files { display: flex; gap: 4px; flex-wrap: wrap; }
|
|
22
|
+
#files button { border: 1px solid var(--border); background: var(--panel); color: var(--text); border-radius: 6px; padding: 4px 10px; font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; cursor: pointer; }
|
|
23
|
+
#files button[aria-current='true'] { border-color: var(--accent); color: var(--accent); }
|
|
24
|
+
#source { min-height: 480px; resize: vertical; padding: 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--panel); color: var(--text); font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; tab-size: 4; }
|
|
25
|
+
.actions { display: flex; align-items: center; gap: 12px; }
|
|
26
|
+
#run { background: var(--accent); color: white; border: none; border-radius: 8px; padding: 8px 20px; font-size: 15px; font-weight: 600; cursor: pointer; }
|
|
27
|
+
#run:disabled { opacity: 0.5; cursor: default; }
|
|
28
|
+
#status { color: var(--muted); font-size: 13px; }
|
|
29
|
+
#diagnostics { margin: 0; padding: 12px; border: 1px solid var(--error); border-radius: 8px; background: var(--panel); color: var(--error); font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; overflow: auto; max-height: 320px; }
|
|
30
|
+
.preview iframe { width: 100%; height: 860px; border: none; border-radius: 12px; background: var(--panel); }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// The playground's compiler, in a worker: the page sends an app's sources,
|
|
2
|
+
// the worker answers with the linked module or the diagnostics. The tools
|
|
3
|
+
// and files are loaded once and kept, and each compile starts from the
|
|
4
|
+
// module cache the previous one left. The macro plugins arrive after the
|
|
5
|
+
// tools: a compile that expands a macro before then waits for them and
|
|
6
|
+
// runs again.
|
|
7
|
+
|
|
8
|
+
import { type AppSources, missingPlugin, buildApp } from './build';
|
|
9
|
+
import { type LoadedToolchain, loadToolchain } from './assets';
|
|
10
|
+
import type { PlaygroundFiles } from './files';
|
|
11
|
+
|
|
12
|
+
export type ToWorker = { type: 'compile'; id: number; base: string; files: PlaygroundFiles; app: AppSources };
|
|
13
|
+
export type FromWorker =
|
|
14
|
+
| { type: 'progress'; text: string }
|
|
15
|
+
| { type: 'result'; id: number; ok: boolean; output: string; wasm?: Uint8Array<ArrayBuffer>; steps: { tool: string; seconds: number }[] }
|
|
16
|
+
| { type: 'failure'; id: number; error: string };
|
|
17
|
+
|
|
18
|
+
const post = (message: FromWorker, transfer: Transferable[] = []): void => (self as unknown as Worker).postMessage(message, transfer);
|
|
19
|
+
let loaded: Promise<LoadedToolchain> | null = null;
|
|
20
|
+
let moduleCache: [string, Uint8Array][] | null = null;
|
|
21
|
+
|
|
22
|
+
self.onmessage = async (event: MessageEvent<ToWorker>) => {
|
|
23
|
+
const { id, base, files, app } = event.data;
|
|
24
|
+
try {
|
|
25
|
+
loaded ??= loadToolchain(base, files, (text) => post({ type: 'progress', text })).then((toolchain) => {
|
|
26
|
+
// Later compiles use the plugins as soon as they are in.
|
|
27
|
+
toolchain.plugins.then((plugins) => (toolchain.toolchain.plugins = plugins), () => {});
|
|
28
|
+
return toolchain;
|
|
29
|
+
});
|
|
30
|
+
const { toolchain, plugins, moduleCache: prebuilt } = await loaded;
|
|
31
|
+
post({ type: 'progress', text: `compiling ${app.name}…` });
|
|
32
|
+
let result = await buildApp(toolchain, app, { moduleCache: moduleCache ?? prebuilt });
|
|
33
|
+
moduleCache = result.moduleCache;
|
|
34
|
+
if (!result.ok && Object.keys(toolchain.plugins).length === 0 && missingPlugin(result.output)) {
|
|
35
|
+
post({ type: 'progress', text: 'waiting for the macro plugins…' });
|
|
36
|
+
toolchain.plugins = await plugins;
|
|
37
|
+
post({ type: 'progress', text: `compiling ${app.name} with the macro plugins…` });
|
|
38
|
+
result = await buildApp(toolchain, app, { moduleCache });
|
|
39
|
+
moduleCache = result.moduleCache;
|
|
40
|
+
}
|
|
41
|
+
const steps = result.steps.map(({ tool, seconds }) => ({ tool, seconds }));
|
|
42
|
+
if (result.wasm) {
|
|
43
|
+
const wasm = new Uint8Array(result.wasm);
|
|
44
|
+
post({ type: 'result', id, ok: result.ok, output: result.output, wasm, steps }, [wasm.buffer]);
|
|
45
|
+
} else {
|
|
46
|
+
post({ type: 'result', id, ok: result.ok, output: result.output, steps });
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
loaded = null;
|
|
50
|
+
post({ type: 'failure', id, error: error instanceof Error ? (error.stack ?? error.message) : String(error) });
|
|
51
|
+
}
|
|
52
|
+
};
|
package/src/protocol.ts
CHANGED
|
@@ -1765,6 +1765,17 @@ export interface GestureProps {
|
|
|
1765
1765
|
longPress?: boolean;
|
|
1766
1766
|
minimumDistance?: number;
|
|
1767
1767
|
minimumDuration?: number;
|
|
1768
|
+
/**
|
|
1769
|
+
* `MagnifyGesture` / `RotateGesture` (src/pinch.ts): two fingers on the
|
|
1770
|
+
* element, a trackpad pinch (a wheel with ctrl, or Safari's gesture events
|
|
1771
|
+
* on a computer) send `pinch` events once the scale has changed by
|
|
1772
|
+
* `minimumScaleDelta` (default 0.01, with `magnify`) or the angle by
|
|
1773
|
+
* `minimumAngleDelta` degrees (default 1, with `rotate`).
|
|
1774
|
+
*/
|
|
1775
|
+
magnify?: boolean;
|
|
1776
|
+
rotate?: boolean;
|
|
1777
|
+
minimumScaleDelta?: number;
|
|
1778
|
+
minimumAngleDelta?: number;
|
|
1768
1779
|
}
|
|
1769
1780
|
|
|
1770
1781
|
/**
|
|
@@ -1892,6 +1903,26 @@ export interface DragEvent {
|
|
|
1892
1903
|
startLocation: Point;
|
|
1893
1904
|
velocity: { width: number; height: number };
|
|
1894
1905
|
}
|
|
1906
|
+
/**
|
|
1907
|
+
* A `gesture` with `magnify` or `rotate`: two fingers (or a trackpad pinch)
|
|
1908
|
+
* since `began`, coalesced to one per animation frame while `changed`.
|
|
1909
|
+
* `scale` is the distance between the fingers over the first distance;
|
|
1910
|
+
* `rotation` the turn of the line between them, in degrees, clockwise
|
|
1911
|
+
* positive; the velocities are per second. `startLocation` is where the
|
|
1912
|
+
* fingers' midpoint began, in points from the element's top leading corner;
|
|
1913
|
+
* `startAnchor` the same as a fraction of the element's size.
|
|
1914
|
+
*/
|
|
1915
|
+
export interface PinchEvent {
|
|
1916
|
+
type: 'pinch';
|
|
1917
|
+
id: ElementId;
|
|
1918
|
+
phase: DragPhase;
|
|
1919
|
+
scale: number;
|
|
1920
|
+
rotation: number;
|
|
1921
|
+
scaleVelocity: number;
|
|
1922
|
+
rotationVelocity: number;
|
|
1923
|
+
startLocation: Point;
|
|
1924
|
+
startAnchor: Point;
|
|
1925
|
+
}
|
|
1895
1926
|
/** A `gesture` with `longPress` was held `minimumDuration` without moving 10 px. */
|
|
1896
1927
|
export interface LongPressEvent {
|
|
1897
1928
|
type: 'longpress';
|
|
@@ -1952,7 +1983,7 @@ export interface DropEvent {
|
|
|
1952
1983
|
}
|
|
1953
1984
|
|
|
1954
1985
|
/** The Phase 8 events (merged into `RendererEvent` above). */
|
|
1955
|
-
export type InteractionEvent = DragEvent | LongPressEvent | DeleteEvent | RefreshEvent | MoveEvent | DragStartEvent | DragEndEvent | DropEvent;
|
|
1986
|
+
export type InteractionEvent = DragEvent | PinchEvent | LongPressEvent | DeleteEvent | RefreshEvent | MoveEvent | DragStartEvent | DragEndEvent | DropEvent;
|
|
1956
1987
|
// Phase 8: alerts, controls, effects
|
|
1957
1988
|
// (docs/ops-protocol.md, "Phase 8: interaction"; gestures, swipe actions,
|
|
1958
1989
|
// context menus, list rows and refresh live in their own block)
|
package/src/renderer.ts
CHANGED
|
@@ -148,6 +148,7 @@ import {
|
|
|
148
148
|
import { filledSymbolName, renderSymbol } from './symbols';
|
|
149
149
|
import { applyPathShape, hasPathFrames, playPathFrames } from './pathShape';
|
|
150
150
|
import { VideoEvents, applyVideo, stopVideos } from './video';
|
|
151
|
+
import { PinchController } from './pinch';
|
|
151
152
|
import { applyCanvas, createCanvasElement } from './canvasDrawing';
|
|
152
153
|
import { scrollToTarget } from './scrollTo';
|
|
153
154
|
import { EMPTY_CATALOG, assetColorVar, type AssetCatalog } from './assets';
|
|
@@ -386,6 +387,7 @@ export class Renderer {
|
|
|
386
387
|
private readonly pressEffects: PressEffects;
|
|
387
388
|
/** Text, links and files dragged in from outside the page (src/dragDrop.ts). */
|
|
388
389
|
private readonly externalDrops: ExternalDrops;
|
|
390
|
+
private readonly pinches: PinchController;
|
|
389
391
|
/** `video` elements' media events, as `video` events (src/video.ts). */
|
|
390
392
|
private readonly videoEvents: VideoEvents;
|
|
391
393
|
private readonly measurer = new CanvasTextMeasurer();
|
|
@@ -839,6 +841,8 @@ export class Renderer {
|
|
|
839
841
|
// iOS 26: the glass under a pressed bar button swells and springs back (src/press.ts).
|
|
840
842
|
this.pressEffects = new PressEffects(root);
|
|
841
843
|
this.externalDrops = new ExternalDrops(host);
|
|
844
|
+
// Two-finger gestures: MagnifyGesture and RotateGesture (src/pinch.ts).
|
|
845
|
+
this.pinches = new PinchController(host);
|
|
842
846
|
root.addEventListener('change', this.onChange);
|
|
843
847
|
root.addEventListener('pointerdown', this.onSliderPointerDown);
|
|
844
848
|
if (typeof ResizeObserver !== 'undefined') {
|
|
@@ -1506,6 +1510,7 @@ export class Renderer {
|
|
|
1506
1510
|
this.interaction.dispose();
|
|
1507
1511
|
this.pressEffects.dispose();
|
|
1508
1512
|
this.externalDrops.dispose();
|
|
1513
|
+
this.pinches.dispose();
|
|
1509
1514
|
this.videoEvents.dispose();
|
|
1510
1515
|
this.resizeObserver?.disconnect();
|
|
1511
1516
|
this.hostObserver?.disconnect();
|
|
@@ -3114,6 +3119,7 @@ function applyKindProps(el: HTMLElement, kind: ElementKind, props: ElementProps,
|
|
|
3114
3119
|
const gesture = (props as GestureProps | undefined) ?? {};
|
|
3115
3120
|
el.toggleAttribute('data-sb-drag', gesture.drag === true);
|
|
3116
3121
|
el.toggleAttribute('data-sb-long-press', gesture.longPress === true);
|
|
3122
|
+
el.toggleAttribute('data-sb-pinch', gesture.magnify === true || gesture.rotate === true);
|
|
3117
3123
|
break;
|
|
3118
3124
|
}
|
|
3119
3125
|
case 'swipeactions': {
|
package/src/styles.css
CHANGED
|
@@ -2405,6 +2405,18 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2405
2405
|
-webkit-backdrop-filter: none;
|
|
2406
2406
|
}
|
|
2407
2407
|
|
|
2408
|
+
/* For the same reason, while a sheet slides in or out, what scrolls in it
|
|
2409
|
+
does not. A list or scroll view taller than the card is a scroller, which
|
|
2410
|
+
WebKit on iOS composites on its own, and the bar runs over its top: the
|
|
2411
|
+
bar button's text, painted over it, is then composited on its own too,
|
|
2412
|
+
and was missing until a sheet of a long form had slid in (a short one,
|
|
2413
|
+
which does not scroll, was fine). `hidden` keeps the scroll offset, and
|
|
2414
|
+
the scroll bars are not drawn (`scrollbar-width: none`), so nothing moves. */
|
|
2415
|
+
.sb-sheet--in :is(.sb-list, .sb-scrollview),
|
|
2416
|
+
.sb-sheet--out :is(.sb-list, .sb-scrollview) {
|
|
2417
|
+
overflow: hidden;
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2408
2420
|
/* iOS 26 press feedback on Liquid Glass (src/press.ts): while a pointer is
|
|
2409
2421
|
down on a bar button or a glass button, the glass under it (a bar
|
|
2410
2422
|
capsule, the back and More circles, the sidebar toggle, a `.glass`
|
|
@@ -2825,7 +2837,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2825
2837
|
interaction"; src/interaction.ts drives the data attributes).
|
|
2826
2838
|
------------------------------------------------------------------------- */
|
|
2827
2839
|
|
|
2828
|
-
/* `gesture`: a drag gesture owns touches in both axes
|
|
2840
|
+
/* `gesture`: a drag or two-finger gesture owns touches in both axes (and the
|
|
2841
|
+
page does not zoom under a pinch); long-press subtrees
|
|
2829
2842
|
never show the browser's text selection or callout. */
|
|
2830
2843
|
.sb-gesture,
|
|
2831
2844
|
.sb-contextmenu {
|
|
@@ -2835,7 +2848,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2835
2848
|
-webkit-tap-highlight-color: transparent;
|
|
2836
2849
|
}
|
|
2837
2850
|
|
|
2838
|
-
.sb-gesture[data-sb-drag]
|
|
2851
|
+
.sb-gesture[data-sb-drag],
|
|
2852
|
+
.sb-gesture[data-sb-pinch] {
|
|
2839
2853
|
touch-action: none;
|
|
2840
2854
|
}
|
|
2841
2855
|
|
package/src/vite-env.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ interface SBLandingApp {
|
|
|
26
26
|
declare const __SB_APPS__: readonly SBLandingApp[];
|
|
27
27
|
/** The app the app page runs without `?app=` (`SiteOptions.defaultApp`, node/config.ts). */
|
|
28
28
|
declare const __SB_DEFAULT_APP__: string;
|
|
29
|
+
/** Where the playground page fetches its assets (plugins/playground.ts). */
|
|
30
|
+
declare const __SB_PLAYGROUND_ASSETS__: string;
|
|
31
|
+
/** The playground's downloads, from the assets' manifest (src/playground/files.ts). */
|
|
32
|
+
declare const __SB_PLAYGROUND_FILES__: import('./playground/files').PlaygroundFiles;
|
|
29
33
|
/** What a wide screen shows: `auto` (the frame) or `device` (the full window, a built single-app site). See plugins/site-layout.ts. */
|
|
30
34
|
declare const __SB_PAGE_MODE__: 'auto' | 'device';
|
|
31
35
|
|