@swiftbrowser/web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +600 -0
  2. package/app/index.html +125 -0
  3. package/index.html +56 -0
  4. package/node/dist/node/apps.d.ts +58 -0
  5. package/node/dist/node/apps.d.ts.map +1 -0
  6. package/node/dist/node/apps.js +60 -0
  7. package/node/dist/node/apps.js.map +1 -0
  8. package/node/dist/node/config.d.ts +42 -0
  9. package/node/dist/node/config.d.ts.map +1 -0
  10. package/node/dist/node/config.js +99 -0
  11. package/node/dist/node/config.js.map +1 -0
  12. package/node/dist/node/examples.d.ts +39 -0
  13. package/node/dist/node/examples.d.ts.map +1 -0
  14. package/node/dist/node/examples.js +116 -0
  15. package/node/dist/node/examples.js.map +1 -0
  16. package/node/dist/node/index.d.ts +47 -0
  17. package/node/dist/node/index.d.ts.map +1 -0
  18. package/node/dist/node/index.js +52 -0
  19. package/node/dist/node/index.js.map +1 -0
  20. package/node/dist/plugins/app-manifests.d.ts +61 -0
  21. package/node/dist/plugins/app-manifests.d.ts.map +1 -0
  22. package/node/dist/plugins/app-manifests.js +168 -0
  23. package/node/dist/plugins/app-manifests.js.map +1 -0
  24. package/node/dist/plugins/asset-catalogs.d.ts +119 -0
  25. package/node/dist/plugins/asset-catalogs.d.ts.map +1 -0
  26. package/node/dist/plugins/asset-catalogs.js +488 -0
  27. package/node/dist/plugins/asset-catalogs.js.map +1 -0
  28. package/node/dist/plugins/bundle-resources.d.ts +49 -0
  29. package/node/dist/plugins/bundle-resources.d.ts.map +1 -0
  30. package/node/dist/plugins/bundle-resources.js +172 -0
  31. package/node/dist/plugins/bundle-resources.js.map +1 -0
  32. package/node/dist/plugins/swift-wasm.d.ts +33 -0
  33. package/node/dist/plugins/swift-wasm.d.ts.map +1 -0
  34. package/node/dist/plugins/swift-wasm.js +241 -0
  35. package/node/dist/plugins/swift-wasm.js.map +1 -0
  36. package/node/dist/src/assets.d.ts +60 -0
  37. package/node/dist/src/assets.d.ts.map +1 -0
  38. package/node/dist/src/assets.js +54 -0
  39. package/node/dist/src/assets.js.map +1 -0
  40. package/node/dist/src/devEvents.d.ts +31 -0
  41. package/node/dist/src/devEvents.d.ts.map +1 -0
  42. package/node/dist/src/devEvents.js +9 -0
  43. package/node/dist/src/devEvents.js.map +1 -0
  44. package/package.json +56 -0
  45. package/plugins/app-manifests.ts +192 -0
  46. package/plugins/asset-catalogs.ts +558 -0
  47. package/plugins/bundle-resources.ts +195 -0
  48. package/plugins/swift-wasm.ts +259 -0
  49. package/public/_headers +23 -0
  50. package/public/apple-touch-icon.png +0 -0
  51. package/public/icon.svg +14 -0
  52. package/public/manifest.webmanifest +14 -0
  53. package/src/animation.ts +521 -0
  54. package/src/assets.ts +86 -0
  55. package/src/chartDom.ts +481 -0
  56. package/src/devEvents.ts +35 -0
  57. package/src/deviceMode.ts +80 -0
  58. package/src/ghost.ts +25 -0
  59. package/src/interaction.ts +1113 -0
  60. package/src/landing.css +261 -0
  61. package/src/landing.ts +141 -0
  62. package/src/layout/chart.ts +477 -0
  63. package/src/layout/controls.ts +116 -0
  64. package/src/layout/engine.ts +3581 -0
  65. package/src/layout/index.ts +91 -0
  66. package/src/layout/measure.ts +86 -0
  67. package/src/layout/text.ts +124 -0
  68. package/src/layout/types.ts +153 -0
  69. package/src/layout/typography.ts +188 -0
  70. package/src/layoutDom.ts +549 -0
  71. package/src/layoutEvents.ts +110 -0
  72. package/src/locale.ts +64 -0
  73. package/src/main.ts +554 -0
  74. package/src/mock.ts +752 -0
  75. package/src/nodeQueries.ts +60 -0
  76. package/src/protocol.ts +1421 -0
  77. package/src/renderer.ts +2882 -0
  78. package/src/styles.css +2834 -0
  79. package/src/styles.ts +373 -0
  80. package/src/swipe.ts +110 -0
  81. package/src/symbols.ts +595 -0
  82. package/src/typography.ts +76 -0
  83. package/src/vite-env.d.ts +26 -0
  84. package/src/wasm.ts +400 -0
@@ -0,0 +1,195 @@
1
+ /**
2
+ * The app's resource files (the Xcode target's Copy Bundle Resources phase,
3
+ * or `resources` in swiftbrowser.json) as one manifest at `/bundle/<App>.json`
4
+ * (`{ files: [{ path, size }] }`) plus the files at `/bundle/<App>/<path>`,
5
+ * dev server and `vite build` alike. The page fetches them before loading the
6
+ * module and mounts them at `/bundle` in the WASI filesystem, where the shim's
7
+ * `Bundle.main` finds them (docs/ops-protocol.md, "Phase 11").
8
+ *
9
+ * A resource entry that is a file is published under its base name, like
10
+ * Xcode copies it; a directory keeps its name and structure (a folder
11
+ * reference). Asset catalogs are not resources here: plugins/asset-catalogs.ts
12
+ * turns them into the catalog manifest.
13
+ */
14
+
15
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
16
+ import path from 'node:path';
17
+ import type { Plugin } from 'vite';
18
+ import type { AppSpec } from '../node/apps.js';
19
+
20
+ export const BUNDLE_DIR = 'bundle';
21
+
22
+ export interface BundleFile {
23
+ /** Path inside the bundle, `/`-separated (`missions.json`, `Data/notes.txt`). */
24
+ path: string;
25
+ size: number;
26
+ }
27
+
28
+ export interface BundleManifest {
29
+ files: BundleFile[];
30
+ }
31
+
32
+ /** The fields of an {@link AppSpec} this plugin reads. */
33
+ export type BundleSpec = Pick<AppSpec, 'name' | 'resources'>;
34
+
35
+ export interface BuiltBundle {
36
+ manifest: BundleManifest;
37
+ /** Published path (`bundle/App/missions.json`) → source file on disk. */
38
+ files: Map<string, string>;
39
+ warnings: string[];
40
+ }
41
+
42
+ const PUBLISHED = /^[A-Za-z0-9_.\-]+$/;
43
+
44
+ /** URL path of an app's bundle manifest. */
45
+ export function bundleManifestPath(app: string): string {
46
+ return `/${BUNDLE_DIR}/${app}.json`;
47
+ }
48
+
49
+ /** URL path of a file inside an app's bundle. */
50
+ export function bundleFilePath(app: string, file: string): string {
51
+ return `/${BUNDLE_DIR}/${app}/${file.split('/').map(encodeURIComponent).join('/')}`;
52
+ }
53
+
54
+ function walk(dir: string, prefix: string, into: (relative: string, file: string) => void): void {
55
+ for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
56
+ if (entry.name.startsWith('.')) continue;
57
+ const full = path.join(dir, entry.name);
58
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
59
+ if (entry.isDirectory()) walk(full, relative, into);
60
+ else if (entry.isFile()) into(relative, full);
61
+ }
62
+ }
63
+
64
+ /** Lists the resources of a spec as published paths and sources. */
65
+ export function buildBundle(app: string, resources: readonly string[] | undefined): BuiltBundle {
66
+ const files = new Map<string, string>();
67
+ const manifest: BundleManifest = { files: [] };
68
+ const warnings: string[] = [];
69
+ const add = (relative: string, source: string): void => {
70
+ if (!relative.split('/').every((part) => PUBLISHED.test(part))) {
71
+ warnings.push(`${relative}: resource names may use letters, digits, ".", "_" and "-" only; skipped`);
72
+ return;
73
+ }
74
+ if (manifest.files.some((f) => f.path === relative)) {
75
+ warnings.push(`${relative}: two resources publish under the same name; the first one wins`);
76
+ return;
77
+ }
78
+ manifest.files.push({ path: relative, size: statSync(source).size });
79
+ files.set(`${BUNDLE_DIR}/${app}/${relative}`, source);
80
+ };
81
+ for (const resource of resources ?? []) {
82
+ if (!existsSync(resource)) {
83
+ warnings.push(`${resource}: not found`);
84
+ continue;
85
+ }
86
+ if (statSync(resource).isDirectory()) walk(resource, path.basename(resource), add);
87
+ else add(path.basename(resource), resource);
88
+ }
89
+ return { manifest, files, warnings };
90
+ }
91
+
92
+ export function bundleManifestJson(manifest: BundleManifest): string {
93
+ return JSON.stringify(manifest, null, 2) + '\n';
94
+ }
95
+
96
+ /** The app named by a manifest request URL (`/bundle/<App>.json`), or null. */
97
+ export function bundleRequestApp(url: string): string | null {
98
+ const match = new RegExp(`^/${BUNDLE_DIR}/([A-Za-z0-9_-]+)\\.json(?:\\?|$)`).exec(url);
99
+ return match?.[1] ?? null;
100
+ }
101
+
102
+ /** The app and published path of a resource request (`/bundle/<App>/<path>`), or null. */
103
+ export function bundleRequestFile(url: string): { app: string; fileName: string } | null {
104
+ const match = new RegExp(`^/${BUNDLE_DIR}/([A-Za-z0-9_-]+)/([^?]+)(?:\\?|$)`).exec(url);
105
+ if (!match?.[1] || !match[2]) return null;
106
+ let relative: string;
107
+ try {
108
+ relative = match[2].split('/').map(decodeURIComponent).join('/');
109
+ } catch {
110
+ return null;
111
+ }
112
+ if (!relative.split('/').every((part) => PUBLISHED.test(part) && part !== '.' && part !== '..')) return null;
113
+ return { app: match[1], fileName: `${BUNDLE_DIR}/${match[1]}/${relative}` };
114
+ }
115
+
116
+ const CONTENT_TYPES: Record<string, string> = {
117
+ '.json': 'application/json',
118
+ '.txt': 'text/plain; charset=utf-8',
119
+ '.md': 'text/markdown; charset=utf-8',
120
+ '.csv': 'text/csv; charset=utf-8',
121
+ '.xml': 'application/xml',
122
+ '.plist': 'application/xml',
123
+ '.strings': 'text/plain; charset=utf-8',
124
+ '.png': 'image/png',
125
+ '.jpg': 'image/jpeg',
126
+ '.jpeg': 'image/jpeg',
127
+ '.gif': 'image/gif',
128
+ '.svg': 'image/svg+xml',
129
+ '.pdf': 'application/pdf',
130
+ '.mp3': 'audio/mpeg',
131
+ '.m4a': 'audio/mp4',
132
+ '.wav': 'audio/wav',
133
+ '.mp4': 'video/mp4',
134
+ };
135
+
136
+ export function bundleContentType(file: string): string {
137
+ return CONTENT_TYPES[path.extname(file).toLowerCase()] ?? 'application/octet-stream';
138
+ }
139
+
140
+ export function bundleResources(specs: readonly BundleSpec[]): Plugin {
141
+ const byName = new Map(specs.map((spec) => [spec.name, spec]));
142
+ return {
143
+ name: 'swiftbrowser:bundle-resources',
144
+ configureServer(server) {
145
+ const warned = new Set<string>();
146
+ const build = (spec: BundleSpec): BuiltBundle => {
147
+ const built = buildBundle(spec.name, spec.resources);
148
+ for (const warning of built.warnings) {
149
+ if (warned.has(warning)) continue;
150
+ warned.add(warning);
151
+ server.config.logger.warn(`[bundle-resources] ${spec.name}: ${warning}`, { timestamp: true });
152
+ }
153
+ return built;
154
+ };
155
+ server.middlewares.use((req, res, next) => {
156
+ const url = req.url ?? '';
157
+ const app = bundleRequestApp(url);
158
+ if (app !== null) {
159
+ const spec = byName.get(app);
160
+ if (!spec) {
161
+ res.statusCode = 404;
162
+ res.end();
163
+ return;
164
+ }
165
+ res.setHeader('Content-Type', 'application/json');
166
+ res.setHeader('Cache-Control', 'no-cache');
167
+ res.end(bundleManifestJson(build(spec).manifest));
168
+ return;
169
+ }
170
+ const file = bundleRequestFile(url);
171
+ if (file === null) return next();
172
+ const spec = byName.get(file.app);
173
+ const source = spec ? build(spec).files.get(file.fileName) : undefined;
174
+ if (!source) {
175
+ res.statusCode = 404;
176
+ res.end();
177
+ return;
178
+ }
179
+ res.setHeader('Content-Type', bundleContentType(source));
180
+ res.setHeader('Cache-Control', 'no-cache');
181
+ res.end(readFileSync(source));
182
+ });
183
+ },
184
+ generateBundle() {
185
+ for (const spec of specs) {
186
+ const built = buildBundle(spec.name, spec.resources);
187
+ for (const warning of built.warnings) this.warn(`${spec.name}: ${warning}`);
188
+ this.emitFile({ type: 'asset', fileName: `${BUNDLE_DIR}/${spec.name}.json`, source: bundleManifestJson(built.manifest) });
189
+ for (const [fileName, source] of built.files) {
190
+ this.emitFile({ type: 'asset', fileName, source: readFileSync(source) });
191
+ }
192
+ }
193
+ },
194
+ };
195
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Vite plugin for the registered apps' Wasm modules.
3
+ *
4
+ * Dev server:
5
+ * - Serves `/<name>.wasm` from each spec's `wasm` file (`application/wasm`,
6
+ * never cached). When the file is missing, or its sources changed since the
7
+ * server started, and the spec has a `build`, the request builds it first.
8
+ * - Watches every spec's `watch` directories through the dev server's own
9
+ * chokidar watcher, debounces changes (300ms) and runs `spec.build()` for
10
+ * every app whose sources changed among those the page has requested since
11
+ * the server started (`/<name>.wasm` requests register an app). Builds run
12
+ * one at a time.
13
+ * - Sends `sb:build-start` / `sb:build-end` websocket events so the page can
14
+ * show a "Rebuilding…" indicator, then a `full-reload` on success or an
15
+ * `sb:build-error` with the compiler output on failure. The page stashes the
16
+ * app's `@State` snapshot before the reload and feeds it back through the
17
+ * `SB_STATE` WASI environment variable (see src/main.ts).
18
+ *
19
+ * `vite build`: copies each spec's `wasm` into the output directory as
20
+ * `<name>.wasm` (building it first when it is missing and the spec can), so
21
+ * nothing has to ride along in `public/`. Files that are there anyway are
22
+ * still copied by Vite as before.
23
+ */
24
+
25
+ import { copyFileSync, createReadStream, existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
26
+ import path from 'node:path';
27
+ import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite';
28
+ import type { AppSpec } from '../node/apps.js';
29
+ import type { BuildEndPayload, BuildErrorPayload, BuildStartPayload } from '../src/devEvents.js';
30
+
31
+ export interface SwiftWasmOptions {
32
+ /** Debounce window for bursts of file changes, in ms. */
33
+ debounceMs?: number;
34
+ }
35
+
36
+ /** The app named by a module request URL (`/<name>.wasm`), or null. */
37
+ export function wasmRequestApp(url: string): string | null {
38
+ const match = /^\/([A-Za-z0-9_-]+)\.wasm(?:\?|$)/.exec(url);
39
+ return match?.[1] ?? null;
40
+ }
41
+
42
+ interface BatchResult {
43
+ ok: boolean;
44
+ /** The failed app and its compiler output, when `ok` is false. */
45
+ failure?: { app: string; output: string };
46
+ }
47
+
48
+ /** The message of a rejected `build()`: the compiler output. */
49
+ function failureOutput(error: unknown): string {
50
+ if (error instanceof Error) return error.message;
51
+ return String(error);
52
+ }
53
+
54
+ export function swiftWasm(specs: readonly AppSpec[], options: SwiftWasmOptions = {}): Plugin {
55
+ const debounceMs = options.debounceMs ?? 300;
56
+ const byName = new Map(specs.map((spec) => [spec.name, spec]));
57
+
58
+ let config: ResolvedConfig | undefined;
59
+ let server: ViteDevServer | undefined;
60
+
61
+ const log = (message: string): void => {
62
+ config?.logger.info(`[swift-wasm] ${message}`, { timestamp: true });
63
+ };
64
+
65
+ // -------------------------------------------------------------------------
66
+ // Dev server: build queue
67
+ // -------------------------------------------------------------------------
68
+
69
+ /** Apps requested since the server started; their sources trigger rebuilds. */
70
+ const tracked = new Set<string>();
71
+ /** Tracked apps whose sources changed since their last build. */
72
+ const dirty = new Set<string>();
73
+ /** Untracked apps whose sources changed: rebuilt before the first request serves them. */
74
+ const stale = new Set<string>();
75
+ /** The build in flight or queued for an app, so a request can wait for it instead of starting another. */
76
+ const queued = new Map<string, Promise<BatchResult>>();
77
+ /** Builds run one at a time (SwiftPM shares one `.build` directory). */
78
+ let queue: Promise<unknown> = Promise.resolve();
79
+ let timer: ReturnType<typeof setTimeout> | undefined;
80
+
81
+ const runBatch = async (targets: string[]): Promise<BatchResult> => {
82
+ const started = Date.now();
83
+ log(`building ${targets.join(', ')}…`);
84
+ server?.ws.send('sb:build-start', { apps: targets } satisfies BuildStartPayload);
85
+ const result: BatchResult = { ok: true };
86
+ for (const app of targets) {
87
+ const spec = byName.get(app);
88
+ if (!spec?.build) continue;
89
+ try {
90
+ await spec.build();
91
+ } catch (error) {
92
+ const output = failureOutput(error);
93
+ result.ok = false;
94
+ result.failure = { app, output };
95
+ log(`build failed for ${app}`);
96
+ for (const line of output.split('\n')) if (line.trim()) log(` ${line}`);
97
+ server?.ws.send('sb:build-error', { app, output } satisfies BuildErrorPayload);
98
+ break;
99
+ }
100
+ }
101
+ const durationMs = Date.now() - started;
102
+ server?.ws.send('sb:build-end', { apps: targets, ok: result.ok, durationMs } satisfies BuildEndPayload);
103
+ if (result.ok) {
104
+ log(`built ${targets.join(', ')} in ${(durationMs / 1000).toFixed(1)}s; reloading`);
105
+ server?.ws.send({ type: 'full-reload', path: '*' });
106
+ }
107
+ return result;
108
+ };
109
+
110
+ const enqueue = (targets: string[]): Promise<BatchResult> => {
111
+ const batch = queue.then(() => runBatch(targets));
112
+ queue = batch.catch(() => undefined);
113
+ for (const app of targets) {
114
+ queued.set(app, batch);
115
+ void batch.finally(() => {
116
+ if (queued.get(app) === batch) queued.delete(app);
117
+ });
118
+ }
119
+ return batch;
120
+ };
121
+
122
+ /** Waits for the build of `app` if one is queued, else queues one. */
123
+ const ensureBuilt = (app: string): Promise<BatchResult> => queued.get(app) ?? enqueue([app]);
124
+
125
+ const flushDirty = (): void => {
126
+ const targets = Array.from(dirty).filter((app) => !queued.has(app));
127
+ dirty.clear();
128
+ if (targets.length === 0) return;
129
+ void enqueue(targets);
130
+ };
131
+
132
+ const schedule = (): void => {
133
+ if (timer) clearTimeout(timer);
134
+ timer = setTimeout(() => {
135
+ timer = undefined;
136
+ flushDirty();
137
+ }, debounceMs);
138
+ };
139
+
140
+ // -------------------------------------------------------------------------
141
+ // Dev server: serving `/<name>.wasm`
142
+ // -------------------------------------------------------------------------
143
+
144
+ const serveWasm = async (
145
+ spec: AppSpec,
146
+ res: import('node:http').ServerResponse,
147
+ method: string | undefined,
148
+ ): Promise<void> => {
149
+ if (spec.build && (!existsSync(spec.wasm) || stale.has(spec.name))) {
150
+ if (!existsSync(spec.wasm)) log(`${spec.name}: ${path.basename(spec.wasm)} is missing; building it first`);
151
+ stale.delete(spec.name);
152
+ const result = await ensureBuilt(spec.name);
153
+ if (!result.ok && !existsSync(spec.wasm)) {
154
+ res.statusCode = 500;
155
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
156
+ res.end(`Build failed for ${spec.name}:\n${result.failure?.output ?? ''}`);
157
+ return;
158
+ }
159
+ }
160
+ if (!existsSync(spec.wasm)) {
161
+ res.statusCode = 404;
162
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
163
+ res.end(`${spec.name}: ${spec.wasm} does not exist`);
164
+ return;
165
+ }
166
+ res.statusCode = 200;
167
+ res.setHeader('Content-Type', 'application/wasm');
168
+ res.setHeader('Cache-Control', 'no-cache');
169
+ res.setHeader('Content-Length', String(statSync(spec.wasm).size));
170
+ if (method === 'HEAD') {
171
+ res.end();
172
+ return;
173
+ }
174
+ createReadStream(spec.wasm).pipe(res);
175
+ };
176
+
177
+ // -------------------------------------------------------------------------
178
+ // `vite build`: copy the modules into the bundle
179
+ // -------------------------------------------------------------------------
180
+
181
+ const copyModules = async (outDir: string): Promise<void> => {
182
+ for (const spec of specs) {
183
+ if (!existsSync(spec.wasm) && spec.build) {
184
+ log(`${spec.name}: ${spec.wasm} is missing; building it`);
185
+ await spec.build();
186
+ }
187
+ if (!existsSync(spec.wasm)) {
188
+ if (spec.build) throw new Error(`[swift-wasm] ${spec.name}: ${spec.wasm} still does not exist after building it`);
189
+ config?.logger.warn(`[swift-wasm] ${spec.name}: ${spec.wasm} does not exist; the site has no module for it`);
190
+ continue;
191
+ }
192
+ mkdirSync(outDir, { recursive: true });
193
+ const dest = path.join(outDir, `${spec.name}.wasm`);
194
+ if (path.resolve(spec.wasm) !== path.resolve(dest)) copyFileSync(spec.wasm, dest);
195
+ }
196
+ // Modules of apps outside the registry (the examples' `public/*.wasm`,
197
+ // copied with the public dir) are not part of this site.
198
+ const known = new Set(specs.map((spec) => `${spec.name}.wasm`));
199
+ for (const entry of readdirSync(outDir)) {
200
+ if (entry.endsWith('.wasm') && !known.has(entry)) rmSync(path.join(outDir, entry), { force: true });
201
+ }
202
+ };
203
+
204
+ return {
205
+ name: 'swiftbrowser:swift-wasm',
206
+ configResolved(resolved) {
207
+ config = resolved;
208
+ },
209
+ configureServer(devServer) {
210
+ server = devServer;
211
+ const watched = specs.flatMap((spec) =>
212
+ (spec.watch ?? []).map((dir) => ({ app: spec.name, root: path.resolve(dir) })),
213
+ );
214
+ const appsOf = (file: string): string[] =>
215
+ file.endsWith('.swift')
216
+ ? watched.filter(({ root }) => file.startsWith(root + path.sep)).map(({ app }) => app)
217
+ : [];
218
+
219
+ devServer.middlewares.use((req, res, next) => {
220
+ const name = wasmRequestApp(req.url ?? '');
221
+ const spec = name === null ? undefined : byName.get(name);
222
+ // Unknown names fall through to Vite (a stray file in `public/`, or a 404).
223
+ if (!spec) return next();
224
+ if (!tracked.has(spec.name)) {
225
+ tracked.add(spec.name);
226
+ log(`tracking ${spec.name} for rebuilds`);
227
+ }
228
+ serveWasm(spec, res, req.method).catch((error: unknown) => {
229
+ config?.logger.error(`[swift-wasm] ${spec.name}: ${failureOutput(error)}`);
230
+ if (!res.headersSent) res.statusCode = 500;
231
+ res.end();
232
+ });
233
+ });
234
+
235
+ const roots = Array.from(new Set(watched.map(({ root }) => root))).filter((root) => existsSync(root));
236
+ if (roots.length > 0) devServer.watcher.add(roots);
237
+ devServer.watcher.on('all', (event, file) => {
238
+ if (event === 'addDir' || event === 'unlinkDir') return;
239
+ const apps = appsOf(file);
240
+ if (apps.length === 0) return;
241
+ log(`${event} ${file}`);
242
+ for (const app of new Set(apps)) {
243
+ if (!byName.get(app)?.build) continue;
244
+ if (tracked.has(app)) dirty.add(app);
245
+ else stale.add(app);
246
+ }
247
+ if (dirty.size > 0) schedule();
248
+ });
249
+ if (roots.length > 0) {
250
+ log(`watching ${roots.join(', ')} for .swift changes (apps: ${specs.map((spec) => spec.name).join(', ')})`);
251
+ }
252
+ },
253
+ async writeBundle(outputOptions) {
254
+ if (!config) return;
255
+ const outDir = outputOptions.dir ?? path.resolve(config.root, config.build.outDir);
256
+ await copyModules(outDir);
257
+ },
258
+ };
259
+ }
@@ -0,0 +1,23 @@
1
+ # Cloudflare Pages headers (https://developers.cloudflare.com/pages/configuration/headers/)
2
+
3
+ # The Wasm modules are not content-hashed: always revalidate them.
4
+ /*.wasm
5
+ Cache-Control: no-cache
6
+ Content-Type: application/wasm
7
+
8
+ # Vite's hashed bundles can be cached forever.
9
+ /assets/*
10
+ Cache-Control: public, max-age=31536000, immutable
11
+
12
+ # Web app manifests (one per example, see plugins/app-manifests.ts).
13
+ /manifest.webmanifest
14
+ Content-Type: application/manifest+json
15
+ Cache-Control: no-cache
16
+ /manifests/*
17
+ Content-Type: application/manifest+json
18
+ Cache-Control: no-cache
19
+
20
+ # Asset catalog manifests and files (one directory per example, see
21
+ # plugins/asset-catalogs.ts). Not content-hashed: always revalidate.
22
+ /catalog/*
23
+ Cache-Control: no-cache
Binary file
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" width="180" height="180">
2
+ <!-- SwiftBrowser: a phone outline on the iOS accent blue. The same picture is
3
+ drawn as apple-touch-icon.png (iOS masks the corners itself). -->
4
+ <defs>
5
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
6
+ <stop offset="0" stop-color="#2f9bff"/>
7
+ <stop offset="1" stop-color="#0060df"/>
8
+ </linearGradient>
9
+ </defs>
10
+ <rect width="180" height="180" fill="url(#bg)"/>
11
+ <rect x="55" y="30" width="70" height="120" rx="14" ry="14" fill="none" stroke="#ffffff" stroke-width="8"/>
12
+ <rect x="76" y="42" width="28" height="6" rx="3" ry="3" fill="#ffffff"/>
13
+ <circle cx="90" cy="92" r="16" fill="#ffffff"/>
14
+ </svg>
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "SwiftBrowser",
3
+ "short_name": "SwiftBrowser",
4
+ "description": "SwiftUI example apps compiled to WebAssembly, rendered in the browser.",
5
+ "start_url": "/app/",
6
+ "scope": "/",
7
+ "display": "standalone",
8
+ "orientation": "portrait",
9
+ "background_color": "#000000",
10
+ "icons": [
11
+ { "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml" },
12
+ { "src": "/apple-touch-icon.png", "sizes": "180x180", "type": "image/png" }
13
+ ]
14
+ }