@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,9 @@
1
+ /**
2
+ * Custom Vite websocket events exchanged between the dev-server plugin
3
+ * (plugins/swift-wasm.ts) and the page (src/main.ts) for hot reload.
4
+ *
5
+ * This file is imported by both sides, so it must stay free of DOM and Node
6
+ * types.
7
+ */
8
+ import 'vite/types/customEvent.d.ts';
9
+ //# sourceMappingURL=devEvents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devEvents.js","sourceRoot":"","sources":["../../../src/devEvents.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,6BAA6B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@swiftbrowser/web",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "SwiftBrowser web renderer and dev server: applies render ops from a Swift/Wasm app to the DOM inside an iPhone frame.",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/dabbott/SwiftBrowser.git",
9
+ "directory": "Web"
10
+ },
11
+ "main": "./node/dist/node/index.js",
12
+ "types": "./node/dist/node/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./node/dist/node/index.d.ts",
16
+ "default": "./node/dist/node/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "node/dist",
21
+ "!node/dist/**/__tests__",
22
+ "plugins",
23
+ "!plugins/__tests__",
24
+ "src",
25
+ "!src/**/__tests__",
26
+ "app",
27
+ "index.html",
28
+ "public/_headers",
29
+ "public/apple-touch-icon.png",
30
+ "public/icon.svg",
31
+ "public/manifest.webmanifest",
32
+ "README.md"
33
+ ],
34
+ "scripts": {
35
+ "dev": "vite",
36
+ "build": "tsc --noEmit -p tsconfig.json && vite build && npm run build:node",
37
+ "build:node": "tsc -p tsconfig.node-build.json",
38
+ "preview": "vite preview",
39
+ "test:e2e": "playwright test",
40
+ "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",
41
+ "test:unit": "vitest run"
42
+ },
43
+ "dependencies": {
44
+ "@bjorn3/browser_wasi_shim": "^0.4.2",
45
+ "lucide": "^1.50.0",
46
+ "qrcode": "^1.5.4",
47
+ "vite": "^7.1.7"
48
+ },
49
+ "devDependencies": {
50
+ "@playwright/test": "^1.56.0",
51
+ "@types/node": "^26.6.4",
52
+ "@types/qrcode": "^1.5.6",
53
+ "typescript": "^5.9.2",
54
+ "vitest": "^5.0.3"
55
+ }
56
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * One web app manifest per registered app, served at
3
+ * `/manifests/<name>.webmanifest` (dev server and `vite build`), plus the
4
+ * app's own icon at `/manifests/<name>-icon.png` when its spec has one.
5
+ *
6
+ * "Add to Home Screen" in Safari takes the start URL from the page's manifest,
7
+ * not from the page's own URL. With the single `/manifest.webmanifest`
8
+ * (`start_url: /app/`) every home-screen icon therefore launched the default
9
+ * app, whichever one was open when it was added. `app/index.html` points
10
+ * `<link rel="manifest">` at the manifest of the app named by `?app=`, so each
11
+ * icon reopens its own app and is labelled with the app's display name.
12
+ *
13
+ * Safari takes the home-screen image from `<link rel="apple-touch-icon">`
14
+ * rather than from the manifest, so for apps with an icon the plugin also
15
+ * injects `window.__SB_APP_ICONS__` (name -> icon URL) into the app page; the
16
+ * inline script in app/index.html repoints that link.
17
+ */
18
+
19
+ import { existsSync, readFileSync } from 'node:fs';
20
+ import type { Plugin } from 'vite';
21
+ import { APP_NAME, displayNameOf, type AppSpec } from '../node/apps.js';
22
+ import { readImageSize } from './asset-catalogs.js';
23
+
24
+ export const MANIFEST_DIR = 'manifests';
25
+
26
+ /** The fields of an {@link AppSpec} a manifest is made from. */
27
+ export type ManifestSpec = Pick<AppSpec, 'name' | 'displayName' | 'description' | 'icon'>;
28
+
29
+ export interface AppManifest {
30
+ id: string;
31
+ name: string;
32
+ short_name: string;
33
+ description: string;
34
+ start_url: string;
35
+ scope: string;
36
+ display: 'standalone';
37
+ orientation: 'portrait';
38
+ background_color: string;
39
+ icons: { src: string; sizes: string; type: string }[];
40
+ }
41
+
42
+ /** URL path of an app's manifest, as the app page links it. */
43
+ export function manifestPath(app: string): string {
44
+ return `/${MANIFEST_DIR}/${app}.webmanifest`;
45
+ }
46
+
47
+ /** URL path of an app's own icon (only served for specs with `icon`). */
48
+ export function manifestIconPath(app: string): string {
49
+ return `/${MANIFEST_DIR}/${app}-icon.png`;
50
+ }
51
+
52
+ /** The app page URL that runs `app` (matches `appPath` in src/landing.ts). */
53
+ export function appStartUrl(app: string): string {
54
+ return `/app/?app=${encodeURIComponent(app)}`;
55
+ }
56
+
57
+ /** Default description, for a spec without one. */
58
+ export function defaultDescription(spec: ManifestSpec): string {
59
+ return `The ${displayNameOf(spec)} app, compiled to WebAssembly and rendered in the browser.`;
60
+ }
61
+
62
+ /** `WxH` of a PNG on disk, or `any` when it cannot be read. */
63
+ function iconSizes(file: string): string {
64
+ try {
65
+ const size = readImageSize(readFileSync(file), 'png');
66
+ return size ? `${size.width}x${size.height}` : 'any';
67
+ } catch {
68
+ return 'any';
69
+ }
70
+ }
71
+
72
+ /**
73
+ * The manifest for one app. No `theme_color`: the status bar is translucent
74
+ * and the page draws its own background under it, and the light/dark
75
+ * `theme-color` metas in app/index.html cover the browser UI.
76
+ */
77
+ export function appManifest(spec: ManifestSpec | string): AppManifest {
78
+ const app: ManifestSpec = typeof spec === 'string' ? { name: spec } : spec;
79
+ if (!APP_NAME.test(app.name)) throw new Error(`Invalid app name: ${JSON.stringify(app.name)}`);
80
+ const start = appStartUrl(app.name);
81
+ const displayName = displayNameOf(app);
82
+ return {
83
+ id: start,
84
+ name: displayName,
85
+ short_name: displayName,
86
+ description: app.description ?? defaultDescription(app),
87
+ start_url: start,
88
+ scope: '/app/',
89
+ display: 'standalone',
90
+ orientation: 'portrait',
91
+ background_color: '#000000',
92
+ icons: app.icon
93
+ ? [{ src: manifestIconPath(app.name), sizes: iconSizes(app.icon), type: 'image/png' }]
94
+ : [
95
+ { src: '/icon.svg', sizes: 'any', type: 'image/svg+xml' },
96
+ { src: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' },
97
+ ],
98
+ };
99
+ }
100
+
101
+ export function appManifestJson(spec: ManifestSpec | string): string {
102
+ return JSON.stringify(appManifest(spec), null, 2) + '\n';
103
+ }
104
+
105
+ /** The app named by a manifest request URL, or null for any other URL. */
106
+ export function manifestRequestApp(url: string): string | null {
107
+ const match = new RegExp(`^/${MANIFEST_DIR}/([A-Za-z0-9_-]+)\\.webmanifest(?:\\?|$)`).exec(url);
108
+ return match?.[1] ?? null;
109
+ }
110
+
111
+ /** The app named by an icon request URL (`/manifests/<name>-icon.png`), or null. */
112
+ export function iconRequestApp(url: string): string | null {
113
+ const match = new RegExp(`^/${MANIFEST_DIR}/([A-Za-z0-9_-]+)-icon\\.png(?:\\?|$)`).exec(url);
114
+ return match?.[1] ?? null;
115
+ }
116
+
117
+ /** `name -> icon URL` for the specs that bring their own icon (what the app page reads). */
118
+ export function appIconUrls(specs: readonly ManifestSpec[]): Record<string, string> {
119
+ const icons: Record<string, string> = {};
120
+ for (const spec of specs) if (spec.icon) icons[spec.name] = manifestIconPath(spec.name);
121
+ return icons;
122
+ }
123
+
124
+ export function appManifests(specs: readonly ManifestSpec[]): Plugin {
125
+ const byName = new Map(specs.map((spec) => [spec.name, spec]));
126
+ const icons = appIconUrls(specs);
127
+ const isAppPage = (pathname: string): boolean => /(^|\/)app\/(index\.html)?$/.test(pathname.split('?')[0] ?? '');
128
+
129
+ return {
130
+ name: 'swiftbrowser:app-manifests',
131
+ configureServer(server) {
132
+ server.middlewares.use((req, res, next) => {
133
+ const url = req.url ?? '';
134
+ const app = manifestRequestApp(url);
135
+ if (app !== null) {
136
+ const spec = byName.get(app);
137
+ if (!spec) {
138
+ res.statusCode = 404;
139
+ res.end();
140
+ return;
141
+ }
142
+ res.setHeader('Content-Type', 'application/manifest+json');
143
+ res.setHeader('Cache-Control', 'no-cache');
144
+ res.end(appManifestJson(spec));
145
+ return;
146
+ }
147
+ const iconApp = iconRequestApp(url);
148
+ if (iconApp === null) return next();
149
+ const icon = byName.get(iconApp)?.icon;
150
+ if (!icon || !existsSync(icon)) {
151
+ res.statusCode = 404;
152
+ res.end();
153
+ return;
154
+ }
155
+ res.setHeader('Content-Type', 'image/png');
156
+ res.setHeader('Cache-Control', 'no-cache');
157
+ res.end(readFileSync(icon));
158
+ });
159
+ },
160
+ transformIndexHtml(_html, ctx) {
161
+ // Only the app page needs the icon map, and only when some app has one.
162
+ if (Object.keys(icons).length === 0 || !isAppPage(ctx.path)) return;
163
+ return [
164
+ {
165
+ tag: 'script',
166
+ injectTo: 'head-prepend',
167
+ children: `window.__SB_APP_ICONS__ = ${JSON.stringify(icons)};`,
168
+ },
169
+ ];
170
+ },
171
+ generateBundle() {
172
+ for (const spec of specs) {
173
+ this.emitFile({
174
+ type: 'asset',
175
+ fileName: `${MANIFEST_DIR}/${spec.name}.webmanifest`,
176
+ source: appManifestJson(spec),
177
+ });
178
+ if (spec.icon) {
179
+ if (!existsSync(spec.icon)) {
180
+ this.warn(`${spec.name}: icon ${spec.icon} does not exist, skipped`);
181
+ continue;
182
+ }
183
+ this.emitFile({
184
+ type: 'asset',
185
+ fileName: `${MANIFEST_DIR}/${spec.name}-icon.png`,
186
+ source: readFileSync(spec.icon),
187
+ });
188
+ }
189
+ }
190
+ },
191
+ };
192
+ }