@swiftbrowser/web 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/index.html +3 -0
- package/node/dist/plugins/playground.d.ts +1 -1
- package/node/dist/plugins/playground.d.ts.map +1 -1
- package/node/dist/plugins/playground.js +3 -1
- package/node/dist/plugins/playground.js.map +1 -1
- package/package.json +6 -1
- package/plugins/playground.ts +3 -1
- package/src/main.ts +6 -2
- package/src/page.css +16 -0
- package/src/playground/app.tsx +311 -0
- package/src/playground/main.tsx +45 -0
- package/src/playground/playground.css +192 -29
- package/src/playground/session.ts +117 -0
- package/src/playground/main.ts +0 -133
package/app/index.html
CHANGED
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
var coarse = typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches;
|
|
41
41
|
var device = mode === 'device' || (mode !== 'frame' && (coarse || site.pageMode === 'device'));
|
|
42
42
|
document.documentElement.setAttribute('data-sb-mode', device ? 'device' : 'frame');
|
|
43
|
+
// ?embed=1: the page is in another's frame (the playground's
|
|
44
|
+
// simulator), which has its own controls: the device alone.
|
|
45
|
+
if (new URLSearchParams(location.search).get('embed') === '1') document.documentElement.setAttribute('data-sb-embed', '');
|
|
43
46
|
})();
|
|
44
47
|
// Point the manifest, the home-screen icon and the titles at the app
|
|
45
48
|
// this page runs (?app=, else the site's default), so Share -> Add to
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The playground page (playground/index.html): Swift compiled in the browser
|
|
3
3
|
* by the WebAssembly compiler (docs/in-browser-compiler.md, Milestone 3).
|
|
4
4
|
* Only this repository's site has it: its code imports
|
|
5
|
-
* @swiftbrowser/wasm-compiler
|
|
5
|
+
* @swiftbrowser/wasm-compiler, the CLI's browser entry and @swiftbrowser/react from their sources
|
|
6
6
|
* (the aliases below). The page fetches the npm packages it reads
|
|
7
7
|
* (src/playground/files.ts) from a CDN that serves them file by file, at one
|
|
8
8
|
* version, except those the site serves itself (`localPackages`): the kit
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"playground.d.ts","sourceRoot":"","sources":["../../../plugins/playground.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EAQL,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACtB,MAAM,4BAA4B,CAAC;AAGpC,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,CAAC;AAEjD,6FAA6F;AAC7F,eAAO,MAAM,mBAAmB,UAAU,CAAC;AAC3C,qEAAqE;AACrE,eAAO,MAAM,WAAW,sBAAsB,CAAC;AAC/C;;;GAGG;AACH,eAAO,MAAM,eAAe,QAAmB,CAAC;AAIhD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QAAY,CAAC;AAE5C,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"playground.d.ts","sourceRoot":"","sources":["../../../plugins/playground.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EAQL,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACtB,MAAM,4BAA4B,CAAC;AAGpC,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,CAAC;AAEjD,6FAA6F;AAC7F,eAAO,MAAM,mBAAmB,UAAU,CAAC;AAC3C,qEAAqE;AACrE,eAAO,MAAM,WAAW,sBAAsB,CAAC;AAC/C;;;GAGG;AACH,eAAO,MAAM,eAAe,QAAmB,CAAC;AAIhD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QAAY,CAAC;AAE5C,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO1E;AAED,2DAA2D;AAC3D,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQvE;AAED,uEAAuE;AACvE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAGxE;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,QAAQ,EAAE,kBAAkB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAQjF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,SAAS,MAAM,EAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CActG;AAMD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,SAAkB,EAAE,YAAY,SAAqB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA0DnM;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,MAAM,CA0BvF"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The playground page (playground/index.html): Swift compiled in the browser
|
|
3
3
|
* by the WebAssembly compiler (docs/in-browser-compiler.md, Milestone 3).
|
|
4
4
|
* Only this repository's site has it: its code imports
|
|
5
|
-
* @swiftbrowser/wasm-compiler
|
|
5
|
+
* @swiftbrowser/wasm-compiler, the CLI's browser entry and @swiftbrowser/react from their sources
|
|
6
6
|
* (the aliases below). The page fetches the npm packages it reads
|
|
7
7
|
* (src/playground/files.ts) from a CDN that serves them file by file, at one
|
|
8
8
|
* version, except those the site serves itself (`localPackages`): the kit
|
|
@@ -41,6 +41,8 @@ export function playgroundAliases(repoRoot) {
|
|
|
41
41
|
return {
|
|
42
42
|
'@swiftbrowser/wasm-compiler': path.join(repoRoot, 'packages/wasm-compiler/src/index.ts'),
|
|
43
43
|
'@swiftbrowser/cli/browser': path.join(repoRoot, 'packages/cli/src/browser.ts'),
|
|
44
|
+
// The page's own UI is SwiftUI views from @swiftbrowser/react.
|
|
45
|
+
'@swiftbrowser/react': path.join(repoRoot, 'packages/react/src/index.ts'),
|
|
44
46
|
};
|
|
45
47
|
}
|
|
46
48
|
/** The workspaces' package directories by package name. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"playground.js","sourceRoot":"","sources":["../../../plugins/playground.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACpI,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEhF,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,uBAAuB,EACvB,kBAAkB,EAClB,WAAW,EACX,QAAQ,GAIT,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAIpE,6FAA6F;AAC7F,MAAM,CAAC,MAAM,mBAAmB,GAAG,OAAO,CAAC;AAC3C,qEAAqE;AACrE,MAAM,CAAC,MAAM,WAAW,GAAG,mBAAmB,CAAC;AAC/C;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAChD,mFAAmF;AACnF,MAAM,WAAW,GAAG,CAAC,CAAC;AAEtB;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,GAAG,IAAI,CAAC;AAE5C,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;
|
|
1
|
+
{"version":3,"file":"playground.js","sourceRoot":"","sources":["../../../plugins/playground.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swiftbrowser/web",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
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": {
|
|
@@ -56,7 +56,12 @@
|
|
|
56
56
|
"@playwright/test": "^1.56.0",
|
|
57
57
|
"@types/node": "^26.6.4",
|
|
58
58
|
"@types/qrcode": "^1.5.6",
|
|
59
|
+
"@types/react": "^19.3.0",
|
|
60
|
+
"@types/react-dom": "^19.3.0",
|
|
59
61
|
"brotli-dec-wasm": "^2.3.2",
|
|
62
|
+
"react": "^19.3.1",
|
|
63
|
+
"react-dom": "^19.3.1",
|
|
64
|
+
"react-reconciler": "^0.34.0",
|
|
60
65
|
"typescript": "^5.9.2",
|
|
61
66
|
"vitest": "^5.0.3"
|
|
62
67
|
}
|
package/plugins/playground.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The playground page (playground/index.html): Swift compiled in the browser
|
|
3
3
|
* by the WebAssembly compiler (docs/in-browser-compiler.md, Milestone 3).
|
|
4
4
|
* Only this repository's site has it: its code imports
|
|
5
|
-
* @swiftbrowser/wasm-compiler
|
|
5
|
+
* @swiftbrowser/wasm-compiler, the CLI's browser entry and @swiftbrowser/react from their sources
|
|
6
6
|
* (the aliases below). The page fetches the npm packages it reads
|
|
7
7
|
* (src/playground/files.ts) from a CDN that serves them file by file, at one
|
|
8
8
|
* version, except those the site serves itself (`localPackages`): the kit
|
|
@@ -58,6 +58,8 @@ export function playgroundAliases(repoRoot: string): Record<string, string> {
|
|
|
58
58
|
return {
|
|
59
59
|
'@swiftbrowser/wasm-compiler': path.join(repoRoot, 'packages/wasm-compiler/src/index.ts'),
|
|
60
60
|
'@swiftbrowser/cli/browser': path.join(repoRoot, 'packages/cli/src/browser.ts'),
|
|
61
|
+
// The page's own UI is SwiftUI views from @swiftbrowser/react.
|
|
62
|
+
'@swiftbrowser/react': path.join(repoRoot, 'packages/react/src/index.ts'),
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
|
package/src/main.ts
CHANGED
|
@@ -147,11 +147,15 @@ function applyTheme(theme: Theme, persist: boolean): void {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* The OS appearance: device mode follows it, and an embedded page
|
|
152
|
+
* (`?embed=1`, the playground's simulator) too; the toolbar toggle is
|
|
153
|
+
* hidden in both.
|
|
154
|
+
*/
|
|
151
155
|
const darkScheme = typeof matchMedia === 'function' ? matchMedia('(prefers-color-scheme: dark)') : null;
|
|
152
156
|
const systemTheme = (): Theme => (darkScheme?.matches ? 'dark' : 'light');
|
|
153
157
|
|
|
154
|
-
if (deviceMode) {
|
|
158
|
+
if (deviceMode || document.documentElement.hasAttribute('data-sb-embed')) {
|
|
155
159
|
applyTheme(systemTheme(), false);
|
|
156
160
|
darkScheme?.addEventListener('change', () => {
|
|
157
161
|
applyTheme(systemTheme(), false);
|
package/src/page.css
CHANGED
|
@@ -232,3 +232,19 @@ html[data-sb-mode="device"] .status {
|
|
|
232
232
|
max-height: 40dvh;
|
|
233
233
|
overflow: auto;
|
|
234
234
|
}
|
|
235
|
+
|
|
236
|
+
/* Embedded (?embed=1, app/index.html): in another page's frame, which has
|
|
237
|
+
its own controls and background. The device alone, at the top. */
|
|
238
|
+
html[data-sb-embed] .toolbar {
|
|
239
|
+
display: none;
|
|
240
|
+
}
|
|
241
|
+
html[data-sb-embed],
|
|
242
|
+
html[data-sb-embed] body {
|
|
243
|
+
min-height: 0;
|
|
244
|
+
background: transparent;
|
|
245
|
+
overflow: hidden;
|
|
246
|
+
}
|
|
247
|
+
/* Room for the device's rings and shadow, which the frame would clip. */
|
|
248
|
+
html[data-sb-embed] .stage {
|
|
249
|
+
padding: 16px;
|
|
250
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// The playground's window, drawn with @swiftbrowser/react's SwiftUI views
|
|
2
|
+
// as Xcode lays one out: the project's files in a sidebar, then the editor
|
|
3
|
+
// beside the simulator. In a compact width (a phone) the code is a stack's
|
|
4
|
+
// root, the simulator a screen it pushes, and the files an inspector. The
|
|
5
|
+
// editor and the frame the app runs in are ordinary DOM in `Host`s.
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Button,
|
|
9
|
+
Divider,
|
|
10
|
+
HStack,
|
|
11
|
+
Host,
|
|
12
|
+
Label,
|
|
13
|
+
List,
|
|
14
|
+
NavigationSplitView,
|
|
15
|
+
NavigationStack,
|
|
16
|
+
Section,
|
|
17
|
+
ToolbarItem,
|
|
18
|
+
ZStack,
|
|
19
|
+
useHorizontalSizeClass,
|
|
20
|
+
} from '@swiftbrowser/react';
|
|
21
|
+
import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, type KeyboardEvent } from 'react';
|
|
22
|
+
import type { Session, SessionState } from './session';
|
|
23
|
+
|
|
24
|
+
/** The value a phone's stack pushes for the simulator's screen. */
|
|
25
|
+
const SIMULATOR = 'simulator';
|
|
26
|
+
|
|
27
|
+
export interface PlaygroundProps {
|
|
28
|
+
session: Session;
|
|
29
|
+
appName: string;
|
|
30
|
+
/** Path relative to the app's Sources/ → text. */
|
|
31
|
+
files: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function Playground({ session, appName, files: initialFiles }: PlaygroundProps) {
|
|
35
|
+
const state = useSyncExternalStore(session.subscribe, () => session.state);
|
|
36
|
+
const [files, setFiles] = useState(initialFiles);
|
|
37
|
+
const names = Object.keys(files).sort();
|
|
38
|
+
const [selection, setSelection] = useState<string | null>(names[0] ?? null);
|
|
39
|
+
// The file the editor shows: the sidebar's selection, or the last one
|
|
40
|
+
// when it has none.
|
|
41
|
+
const [shown, setShown] = useState(names[0] ?? '');
|
|
42
|
+
const select = (file: string | null): void => {
|
|
43
|
+
setSelection(file);
|
|
44
|
+
if (file) setShown(file);
|
|
45
|
+
};
|
|
46
|
+
const compact = useHorizontalSizeClass() === 'compact';
|
|
47
|
+
// A phone's stack: empty, or the simulator pushed over the code.
|
|
48
|
+
const [path, setPath] = useState<readonly unknown[]>([]);
|
|
49
|
+
const [filesShown, setFilesShown] = useState(false);
|
|
50
|
+
const latest = useRef(files);
|
|
51
|
+
latest.current = files;
|
|
52
|
+
const run = (): void => session.compile(latest.current);
|
|
53
|
+
|
|
54
|
+
// The first run loads the compiler and the SDK too.
|
|
55
|
+
useEffect(() => run(), []);
|
|
56
|
+
// On a phone a linked app comes into view.
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (state.linked > 0 && compact) setPath((stack) => (stack.length > 0 ? stack : [SIMULATOR]));
|
|
59
|
+
}, [state.linked]);
|
|
60
|
+
|
|
61
|
+
const editor = (
|
|
62
|
+
<Host sizing="fill" className="pg-pane" frame={{ maxWidth: Infinity, maxHeight: Infinity }}>
|
|
63
|
+
<Editor
|
|
64
|
+
file={shown}
|
|
65
|
+
text={files[shown] ?? ''}
|
|
66
|
+
onChange={(text) => setFiles((all) => ({ ...all, [shown]: text }))}
|
|
67
|
+
onRun={run}
|
|
68
|
+
state={state}
|
|
69
|
+
/>
|
|
70
|
+
</Host>
|
|
71
|
+
);
|
|
72
|
+
const preview = (
|
|
73
|
+
<Host sizing="fill" className="pg-pane" frame={compact ? { maxWidth: Infinity, maxHeight: Infinity } : { width: 430, maxHeight: Infinity }}>
|
|
74
|
+
<Simulator session={session} state={state} />
|
|
75
|
+
</Host>
|
|
76
|
+
);
|
|
77
|
+
const runButton = (
|
|
78
|
+
<ToolbarItem placement="primaryAction">
|
|
79
|
+
<Button action={run} disabled={state.busy}>
|
|
80
|
+
<Label title="Run" systemImage="play.fill" labelStyle={compact ? 'iconOnly' : 'titleAndIcon'} />
|
|
81
|
+
</Button>
|
|
82
|
+
</ToolbarItem>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const sources = (
|
|
86
|
+
<Section header="Sources">
|
|
87
|
+
{names.map((name) => (
|
|
88
|
+
<Label key={name} tag={name} title={name} systemImage="swift" />
|
|
89
|
+
))}
|
|
90
|
+
</Section>
|
|
91
|
+
);
|
|
92
|
+
// Picking a file in the phone's inspector opens it and closes the inspector.
|
|
93
|
+
const pick = (file: string | null): void => {
|
|
94
|
+
if (!file) return;
|
|
95
|
+
select(file);
|
|
96
|
+
setFilesShown(false);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (compact) {
|
|
100
|
+
// A phone: one stack. The code is its root, with the files in an
|
|
101
|
+
// inspector (a sheet in a compact width) and the simulator a screen the
|
|
102
|
+
// bar's iPhone button pushes, as does a build that links. Popping back
|
|
103
|
+
// unloads the app, which leaves its memory to the compiler.
|
|
104
|
+
return (
|
|
105
|
+
<NavigationStack
|
|
106
|
+
path={[path, setPath]}
|
|
107
|
+
destination={() => (
|
|
108
|
+
<ZStack navigationTitle={appName} navigationBarTitleDisplayMode="inline" toolbar={runButton}>
|
|
109
|
+
{preview}
|
|
110
|
+
</ZStack>
|
|
111
|
+
)}
|
|
112
|
+
>
|
|
113
|
+
<ZStack
|
|
114
|
+
navigationTitle={shown}
|
|
115
|
+
navigationBarTitleDisplayMode="inline"
|
|
116
|
+
toolbar={
|
|
117
|
+
<>
|
|
118
|
+
<ToolbarItem placement="topBarLeading">
|
|
119
|
+
<Button action={() => setFilesShown(true)}>
|
|
120
|
+
<Label title="Files" systemImage="folder" labelStyle="iconOnly" />
|
|
121
|
+
</Button>
|
|
122
|
+
</ToolbarItem>
|
|
123
|
+
{runButton}
|
|
124
|
+
<ToolbarItem placement="primaryAction">
|
|
125
|
+
<Button action={() => setPath([SIMULATOR])}>
|
|
126
|
+
<Label title="Simulator" systemImage="iphone" labelStyle="iconOnly" />
|
|
127
|
+
</Button>
|
|
128
|
+
</ToolbarItem>
|
|
129
|
+
</>
|
|
130
|
+
}
|
|
131
|
+
inspector={{
|
|
132
|
+
isPresented: [filesShown, setFilesShown],
|
|
133
|
+
content: (
|
|
134
|
+
<NavigationStack>
|
|
135
|
+
<List
|
|
136
|
+
selection={[shown, pick]}
|
|
137
|
+
navigationTitle="Files"
|
|
138
|
+
navigationBarTitleDisplayMode="inline"
|
|
139
|
+
presentationDetents={['medium', 'large']}
|
|
140
|
+
toolbar={
|
|
141
|
+
<ToolbarItem placement="confirmationAction">
|
|
142
|
+
<Button title="Done" action={() => setFilesShown(false)} />
|
|
143
|
+
</ToolbarItem>
|
|
144
|
+
}
|
|
145
|
+
>
|
|
146
|
+
{sources}
|
|
147
|
+
</List>
|
|
148
|
+
</NavigationStack>
|
|
149
|
+
),
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{editor}
|
|
153
|
+
</ZStack>
|
|
154
|
+
</NavigationStack>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<NavigationSplitView
|
|
160
|
+
sidebar={
|
|
161
|
+
<List selection={[selection, select]} navigationTitle={appName}>
|
|
162
|
+
{sources}
|
|
163
|
+
</List>
|
|
164
|
+
}
|
|
165
|
+
detail={
|
|
166
|
+
<HStack spacing={0} navigationTitle={shown} navigationBarTitleDisplayMode="inline" toolbar={runButton}>
|
|
167
|
+
{editor}
|
|
168
|
+
<Divider />
|
|
169
|
+
{preview}
|
|
170
|
+
</HStack>
|
|
171
|
+
}
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
interface EditorProps {
|
|
177
|
+
file: string;
|
|
178
|
+
text: string;
|
|
179
|
+
onChange: (text: string) => void;
|
|
180
|
+
onRun: () => void;
|
|
181
|
+
state: SessionState;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The source editor: a textarea with a line-number gutter, the issues below it, and a status bar. */
|
|
185
|
+
function Editor({ file, text, onChange, onRun, state }: EditorProps) {
|
|
186
|
+
const gutter = useRef<HTMLPreElement>(null);
|
|
187
|
+
const area = useRef<HTMLTextAreaElement>(null);
|
|
188
|
+
const [cursor, setCursor] = useState({ line: 1, column: 1 });
|
|
189
|
+
const lines = text.split('\n').length;
|
|
190
|
+
const syncScroll = (): void => {
|
|
191
|
+
if (gutter.current && area.current) gutter.current.scrollTop = area.current.scrollTop;
|
|
192
|
+
};
|
|
193
|
+
useLayoutEffect(syncScroll, [text, file]);
|
|
194
|
+
const track = (): void => {
|
|
195
|
+
const element = area.current;
|
|
196
|
+
if (!element) return;
|
|
197
|
+
const before = element.value.slice(0, element.selectionStart).split('\n');
|
|
198
|
+
setCursor({ line: before.length, column: before.at(-1)!.length + 1 });
|
|
199
|
+
};
|
|
200
|
+
const keyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {
|
|
201
|
+
// ⌘R or ⌘↩ (Ctrl on other systems) runs, as in Xcode.
|
|
202
|
+
if ((event.metaKey || event.ctrlKey) && (event.key === 'r' || event.key === 'Enter')) {
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
onRun();
|
|
205
|
+
} else if (event.key === 'Tab' && !event.shiftKey) {
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
const element = event.currentTarget;
|
|
208
|
+
const { selectionStart, selectionEnd, value } = element;
|
|
209
|
+
onChange(`${value.slice(0, selectionStart)} ${value.slice(selectionEnd)}`);
|
|
210
|
+
requestAnimationFrame(() => element.setSelectionRange(selectionStart + 4, selectionStart + 4));
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const issues = state.output.trim();
|
|
214
|
+
const count = (kind: string): number => (issues.match(new RegExp(`^[^\\s:][^:]*:\\d+:\\d+: ${kind}:`, 'gm')) ?? []).length;
|
|
215
|
+
const errors = count('error');
|
|
216
|
+
const warnings = count('warning');
|
|
217
|
+
// Issues open by themselves when a build fails; warnings wait to be asked for.
|
|
218
|
+
const [open, setOpen] = useState(false);
|
|
219
|
+
useEffect(() => setOpen(state.failed), [state.output, state.failed]);
|
|
220
|
+
const summary = [errors && `${errors} error${errors === 1 ? '' : 's'}`, warnings && `${warnings} warning${warnings === 1 ? '' : 's'}`].filter(Boolean).join(', ') || (state.failed ? 'Failed' : 'Output');
|
|
221
|
+
return (
|
|
222
|
+
<div className="pg-editor">
|
|
223
|
+
{state.warning && (
|
|
224
|
+
<p id="warning" role="alert">
|
|
225
|
+
{state.warning}
|
|
226
|
+
</p>
|
|
227
|
+
)}
|
|
228
|
+
<div className="pg-code">
|
|
229
|
+
<pre ref={gutter} className="pg-gutter" aria-hidden="true">
|
|
230
|
+
{Array.from({ length: lines }, (_, index) => `${index + 1}\n`).join('')}
|
|
231
|
+
</pre>
|
|
232
|
+
<textarea
|
|
233
|
+
ref={area}
|
|
234
|
+
id="source"
|
|
235
|
+
aria-label={file}
|
|
236
|
+
spellCheck={false}
|
|
237
|
+
autoCapitalize="off"
|
|
238
|
+
autoComplete="off"
|
|
239
|
+
autoCorrect="off"
|
|
240
|
+
wrap="off"
|
|
241
|
+
value={text}
|
|
242
|
+
onChange={(event) => onChange(event.target.value)}
|
|
243
|
+
onScroll={syncScroll}
|
|
244
|
+
onKeyDown={keyDown}
|
|
245
|
+
onKeyUp={track}
|
|
246
|
+
onClick={track}
|
|
247
|
+
/>
|
|
248
|
+
</div>
|
|
249
|
+
<pre id="diagnostics" className={state.failed ? 'pg-errors' : ''} hidden={issues === '' || !open}>
|
|
250
|
+
{issues}
|
|
251
|
+
</pre>
|
|
252
|
+
<div className="pg-statusbar">
|
|
253
|
+
<span id="status" role="status" className={state.busy ? 'pg-busy' : ''}>
|
|
254
|
+
{state.status}
|
|
255
|
+
</span>
|
|
256
|
+
{issues !== '' && (
|
|
257
|
+
<button type="button" className={`pg-issues${errors || state.failed ? ' pg-has-errors' : ''}`} aria-expanded={open} onClick={() => setOpen(!open)}>
|
|
258
|
+
{summary}
|
|
259
|
+
</button>
|
|
260
|
+
)}
|
|
261
|
+
<span className="pg-position">{`Line ${cursor.line}, Col ${cursor.column}`}</span>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Whether this is a touch device, where the app runs full screen in the simulator's pane. */
|
|
268
|
+
export const TOUCH = matchMedia('(pointer: coarse)').matches;
|
|
269
|
+
/**
|
|
270
|
+
* The embedded app page's size: its iPhone, bezel included (styles.css's
|
|
271
|
+
* --device-*), and 16 around it for the shadow (page.css).
|
|
272
|
+
*/
|
|
273
|
+
const DEVICE = { width: 417 + 32, height: 876 + 32 };
|
|
274
|
+
/** The simulator's margin around that, beyond the shadow's room. */
|
|
275
|
+
const MARGIN = 4;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The simulator: the frame the app runs in, and the compiler's progress
|
|
279
|
+
* over it while it runs. Off a touch device the frame is the iPhone's size,
|
|
280
|
+
* scaled to fit the pane; the app inside never knows, so its taps and
|
|
281
|
+
* scrolling are exact.
|
|
282
|
+
*/
|
|
283
|
+
function Simulator({ session, state }: { session: Session; state: SessionState }) {
|
|
284
|
+
const box = useRef<HTMLDivElement>(null);
|
|
285
|
+
const [fit, setFit] = useState({ scale: 1, top: MARGIN });
|
|
286
|
+
useLayoutEffect(() => {
|
|
287
|
+
const element = box.current;
|
|
288
|
+
if (TOUCH || !element) return;
|
|
289
|
+
const observer = new ResizeObserver(() => {
|
|
290
|
+
const { clientWidth: width, clientHeight: height } = element;
|
|
291
|
+
const scale = Math.max(0.2, Math.min(1, (width - 2 * MARGIN) / DEVICE.width, (height - 2 * MARGIN) / DEVICE.height));
|
|
292
|
+
setFit({ scale, top: Math.max(MARGIN, (height - DEVICE.height * scale) / 2) });
|
|
293
|
+
});
|
|
294
|
+
observer.observe(element);
|
|
295
|
+
return () => observer.disconnect();
|
|
296
|
+
}, []);
|
|
297
|
+
const style = TOUCH
|
|
298
|
+
? undefined
|
|
299
|
+
: { width: DEVICE.width, height: DEVICE.height, top: fit.top, transform: `translateX(-50%) scale(${fit.scale})` };
|
|
300
|
+
return (
|
|
301
|
+
<div ref={box} className={TOUCH ? 'pg-simulator pg-full' : 'pg-simulator'}>
|
|
302
|
+
<iframe ref={session.attachFrame} id="app" title="The app" src="about:blank" style={style} />
|
|
303
|
+
{state.busy && (
|
|
304
|
+
<div className="pg-activity" aria-hidden="true">
|
|
305
|
+
<span className="pg-spinner" />
|
|
306
|
+
{state.status}
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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 (session.ts, worker.ts), and run
|
|
4
|
+
// by the renderer's own page in a frame. The page itself is SwiftUI views
|
|
5
|
+
// from @swiftbrowser/react (app.tsx). Counter's sources come with the page,
|
|
6
|
+
// so after the first visit's downloads nothing reaches a server.
|
|
7
|
+
|
|
8
|
+
import '../styles.css';
|
|
9
|
+
import './playground.css';
|
|
10
|
+
import { SwiftUIView } from '@swiftbrowser/react';
|
|
11
|
+
import { useSyncExternalStore } from 'react';
|
|
12
|
+
import { createRoot } from 'react-dom/client';
|
|
13
|
+
import { Playground, TOUCH } from './app';
|
|
14
|
+
import { Session } from './session';
|
|
15
|
+
|
|
16
|
+
const sources = import.meta.glob('../../../Examples/Counter/Sources/**/*.swift', { query: '?raw', import: 'default', eager: true }) as Record<string, string>;
|
|
17
|
+
const APP = 'Counter';
|
|
18
|
+
|
|
19
|
+
const source = {
|
|
20
|
+
...__SB_PLAYGROUND__,
|
|
21
|
+
npm: new URL(__SB_PLAYGROUND__.npm, location.href).href,
|
|
22
|
+
...(__SB_PLAYGROUND__.packages
|
|
23
|
+
? { packages: Object.fromEntries(Object.entries(__SB_PLAYGROUND__.packages).map(([name, base]) => [name, new URL(base, location.href).href])) }
|
|
24
|
+
: {}),
|
|
25
|
+
};
|
|
26
|
+
const files = Object.fromEntries(Object.entries(sources).map(([file, text]) => [file.replace(/^.*\/Sources\//, ''), text]));
|
|
27
|
+
// A touch device runs the app full screen in its pane; elsewhere the
|
|
28
|
+
// simulator shows the app page's iPhone (app.tsx).
|
|
29
|
+
const session = new Session(source, APP, TOUCH ? 'device' : 'frame');
|
|
30
|
+
const scheme = matchMedia('(prefers-color-scheme: dark)');
|
|
31
|
+
const watchScheme = (change: () => void): (() => void) => {
|
|
32
|
+
scheme.addEventListener('change', change);
|
|
33
|
+
return () => scheme.removeEventListener('change', change);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function Root() {
|
|
37
|
+
const dark = useSyncExternalStore(watchScheme, () => scheme.matches);
|
|
38
|
+
return (
|
|
39
|
+
<SwiftUIView className="pg-root" colorScheme={dark ? 'dark' : 'light'} safeArea={0}>
|
|
40
|
+
<Playground session={session} appName={APP} files={files} />
|
|
41
|
+
</SwiftUIView>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
createRoot(document.getElementById('root')!).render(<Root />);
|
|
@@ -1,31 +1,194 @@
|
|
|
1
|
-
:
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
/* The playground's window (app.tsx): SwiftUI views fill it; the editor and
|
|
2
|
+
the simulator are DOM in their hosts, styled with the renderer's tokens so
|
|
3
|
+
they follow dark mode. */
|
|
4
|
+
html,
|
|
5
|
+
body,
|
|
6
|
+
#root,
|
|
7
|
+
.pg-root {
|
|
8
|
+
margin: 0;
|
|
9
|
+
height: 100%;
|
|
10
|
+
}
|
|
11
|
+
/* The page behind the views, which a phone's browser tints its bars with:
|
|
12
|
+
the system background of the appearance the views follow (main.tsx). The
|
|
13
|
+
tokens are the island's, so not the body's to read. */
|
|
14
|
+
body {
|
|
15
|
+
background: #ffffff;
|
|
16
|
+
overscroll-behavior: none;
|
|
10
17
|
}
|
|
11
18
|
@media (prefers-color-scheme: dark) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
body {
|
|
20
|
+
background: #000000;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.pg-pane {
|
|
25
|
+
height: 100%;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* The editor: a gutter and the text, issues below, a status bar at the foot. */
|
|
29
|
+
.pg-editor {
|
|
30
|
+
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif;
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
height: 100%;
|
|
34
|
+
min-width: 0;
|
|
35
|
+
background: var(--sb-color-system-background);
|
|
36
|
+
color: var(--sb-color-primary);
|
|
37
|
+
--pg-mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
|
|
38
|
+
}
|
|
39
|
+
.pg-code {
|
|
40
|
+
position: relative;
|
|
41
|
+
display: flex;
|
|
42
|
+
flex: 1;
|
|
43
|
+
min-height: 0;
|
|
44
|
+
}
|
|
45
|
+
.pg-gutter,
|
|
46
|
+
.pg-code textarea {
|
|
47
|
+
margin: 0;
|
|
48
|
+
padding: 12px 0;
|
|
49
|
+
font: 13px/20px var(--pg-mono);
|
|
50
|
+
}
|
|
51
|
+
.pg-gutter {
|
|
52
|
+
flex: none;
|
|
53
|
+
width: 44px;
|
|
54
|
+
padding-right: 10px;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
text-align: right;
|
|
57
|
+
color: var(--sb-color-tertiary-label);
|
|
58
|
+
user-select: none;
|
|
59
|
+
white-space: pre;
|
|
60
|
+
}
|
|
61
|
+
.pg-code textarea {
|
|
62
|
+
flex: 1;
|
|
63
|
+
min-width: 0;
|
|
64
|
+
padding-left: 6px;
|
|
65
|
+
padding-right: 12px;
|
|
66
|
+
border: none;
|
|
67
|
+
outline: none;
|
|
68
|
+
resize: none;
|
|
69
|
+
background: transparent;
|
|
70
|
+
color: inherit;
|
|
71
|
+
tab-size: 4;
|
|
72
|
+
white-space: pre;
|
|
73
|
+
caret-color: var(--sb-color-accent);
|
|
74
|
+
}
|
|
75
|
+
#warning {
|
|
76
|
+
margin: 0;
|
|
77
|
+
padding: 8px 12px;
|
|
78
|
+
background: color-mix(in srgb, var(--sb-color-yellow) 18%, transparent);
|
|
79
|
+
border-bottom: 1px solid var(--sb-color-separator);
|
|
80
|
+
font-size: 13px;
|
|
81
|
+
}
|
|
82
|
+
#diagnostics {
|
|
83
|
+
flex: none;
|
|
84
|
+
max-height: 38%;
|
|
85
|
+
margin: 0;
|
|
86
|
+
padding: 10px 12px;
|
|
87
|
+
overflow: auto;
|
|
88
|
+
border-top: 1px solid var(--sb-color-separator);
|
|
89
|
+
background: var(--sb-color-secondary-system-background);
|
|
90
|
+
font: 12px/1.45 var(--pg-mono);
|
|
91
|
+
white-space: pre-wrap;
|
|
92
|
+
}
|
|
93
|
+
#diagnostics.pg-errors {
|
|
94
|
+
color: var(--sb-color-red);
|
|
95
|
+
}
|
|
96
|
+
.pg-statusbar {
|
|
97
|
+
flex: none;
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: 12px;
|
|
101
|
+
height: 26px;
|
|
102
|
+
padding: 0 12px;
|
|
103
|
+
border-top: 1px solid var(--sb-color-separator);
|
|
104
|
+
background: var(--sb-color-secondary-system-background);
|
|
105
|
+
color: var(--sb-color-secondary);
|
|
106
|
+
font-size: 11px;
|
|
107
|
+
white-space: nowrap;
|
|
108
|
+
}
|
|
109
|
+
#status {
|
|
110
|
+
flex: 1;
|
|
111
|
+
min-width: 0;
|
|
112
|
+
overflow: hidden;
|
|
113
|
+
text-overflow: ellipsis;
|
|
114
|
+
}
|
|
115
|
+
.pg-issues {
|
|
116
|
+
flex: none;
|
|
117
|
+
padding: 1px 8px;
|
|
118
|
+
border: none;
|
|
119
|
+
border-radius: 999px;
|
|
120
|
+
background: color-mix(in srgb, var(--sb-color-yellow) 28%, transparent);
|
|
121
|
+
color: var(--sb-color-primary);
|
|
122
|
+
font: inherit;
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
}
|
|
125
|
+
.pg-issues.pg-has-errors {
|
|
126
|
+
background: color-mix(in srgb, var(--sb-color-red) 22%, transparent);
|
|
127
|
+
}
|
|
128
|
+
.pg-issues[aria-expanded='true'] {
|
|
129
|
+
outline: 1px solid var(--sb-color-separator);
|
|
130
|
+
}
|
|
131
|
+
.pg-position {
|
|
132
|
+
font-variant-numeric: tabular-nums;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* The simulator: the app's own page (its iPhone, or full screen on a touch
|
|
136
|
+
device) in a frame, with the compiler's progress floating over it. */
|
|
137
|
+
.pg-simulator {
|
|
138
|
+
position: relative;
|
|
139
|
+
height: 100%;
|
|
140
|
+
background: var(--sb-color-secondary-system-background);
|
|
141
|
+
}
|
|
142
|
+
.pg-simulator {
|
|
143
|
+
overflow: hidden;
|
|
144
|
+
}
|
|
145
|
+
.pg-simulator iframe {
|
|
146
|
+
position: absolute;
|
|
147
|
+
left: 50%;
|
|
148
|
+
display: block;
|
|
149
|
+
border: none;
|
|
150
|
+
transform-origin: top center;
|
|
151
|
+
color-scheme: normal;
|
|
152
|
+
}
|
|
153
|
+
.pg-simulator.pg-full iframe {
|
|
154
|
+
inset: 0;
|
|
155
|
+
left: 0;
|
|
156
|
+
width: 100%;
|
|
157
|
+
height: 100%;
|
|
158
|
+
}
|
|
159
|
+
.pg-activity {
|
|
160
|
+
position: absolute;
|
|
161
|
+
top: 12px;
|
|
162
|
+
left: 50%;
|
|
163
|
+
transform: translateX(-50%);
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
gap: 8px;
|
|
167
|
+
max-width: calc(100% - 32px);
|
|
168
|
+
padding: 8px 14px;
|
|
169
|
+
border-radius: 999px;
|
|
170
|
+
background: var(--sb-color-material-base, rgba(255, 255, 255, 0.8));
|
|
171
|
+
-webkit-backdrop-filter: blur(20px) saturate(1.8);
|
|
172
|
+
backdrop-filter: blur(20px) saturate(1.8);
|
|
173
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
174
|
+
color: var(--sb-color-primary);
|
|
175
|
+
font-size: 13px;
|
|
176
|
+
white-space: nowrap;
|
|
177
|
+
overflow: hidden;
|
|
178
|
+
text-overflow: ellipsis;
|
|
179
|
+
pointer-events: none;
|
|
180
|
+
}
|
|
181
|
+
.pg-spinner {
|
|
182
|
+
flex: none;
|
|
183
|
+
width: 12px;
|
|
184
|
+
height: 12px;
|
|
185
|
+
border: 2px solid var(--sb-color-tertiary-label);
|
|
186
|
+
border-top-color: var(--sb-color-accent);
|
|
187
|
+
border-radius: 50%;
|
|
188
|
+
animation: pg-spin 0.8s linear infinite;
|
|
189
|
+
}
|
|
190
|
+
@keyframes pg-spin {
|
|
191
|
+
to {
|
|
192
|
+
transform: rotate(360deg);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// The playground's compiles: an app's sources sent to the compiler's worker
|
|
2
|
+
// (worker.ts), the module it links run by the renderer's own page in the
|
|
3
|
+
// frame (`?module=`, a blob: URL), and what the page shows meanwhile. The
|
|
4
|
+
// UI (app.tsx) reads `state` and calls `compile`; it hands over the frame
|
|
5
|
+
// with `attachFrame` whenever it mounts one.
|
|
6
|
+
|
|
7
|
+
import { frozenCopy, restoreScroll } from './freeze';
|
|
8
|
+
import type { PlaygroundSource } from './files';
|
|
9
|
+
import type { FromWorker, ToWorker } from './worker';
|
|
10
|
+
|
|
11
|
+
export interface SessionState {
|
|
12
|
+
/** A compile is under way. */
|
|
13
|
+
busy: boolean;
|
|
14
|
+
/** One line on what the compiler is doing or did. */
|
|
15
|
+
status: string;
|
|
16
|
+
/** The compiler's diagnostics, or the error the worker failed with. */
|
|
17
|
+
output: string;
|
|
18
|
+
/** Whether the last compile failed (its output is errors). */
|
|
19
|
+
failed: boolean;
|
|
20
|
+
/** Why the kit may not suit this page's renderer, when it may not. */
|
|
21
|
+
warning: string | null;
|
|
22
|
+
/** How many compiles have linked an app. */
|
|
23
|
+
linked: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class Session {
|
|
27
|
+
state: SessionState = { busy: false, status: 'Loading the compiler…', output: '', failed: false, warning: null, linked: 0 };
|
|
28
|
+
private readonly worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
|
|
29
|
+
private readonly listeners = new Set<() => void>();
|
|
30
|
+
private nextId = 1;
|
|
31
|
+
private started = 0;
|
|
32
|
+
private frame: HTMLIFrameElement | null = null;
|
|
33
|
+
/** The last linked app, and the still copy of it a compile shows instead (freeze.ts). */
|
|
34
|
+
private moduleUrl: string | null = null;
|
|
35
|
+
private frozenUrl: string | null = null;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `frame`: the app page in an embedded frame mode (its iPhone, without
|
|
39
|
+
* its toolbar) for the simulator to scale; `device`: full screen.
|
|
40
|
+
*/
|
|
41
|
+
constructor(
|
|
42
|
+
private readonly source: PlaygroundSource,
|
|
43
|
+
private readonly appName: string,
|
|
44
|
+
private readonly appMode: 'frame' | 'device',
|
|
45
|
+
) {
|
|
46
|
+
this.worker.onmessage = (event: MessageEvent<FromWorker>) => this.receive(event.data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
50
|
+
this.listeners.add(listener);
|
|
51
|
+
return () => this.listeners.delete(listener);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
private set(changes: Partial<SessionState>): void {
|
|
55
|
+
this.state = { ...this.state, ...changes };
|
|
56
|
+
for (const listener of this.listeners) listener();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The frame the app runs in, or null when the UI unmounts it; a new one shows what the last had. */
|
|
60
|
+
attachFrame = (frame: HTMLIFrameElement | null): void => {
|
|
61
|
+
this.frame = frame;
|
|
62
|
+
if (!frame) return;
|
|
63
|
+
frame.addEventListener('load', () => {
|
|
64
|
+
if (this.frozenUrl && frame.contentDocument && frame.contentWindow?.location.href === this.frozenUrl) restoreScroll(frame.contentDocument);
|
|
65
|
+
});
|
|
66
|
+
const url = this.frozenUrl ?? (this.moduleUrl ? this.appUrl(this.moduleUrl) : null);
|
|
67
|
+
if (url) frame.src = url;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
private appUrl(moduleUrl: string): string {
|
|
71
|
+
const mode = this.appMode === 'frame' ? 'mode=frame&embed=1' : 'mode=device';
|
|
72
|
+
return `${import.meta.env.BASE_URL}app/?app=${this.appName}&${mode}&module=${encodeURIComponent(moduleUrl)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
compile(files: Record<string, string>): void {
|
|
76
|
+
if (this.state.busy) return;
|
|
77
|
+
// The last build's app goes while the compiler runs: its page, its module
|
|
78
|
+
// and its memory would stay beside the linker's, which a phone has not got.
|
|
79
|
+
// A still copy of it stays in its place.
|
|
80
|
+
if (this.moduleUrl) {
|
|
81
|
+
let copy: string | null = null;
|
|
82
|
+
try {
|
|
83
|
+
copy = this.frame?.contentDocument ? frozenCopy(this.frame.contentDocument) : null;
|
|
84
|
+
} catch {
|
|
85
|
+
// No copy: the frame goes blank.
|
|
86
|
+
}
|
|
87
|
+
if (this.frozenUrl) URL.revokeObjectURL(this.frozenUrl);
|
|
88
|
+
this.frozenUrl = copy ? URL.createObjectURL(new Blob([copy], { type: 'text/html' })) : null;
|
|
89
|
+
if (this.frame) this.frame.src = this.frozenUrl ?? 'about:blank';
|
|
90
|
+
URL.revokeObjectURL(this.moduleUrl);
|
|
91
|
+
this.moduleUrl = null;
|
|
92
|
+
}
|
|
93
|
+
this.started = performance.now();
|
|
94
|
+
this.set({ busy: true, status: 'Compiling…' });
|
|
95
|
+
const message: ToWorker = { type: 'compile', id: this.nextId++, source: this.source, app: { name: this.appName, files: { ...files } } };
|
|
96
|
+
this.worker.postMessage(message);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private receive(message: FromWorker): void {
|
|
100
|
+
if (message.type === 'progress') return this.set({ status: message.text });
|
|
101
|
+
if (message.type === 'warning') return this.set({ warning: message.text });
|
|
102
|
+
if (message.type === 'failure') return this.set({ busy: false, status: 'The compiler failed.', output: message.error, failed: true });
|
|
103
|
+
const seconds = (performance.now() - this.started) / 1000;
|
|
104
|
+
// Each tool's memory too: what a phone's browser runs out of first.
|
|
105
|
+
const steps = message.steps
|
|
106
|
+
.map((step) => `${step.tool} ${step.seconds.toFixed(1)} s${step.memoryBytes ? `, ${Math.round(step.memoryBytes / 1e6)} MB` : ''}`)
|
|
107
|
+
.join('; ');
|
|
108
|
+
if (!message.ok || !message.wasm) {
|
|
109
|
+
return this.set({ busy: false, status: `Did not compile (${steps}).`, output: message.output, failed: true });
|
|
110
|
+
}
|
|
111
|
+
if (this.frozenUrl) URL.revokeObjectURL(this.frozenUrl);
|
|
112
|
+
this.frozenUrl = null;
|
|
113
|
+
this.moduleUrl = URL.createObjectURL(new Blob([message.wasm], { type: 'application/wasm' }));
|
|
114
|
+
if (this.frame) this.frame.src = this.appUrl(this.moduleUrl);
|
|
115
|
+
this.set({ busy: false, status: `Compiled and linked in ${seconds.toFixed(1)} s (${steps}).`, output: message.output, failed: false, linked: this.state.linked + 1 });
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/playground/main.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
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 { frozenCopy, restoreScroll } from './freeze';
|
|
9
|
-
import type { FromWorker, ToWorker } from './worker';
|
|
10
|
-
|
|
11
|
-
const sources = import.meta.glob('../../../Examples/Counter/Sources/**/*.swift', { query: '?raw', import: 'default', eager: true }) as Record<string, string>;
|
|
12
|
-
const APP = 'Counter';
|
|
13
|
-
|
|
14
|
-
function $<T extends HTMLElement>(selector: string): T {
|
|
15
|
-
const element = document.querySelector<T>(selector);
|
|
16
|
-
if (!element) throw new Error(`missing ${selector}`);
|
|
17
|
-
return element;
|
|
18
|
-
}
|
|
19
|
-
const filesNav = $<HTMLElement>('#files');
|
|
20
|
-
const editor = $<HTMLTextAreaElement>('#source');
|
|
21
|
-
const run = $<HTMLButtonElement>('#run');
|
|
22
|
-
const status = $<HTMLElement>('#status');
|
|
23
|
-
const diagnostics = $<HTMLPreElement>('#diagnostics');
|
|
24
|
-
const frame = $<HTMLIFrameElement>('#app');
|
|
25
|
-
const warning = $<HTMLElement>('#warning');
|
|
26
|
-
|
|
27
|
-
// Path relative to Sources/ → text, as edited.
|
|
28
|
-
const files: Record<string, string> = Object.fromEntries(
|
|
29
|
-
Object.entries(sources).map(([file, text]) => [file.replace(/^.*\/Sources\//, ''), text]),
|
|
30
|
-
);
|
|
31
|
-
let current = Object.keys(files).sort()[0]!;
|
|
32
|
-
|
|
33
|
-
function showFiles(): void {
|
|
34
|
-
filesNav.replaceChildren(...Object.keys(files).sort().map((file) => {
|
|
35
|
-
const button = document.createElement('button');
|
|
36
|
-
button.type = 'button';
|
|
37
|
-
button.textContent = file;
|
|
38
|
-
button.setAttribute('aria-current', String(file === current));
|
|
39
|
-
button.addEventListener('click', () => {
|
|
40
|
-
files[current] = editor.value;
|
|
41
|
-
current = file;
|
|
42
|
-
editor.value = files[file]!;
|
|
43
|
-
showFiles();
|
|
44
|
-
});
|
|
45
|
-
return button;
|
|
46
|
-
}));
|
|
47
|
-
}
|
|
48
|
-
editor.value = files[current]!;
|
|
49
|
-
editor.addEventListener('input', () => (files[current] = editor.value));
|
|
50
|
-
showFiles();
|
|
51
|
-
|
|
52
|
-
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
|
|
53
|
-
const source = {
|
|
54
|
-
...__SB_PLAYGROUND__,
|
|
55
|
-
npm: new URL(__SB_PLAYGROUND__.npm, location.href).href,
|
|
56
|
-
...(__SB_PLAYGROUND__.packages
|
|
57
|
-
? { packages: Object.fromEntries(Object.entries(__SB_PLAYGROUND__.packages).map(([name, base]) => [name, new URL(base, location.href).href])) }
|
|
58
|
-
: {}),
|
|
59
|
-
};
|
|
60
|
-
let nextId = 1;
|
|
61
|
-
let moduleUrl: string | null = null;
|
|
62
|
-
/** The still copy of the last app the frame shows while a compile runs. */
|
|
63
|
-
let frozenUrl: string | null = null;
|
|
64
|
-
frame.addEventListener('load', () => {
|
|
65
|
-
if (frozenUrl && frame.contentDocument && frame.contentWindow?.location.href === frozenUrl) restoreScroll(frame.contentDocument);
|
|
66
|
-
});
|
|
67
|
-
const started = new Map<number, number>();
|
|
68
|
-
|
|
69
|
-
worker.onmessage = (event: MessageEvent<FromWorker>) => {
|
|
70
|
-
const message = event.data;
|
|
71
|
-
if (message.type === 'progress') {
|
|
72
|
-
status.textContent = message.text;
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (message.type === 'warning') {
|
|
76
|
-
warning.hidden = false;
|
|
77
|
-
warning.textContent = message.text;
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
run.disabled = false;
|
|
81
|
-
if (message.type === 'failure') {
|
|
82
|
-
status.textContent = 'The compiler failed.';
|
|
83
|
-
diagnostics.hidden = false;
|
|
84
|
-
diagnostics.textContent = message.error;
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const seconds = (performance.now() - (started.get(message.id) ?? performance.now())) / 1000;
|
|
88
|
-
// Each tool's memory too: what a phone's browser runs out of first.
|
|
89
|
-
const steps = message.steps
|
|
90
|
-
.map((step) => `${step.tool} ${step.seconds.toFixed(1)} s${step.memoryBytes ? `, ${Math.round(step.memoryBytes / 1e6)} MB` : ''}`)
|
|
91
|
-
.join('; ');
|
|
92
|
-
diagnostics.hidden = message.ok || message.output === '';
|
|
93
|
-
diagnostics.textContent = message.output;
|
|
94
|
-
if (!message.ok || !message.wasm) {
|
|
95
|
-
status.textContent = `Did not compile (${steps}).`;
|
|
96
|
-
diagnostics.hidden = false;
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
status.textContent = `Compiled and linked in ${seconds.toFixed(1)} s (${steps}).`;
|
|
100
|
-
if (frozenUrl) URL.revokeObjectURL(frozenUrl);
|
|
101
|
-
frozenUrl = null;
|
|
102
|
-
moduleUrl = URL.createObjectURL(new Blob([message.wasm], { type: 'application/wasm' }));
|
|
103
|
-
frame.src = `${import.meta.env.BASE_URL}app/?app=${APP}&module=${encodeURIComponent(moduleUrl)}`;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
function compile(): void {
|
|
107
|
-
run.disabled = true;
|
|
108
|
-
// The last build's app goes while the compiler runs: its page, its module
|
|
109
|
-
// and its memory would stay beside the linker's, which a phone has not got.
|
|
110
|
-
// A still copy of it stays in its place (freeze.ts).
|
|
111
|
-
if (moduleUrl) {
|
|
112
|
-
let copy: string | null = null;
|
|
113
|
-
try {
|
|
114
|
-
copy = frame.contentDocument ? frozenCopy(frame.contentDocument) : null;
|
|
115
|
-
} catch {
|
|
116
|
-
// No copy: the frame goes blank.
|
|
117
|
-
}
|
|
118
|
-
if (frozenUrl) URL.revokeObjectURL(frozenUrl);
|
|
119
|
-
frozenUrl = copy ? URL.createObjectURL(new Blob([copy], { type: 'text/html' })) : null;
|
|
120
|
-
frame.src = frozenUrl ?? 'about:blank';
|
|
121
|
-
URL.revokeObjectURL(moduleUrl);
|
|
122
|
-
moduleUrl = null;
|
|
123
|
-
}
|
|
124
|
-
diagnostics.hidden = true;
|
|
125
|
-
status.textContent = 'Compiling…';
|
|
126
|
-
const id = nextId++;
|
|
127
|
-
started.set(id, performance.now());
|
|
128
|
-
const message: ToWorker = { type: 'compile', id, source, app: { name: APP, files: { ...files } } };
|
|
129
|
-
worker.postMessage(message);
|
|
130
|
-
}
|
|
131
|
-
run.addEventListener('click', compile);
|
|
132
|
-
// The first run loads the compiler and the SDK too.
|
|
133
|
-
compile();
|