@swiftbrowser/web 0.6.7 → 0.8.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.
@@ -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;AAEhI,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;CACzB;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,CAuDjE"}
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"}
@@ -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":"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"}
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.6.7",
3
+ "version": "0.8.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
- const url = `${import.meta.env.BASE_URL}${requestedApp}.wasm`;
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
@@ -2837,7 +2837,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2837
2837
  interaction"; src/interaction.ts drives the data attributes).
2838
2838
  ------------------------------------------------------------------------- */
2839
2839
 
2840
- /* `gesture`: a drag gesture owns touches in both axes; long-press subtrees
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
2841
2842
  never show the browser's text selection or callout. */
2842
2843
  .sb-gesture,
2843
2844
  .sb-contextmenu {
@@ -2847,7 +2848,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2847
2848
  -webkit-tap-highlight-color: transparent;
2848
2849
  }
2849
2850
 
2850
- .sb-gesture[data-sb-drag] {
2851
+ .sb-gesture[data-sb-drag],
2852
+ .sb-gesture[data-sb-pinch] {
2851
2853
  touch-action: none;
2852
2854
  }
2853
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