@start.dev/container 0.0.1
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/dist/_wc-vendored-types.d.ts +38 -0
- package/dist/command-resolver.d.ts +85 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.js +1805 -0
- package/dist/runtime-assets.d.ts +44 -0
- package/dist/runtime-assets.js +27 -0
- package/dist/types.d.ts +230 -0
- package/dist/workers/runtime-sw.js +269 -0
- package/dist/workers/shell-host.worker.js +42835 -0
- package/dist/workers/streaming-process.worker.js +31493 -0
- package/dist/workers/vite-serve.worker.js +35697 -0
- package/package.json +27 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolvers for the worker + Service-Worker assets that @start.dev/container SHIPS in its
|
|
3
|
+
* built `dist/`. These let an external consumer (start.dev) wire
|
|
4
|
+
* `WebContainer.boot({ runtime })` WITHOUT the monorepo, a runtime bundler, or
|
|
5
|
+
* hand-copied worker files: the URLs point at the assets that live inside the
|
|
6
|
+
* installed package.
|
|
7
|
+
*
|
|
8
|
+
* Mechanism: each resolver returns `new URL('./workers/<file>', import.meta.url)`.
|
|
9
|
+
* After the build this module lives at `dist/runtime-assets.js`, so `import.meta.url`
|
|
10
|
+
* is the installed package location and the workers resolve next to it. Every modern
|
|
11
|
+
* bundler (Vite, webpack 5, Rollup, esbuild) understands the
|
|
12
|
+
* `new URL('…', import.meta.url)` pattern and will EMIT these as first-class assets,
|
|
13
|
+
* so the consumer's build pipeline serves them automatically.
|
|
14
|
+
*
|
|
15
|
+
* NOTE — this file is only meaningful in the BUILT package. In the monorepo source
|
|
16
|
+
* there is no `dist/workers/*`, so the in-repo acceptance harness keeps building +
|
|
17
|
+
* serving the worker URLs itself (it does not import this module). External
|
|
18
|
+
* consumers import it from `@start.dev/container/runtime-assets`.
|
|
19
|
+
*/
|
|
20
|
+
/** URL of the shipped, pre-bundled real Vite-serve worker (ESM module worker). */
|
|
21
|
+
export declare function viteWorkerUrl(): string;
|
|
22
|
+
/** URL of the shipped, pre-bundled real Node streaming-process worker (ESM module worker). */
|
|
23
|
+
export declare function processWorkerUrl(): string;
|
|
24
|
+
/** URL of the shipped, pre-bundled real shell worker (@wc/shell / just-bash; ESM module worker). */
|
|
25
|
+
export declare function shellWorkerUrl(): string;
|
|
26
|
+
/**
|
|
27
|
+
* URL of the shipped substrate Service Worker script. The consumer must serve this
|
|
28
|
+
* UNDER its preview base scope with the runtime COOP/COEP headers (it cannot be a
|
|
29
|
+
* cross-origin URL — a Service Worker can only control same-origin scopes), so most
|
|
30
|
+
* hosts copy it into their served `previewBase` directory rather than importing this
|
|
31
|
+
* URL directly. Returned here for completeness / programmatic copying.
|
|
32
|
+
*/
|
|
33
|
+
export declare function serviceWorkerUrl(): string;
|
|
34
|
+
/**
|
|
35
|
+
* Convenience: the worker-URL subset of `RuntimeWiring` filled from the shipped
|
|
36
|
+
* assets. The consumer still supplies the host-specific fields it owns
|
|
37
|
+
* (`serviceWorkerUrl` served under its scope, `previewBase`, the WASM URLs, and the
|
|
38
|
+
* project `dependencyClosure`). Spread this into `boot({ runtime: { ... } })`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function shippedWorkerUrls(): {
|
|
41
|
+
viteWorkerUrl: string;
|
|
42
|
+
processWorkerUrl: string;
|
|
43
|
+
shellWorkerUrl: string;
|
|
44
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/runtime-assets.ts
|
|
2
|
+
function viteWorkerUrl() {
|
|
3
|
+
return new URL("./workers/vite-serve.worker.js", import.meta.url).href;
|
|
4
|
+
}
|
|
5
|
+
function processWorkerUrl() {
|
|
6
|
+
return new URL("./workers/streaming-process.worker.js", import.meta.url).href;
|
|
7
|
+
}
|
|
8
|
+
function shellWorkerUrl() {
|
|
9
|
+
return new URL("./workers/shell-host.worker.js", import.meta.url).href;
|
|
10
|
+
}
|
|
11
|
+
function serviceWorkerUrl() {
|
|
12
|
+
return new URL("./workers/runtime-sw.js", import.meta.url).href;
|
|
13
|
+
}
|
|
14
|
+
function shippedWorkerUrls() {
|
|
15
|
+
return {
|
|
16
|
+
viteWorkerUrl: viteWorkerUrl(),
|
|
17
|
+
processWorkerUrl: processWorkerUrl(),
|
|
18
|
+
shellWorkerUrl: shellWorkerUrl()
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
viteWorkerUrl,
|
|
23
|
+
shippedWorkerUrls,
|
|
24
|
+
shellWorkerUrl,
|
|
25
|
+
serviceWorkerUrl,
|
|
26
|
+
processWorkerUrl
|
|
27
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public TypeScript surface for `@start.dev/container`, mirroring StackBlitz's official
|
|
3
|
+
* `@webcontainer/api` so the start.dev adapter (and any consumer) can build against
|
|
4
|
+
* the canonical shape and drop us in. Names + signatures match `@webcontainer/api`
|
|
5
|
+
* 1.x; members our runtime does not yet cover are documented as honest stubs in
|
|
6
|
+
* `WebContainer` (they throw "not yet implemented"), never faked.
|
|
7
|
+
*/
|
|
8
|
+
import type { FsWatcher, WatchListener, WatchOptions } from './_wc-vendored-types';
|
|
9
|
+
export type { FsWatcher, WatchListener, WatchOptions } from './_wc-vendored-types';
|
|
10
|
+
/** A regular file node: `{ file: { contents } }`. */
|
|
11
|
+
export interface FileNode {
|
|
12
|
+
file: {
|
|
13
|
+
/** UTF-8 string or raw bytes. Honors the runtime's 1 MiB/utf8 limits. */
|
|
14
|
+
contents: string | Uint8Array;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A symlink node: `{ file: { symlink } }`. Accepted by the type for drop-in
|
|
19
|
+
* compatibility; the current MemoryFS mount path materializes regular files +
|
|
20
|
+
* directories only, so a symlink node throws an honest "not yet implemented".
|
|
21
|
+
*/
|
|
22
|
+
export interface SymlinkNode {
|
|
23
|
+
file: {
|
|
24
|
+
symlink: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** A directory node: `{ directory: { ...nested tree } }`. */
|
|
28
|
+
export interface DirectoryNode {
|
|
29
|
+
directory: FileSystemTree;
|
|
30
|
+
}
|
|
31
|
+
/** A nested filesystem tree: directory names → file/dir nodes. */
|
|
32
|
+
export interface FileSystemTree {
|
|
33
|
+
[name: string]: FileNode | SymlinkNode | DirectoryNode;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Boot options. `@webcontainer/api` exposes `coep`, `workdirName`, and
|
|
37
|
+
* `forwardPreviewErrors`; we mirror those and add the browser-runtime wiring our
|
|
38
|
+
* in-page orchestrator needs (the bundled worker + WASM URLs + the served
|
|
39
|
+
* dependency closure), which `@webcontainer/api` hides behind its hosted runtime.
|
|
40
|
+
*/
|
|
41
|
+
export interface BootOptions {
|
|
42
|
+
/**
|
|
43
|
+
* COEP policy hint, mirroring @webcontainer/api. Informational here — the
|
|
44
|
+
* cross-origin isolation headers are set by whoever serves the host page; the
|
|
45
|
+
* facade asserts `crossOriginIsolated` at boot.
|
|
46
|
+
*/
|
|
47
|
+
coep?: 'require-corp' | 'credentialless' | 'none';
|
|
48
|
+
/** Name of the working directory (default 'proj'); `workdir` becomes `/<name>`. */
|
|
49
|
+
workdirName?: string;
|
|
50
|
+
/** Reserved for parity with @webcontainer/api; currently informational. */
|
|
51
|
+
forwardPreviewErrors?: boolean | 'exceptions-only';
|
|
52
|
+
/**
|
|
53
|
+
* Runtime wiring (browser-specific; not in @webcontainer/api, whose runtime is
|
|
54
|
+
* hosted). The host page supplies the bundled Vite-serve worker URL, the WASM
|
|
55
|
+
* override URLs, and the pre-mounted dependency closure so the facade stays
|
|
56
|
+
* browser-pure (no Node `fs`). The start.dev adapter fills these from its
|
|
57
|
+
* served artifact.
|
|
58
|
+
*/
|
|
59
|
+
runtime?: RuntimeWiring;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The browser-runtime wiring the facade needs to boot the real Vite worker. The
|
|
63
|
+
* host page (or the start.dev adapter) provides these; the facade does not read
|
|
64
|
+
* the host filesystem itself.
|
|
65
|
+
*/
|
|
66
|
+
export interface RuntimeWiring {
|
|
67
|
+
/** URL of the bundled `vite-serve.worker.ts` (served same-origin, ESM). */
|
|
68
|
+
viteWorkerUrl: string;
|
|
69
|
+
/**
|
|
70
|
+
* URL of the bundled `streaming-process-host.worker.ts` (same-origin, ESM). This
|
|
71
|
+
* is the REAL Node process worker the command resolver uses for `node <file>`,
|
|
72
|
+
* `npm run <script>`, `npx <bin>`, and direct local bins — it runs the unmodified
|
|
73
|
+
* entry/bin on the node-compat module loader over the mounted MemoryFS and streams
|
|
74
|
+
* its real stdout/stderr/exit. Optional: when absent, those commands throw an
|
|
75
|
+
* honest "process worker not wired" error rather than faking output. (The
|
|
76
|
+
* dev-server path — `npm run dev` / `vite` — uses `viteWorkerUrl` instead.)
|
|
77
|
+
*/
|
|
78
|
+
processWorkerUrl?: string;
|
|
79
|
+
/**
|
|
80
|
+
* URL of the bundled shell worker (same-origin, ESM) that runs the REAL @wc/shell
|
|
81
|
+
* (vercel-labs/just-bash) over the mounted MemoryFS. Used to back `spawn('jsh')`
|
|
82
|
+
* and `spawn('bash')` (jsh is StackBlitz's shell name; we substitute just-bash —
|
|
83
|
+
* a real AST-based bash interpreter). Optional: when absent, `jsh`/`bash` throw an
|
|
84
|
+
* honest "shell worker not wired" error rather than faking a prompt.
|
|
85
|
+
*/
|
|
86
|
+
shellWorkerUrl?: string;
|
|
87
|
+
/** URL of the substrate Service Worker script (served under the preview base). */
|
|
88
|
+
serviceWorkerUrl: string;
|
|
89
|
+
/** Preview base path the SW is scoped to + Vite serves under (e.g. '/preview/'). */
|
|
90
|
+
previewBase: string;
|
|
91
|
+
/** URL of the served `esbuild.wasm` (esbuild-wasm needs `initialize({wasmURL})`). */
|
|
92
|
+
esbuildWasmUrl: string;
|
|
93
|
+
/** URL of the served `@rollup/wasm-node` `bindings_wasm_bg.wasm`. */
|
|
94
|
+
rollupWasmUrl: string;
|
|
95
|
+
/** Source of `@rollup/wasm-node/.../bindings_wasm.js` (eval'd in the worker). */
|
|
96
|
+
rollupBindingsSource: string;
|
|
97
|
+
/** Source of `@rollup/wasm-node/.../shared/parseAst.js` (eval'd in the worker). */
|
|
98
|
+
rollupParseAstSharedSource: string;
|
|
99
|
+
/**
|
|
100
|
+
* The pre-mounted dependency closure (vite + rollup + react + react-dom +
|
|
101
|
+
* @vitejs/plugin-react), keyed by VFS path. Mounted into the worker's MemoryFS
|
|
102
|
+
* alongside the user's project files. The host page builds this with
|
|
103
|
+
* `mountPackageClosure` (a Node-side op) and serves it as JSON.
|
|
104
|
+
*/
|
|
105
|
+
dependencyClosure: Record<string, string>;
|
|
106
|
+
}
|
|
107
|
+
/** Options for `mount`. */
|
|
108
|
+
export interface MountOptions {
|
|
109
|
+
/** Mount the tree under this absolute VFS dir (default: `workdir`). */
|
|
110
|
+
mountPoint?: string;
|
|
111
|
+
}
|
|
112
|
+
export type BufferEncoding = 'utf8' | 'utf-8';
|
|
113
|
+
export interface FileSystemAPI {
|
|
114
|
+
/**
|
|
115
|
+
* Read a file. With no encoding, returns a `Uint8Array`; with 'utf8'/'utf-8',
|
|
116
|
+
* returns a string. Mirrors @webcontainer/api's `readFile`.
|
|
117
|
+
*/
|
|
118
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
119
|
+
readFile(path: string, encoding: BufferEncoding): Promise<string>;
|
|
120
|
+
/** Write a file (creates parent dirs). Honors the 1 MiB/utf8 limit (throws on overflow). */
|
|
121
|
+
writeFile(path: string, data: string | Uint8Array, options?: {
|
|
122
|
+
encoding?: BufferEncoding | null;
|
|
123
|
+
} | BufferEncoding | null): Promise<void>;
|
|
124
|
+
/** List a directory. `{ withFileTypes: true }` returns Dirent-like entries. */
|
|
125
|
+
readdir(path: string): Promise<string[]>;
|
|
126
|
+
readdir(path: string, options: {
|
|
127
|
+
withFileTypes: true;
|
|
128
|
+
}): Promise<DirEnt[]>;
|
|
129
|
+
readdir(path: string, options: {
|
|
130
|
+
encoding?: BufferEncoding;
|
|
131
|
+
withFileTypes?: false;
|
|
132
|
+
}): Promise<string[]>;
|
|
133
|
+
/** Create a directory. `{ recursive: true }` creates parents. */
|
|
134
|
+
mkdir(path: string, options?: {
|
|
135
|
+
recursive?: boolean;
|
|
136
|
+
}): Promise<void>;
|
|
137
|
+
/** Remove a file or directory. `{ recursive: true }` removes a non-empty dir. */
|
|
138
|
+
rm(path: string, options?: {
|
|
139
|
+
force?: boolean;
|
|
140
|
+
recursive?: boolean;
|
|
141
|
+
}): Promise<void>;
|
|
142
|
+
/** Rename / move a path. */
|
|
143
|
+
rename(oldPath: string, newPath: string): Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* Watch a path for changes (mirrors @webcontainer/api). Backed by the REAL
|
|
146
|
+
* MemoryFS watch primitive: delivers genuine 'rename'/'change' events for the
|
|
147
|
+
* watched VFS subtree and returns a `{ close() }` watcher handle. The watcher is
|
|
148
|
+
* tracked by the container and closed on `teardown()`.
|
|
149
|
+
*/
|
|
150
|
+
watch(path: string, listener: WatchListener): FsWatcher;
|
|
151
|
+
watch(path: string, options: WatchOptions, listener: WatchListener): FsWatcher;
|
|
152
|
+
watch(path: string, options?: WatchOptions): FsWatcher;
|
|
153
|
+
}
|
|
154
|
+
/** A Dirent-like entry returned by `readdir(path, { withFileTypes: true })`. */
|
|
155
|
+
export interface DirEnt {
|
|
156
|
+
name: string;
|
|
157
|
+
isFile(): boolean;
|
|
158
|
+
isDirectory(): boolean;
|
|
159
|
+
}
|
|
160
|
+
export interface SpawnOptions {
|
|
161
|
+
/** Working directory for the command (default: `workdir`). */
|
|
162
|
+
cwd?: string;
|
|
163
|
+
/** Environment variables merged over the defaults. */
|
|
164
|
+
env?: Record<string, string | number | boolean>;
|
|
165
|
+
/** Initial terminal dimensions (accepted for parity; informational). */
|
|
166
|
+
terminal?: {
|
|
167
|
+
cols: number;
|
|
168
|
+
rows: number;
|
|
169
|
+
};
|
|
170
|
+
/** Output stream as raw bytes instead of decoded strings (parity; default false). */
|
|
171
|
+
output?: boolean;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A spawned process handle. Mirrors @webcontainer/api's `WebContainerProcess`:
|
|
175
|
+
* a string output stream, a string input stream, an exit promise, and `kill()`.
|
|
176
|
+
*/
|
|
177
|
+
export interface WebContainerProcess {
|
|
178
|
+
/** Combined stdout/stderr as decoded strings. */
|
|
179
|
+
readonly output: ReadableStream<string>;
|
|
180
|
+
/** stdin as decoded strings. */
|
|
181
|
+
readonly input: WritableStream<string>;
|
|
182
|
+
/** Resolves with the process exit code. */
|
|
183
|
+
readonly exit: Promise<number>;
|
|
184
|
+
/** Resize the (virtual) terminal (parity; informational for the dev server). */
|
|
185
|
+
resize?(dimensions: {
|
|
186
|
+
cols: number;
|
|
187
|
+
rows: number;
|
|
188
|
+
}): void;
|
|
189
|
+
/** Terminate the process. */
|
|
190
|
+
kill(): void;
|
|
191
|
+
}
|
|
192
|
+
export type Port = number;
|
|
193
|
+
/** `on('server-ready')` — fired when a dev server starts listening. */
|
|
194
|
+
export type ServerReadyListener = (port: Port, url: string) => void;
|
|
195
|
+
/** Port lifecycle type for `on('port')`. */
|
|
196
|
+
export type PortListenerType = 'open' | 'close';
|
|
197
|
+
/** `on('port')` — fired when a port opens/closes; mirrors @webcontainer/api. */
|
|
198
|
+
export type PortListener = (port: Port, type: PortListenerType, url: string) => void;
|
|
199
|
+
/** `on('error')` — fired on an internal error. */
|
|
200
|
+
export type ErrorListener = (error: {
|
|
201
|
+
message: string;
|
|
202
|
+
}) => void;
|
|
203
|
+
/**
|
|
204
|
+
* `on('preview-message')` — preview-origin postMessage relay. NOT yet
|
|
205
|
+
* implemented; the listener type exists for parity but the facade does not yet
|
|
206
|
+
* forward preview-origin messages (documented stub).
|
|
207
|
+
*/
|
|
208
|
+
export type PreviewMessageListener = (message: unknown) => void;
|
|
209
|
+
/**
|
|
210
|
+
* `on('xdg-open')` — a guest "open this url/file" request, mirroring
|
|
211
|
+
* @webcontainer/api. Accepted for parity (registering a listener never throws);
|
|
212
|
+
* the facade does not yet EMIT it (no guest process surfaces xdg-open yet).
|
|
213
|
+
*/
|
|
214
|
+
export type XdgOpenListener = (url: string) => void;
|
|
215
|
+
/**
|
|
216
|
+
* `on('code')` — a guest "open in editor" request, mirroring @webcontainer/api's
|
|
217
|
+
* IDE integration hook. Accepted for parity (registering a listener never
|
|
218
|
+
* throws); the facade does not yet EMIT it.
|
|
219
|
+
*/
|
|
220
|
+
export type CodeListener = (path: string) => void;
|
|
221
|
+
export interface WebContainerEventMap {
|
|
222
|
+
'server-ready': ServerReadyListener;
|
|
223
|
+
port: PortListener;
|
|
224
|
+
error: ErrorListener;
|
|
225
|
+
'preview-message': PreviewMessageListener;
|
|
226
|
+
/** Parity hook (never emitted yet); registering a listener never throws. */
|
|
227
|
+
'xdg-open': XdgOpenListener;
|
|
228
|
+
/** Parity hook (never emitted yet); registering a listener never throws. */
|
|
229
|
+
code: CodeListener;
|
|
230
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
self.addEventListener('install', (event) => {
|
|
2
|
+
event.waitUntil(self.skipWaiting());
|
|
3
|
+
});
|
|
4
|
+
|
|
5
|
+
self.addEventListener('activate', (event) => {
|
|
6
|
+
event.waitUntil(self.clients.claim());
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
let runtimeRequestId = 0;
|
|
10
|
+
|
|
11
|
+
// ── Navigation-proxy opt-in (Next App Router document-level hydration) ──
|
|
12
|
+
// The Vite preview injects a module entry into its own bootstrap document and
|
|
13
|
+
// RELIES on navigations being bypassed (return fetch). The Next App Router calls
|
|
14
|
+
// hydrateRoot(document, …) — it hydrates the WHOLE document — so the iframe
|
|
15
|
+
// document must BE Next's real SSR HTML, which means the navigation itself has to
|
|
16
|
+
// be served by the runtime (not the static bootstrap shell). This is enabled ONLY
|
|
17
|
+
// when the SW script was registered with the explicit ?navProxy=1 marker, so the
|
|
18
|
+
// Vite registration (plain runtime-sw.js) keeps the byte-identical navigate-bypass.
|
|
19
|
+
const NAV_PROXY_ENABLED = (() => {
|
|
20
|
+
try {
|
|
21
|
+
return new URL(self.location.href).searchParams.get('navProxy') === '1';
|
|
22
|
+
} catch (_) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
26
|
+
|
|
27
|
+
// The window client (the parent page that owns the runtime worker + dispatch glue)
|
|
28
|
+
// that relays runtime HTTP requests for NAVIGATIONS. A navigation has no controlling
|
|
29
|
+
// client yet (the document is mid-creation), so the SW cannot post to event.clientId.
|
|
30
|
+
// The parent page registers itself here via a wc:nav-relay-register message; the SW
|
|
31
|
+
// then routes navigation requests to it, exactly like a subresource but flagged
|
|
32
|
+
// navigate:true so the parent can frame the response as a full document.
|
|
33
|
+
let navRelayClientId = null;
|
|
34
|
+
|
|
35
|
+
self.addEventListener('message', (event) => {
|
|
36
|
+
const data = event.data;
|
|
37
|
+
if (data && data.type === 'wc:nav-relay-register' && event.source && event.source.id) {
|
|
38
|
+
navRelayClientId = event.source.id;
|
|
39
|
+
if (event.ports && event.ports[0]) {
|
|
40
|
+
try { event.ports[0].postMessage({ type: 'wc:nav-relay-registered' }); } catch (_) {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Response headers that describe the ON-THE-WIRE transfer framing of the routed
|
|
46
|
+
// response. The runtime worker buffers the whole body and hands us the decoded
|
|
47
|
+
// bytes, so re-advertising chunked/gzip/length from the upstream Next response would
|
|
48
|
+
// make the browser's native DOCUMENT loader mis-frame the navigation (observed as
|
|
49
|
+
// net::ERR_FAILED / net::ERR_CONTENT_LENGTH_MISMATCH). Strip them for navigations so
|
|
50
|
+
// the browser frames the already-decoded body itself.
|
|
51
|
+
function stripTransferFramingHeaders(headers) {
|
|
52
|
+
if (!Array.isArray(headers)) return headers;
|
|
53
|
+
return headers.filter((header) => {
|
|
54
|
+
if (!Array.isArray(header) || typeof header[0] !== 'string') return true;
|
|
55
|
+
const name = header[0].toLowerCase();
|
|
56
|
+
return name !== 'transfer-encoding' && name !== 'content-encoding' && name !== 'content-length';
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function responseFromMessage(message, expectedId, stripFraming) {
|
|
61
|
+
if (!message || !(message.type === 'wc:runtime-http-response') || message.id !== expectedId) {
|
|
62
|
+
throw new Error('Unexpected WebContainers runtime HTTP response for ' + expectedId);
|
|
63
|
+
}
|
|
64
|
+
if (!(message.status === 101 || (message.status >= 200 && message.status <= 599))) {
|
|
65
|
+
throw new Error('ERR_WC_SERVICE_WORKER_RESPONSE_STATUS_UNSUPPORTED: Runtime Service Worker routing received unsupported response status ' + message.status + ' for ' + expectedId + '. Refusing to construct a browser Response outside the currently proven 101/200-599 status subset; do not claim arbitrary Fetch Response construction, browser status normalization, or broad Service Worker response serialization parity until automated browser smoke proves those semantics.');
|
|
66
|
+
}
|
|
67
|
+
if (typeof ReadableStream !== 'undefined' && message.body instanceof ReadableStream) {
|
|
68
|
+
throw new Error('ERR_WC_SERVICE_WORKER_RESPONSE_STREAM_UNSUPPORTED: Runtime Service Worker routing streaming response bodies and backpressure are not implemented. Send string, ArrayBuffer, Blob, FormData, URLSearchParams, or null response bodies only; do not claim response streaming parity, browser UX parity, production Service Worker deployment readiness, or broad Service Worker routing parity until automated browser smoke proves streaming and cleanup semantics.');
|
|
69
|
+
}
|
|
70
|
+
const body = message.body;
|
|
71
|
+
const supportedBody = body === undefined || body === null || typeof body === 'string' || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || (typeof Blob !== 'undefined' && body instanceof Blob) || (typeof FormData !== 'undefined' && body instanceof FormData) || (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams);
|
|
72
|
+
if (!supportedBody) {
|
|
73
|
+
throw new Error('ERR_WC_SERVICE_WORKER_RESPONSE_BODY_UNSUPPORTED: Runtime Service Worker routing received an object response body for ' + expectedId + '. Send string, ArrayBuffer, Blob, FormData, URLSearchParams, or null response bodies only; do not claim arbitrary response body serialization, implicit body coercion, Fetch BodyInit parity, or broad Service Worker response parity until automated browser smoke proves those semantics.');
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(message.headers) && message.headers.some((header) => !Array.isArray(header) || typeof header[0] !== 'string' || typeof header[1] !== 'string')) {
|
|
76
|
+
throw new Error('ERR_WC_SERVICE_WORKER_RESPONSE_HEADERS_UNSUPPORTED: Runtime Service Worker routing received a non-string routed response header for ' + expectedId + '. Refusing to coerce header names or values through browser Headers; do not claim arbitrary response header serialization, implicit header coercion, Fetch header normalization, or broad Service Worker response parity until automated browser smoke proves those semantics.');
|
|
77
|
+
}
|
|
78
|
+
if (Array.isArray(message.headers) && message.headers.some((header) => header[0].toLowerCase() === 'set-cookie')) {
|
|
79
|
+
throw new Error('ERR_WC_SERVICE_WORKER_RESPONSE_HEADERS_UNSUPPORTED: Set-Cookie response headers are not represented by the current Service Worker routing protocol. Refusing to construct a Response that could collapse, expose, or misrepresent forbidden response headers; do not claim multi-value Set-Cookie, forbidden response-header filtering, browser cookie, Fetch, or broad Service Worker header parity until automated browser smoke proves those semantics.');
|
|
80
|
+
}
|
|
81
|
+
return new Response(message.body ?? null, {
|
|
82
|
+
status: message.status,
|
|
83
|
+
statusText: message.statusText,
|
|
84
|
+
headers: stripFraming ? stripTransferFramingHeaders(message.headers) : message.headers,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function requestBodySerializationError(request, cause) {
|
|
89
|
+
const causeText = cause && cause.name ? cause.name + ': ' + cause.message : String(cause);
|
|
90
|
+
return new Error('ERR_WC_SERVICE_WORKER_REQUEST_BODY_UNSUPPORTED: Runtime Service Worker routing could not clone or serialize the request body for ' + request.method + ' ' + request.url + '. The request body has already been consumed, is locked, or cannot be represented by the current string/ArrayBuffer routing subset; do not claim arbitrary request cloning/body serialization parity, streamed upload parity, browser UX parity, production Service Worker deployment readiness, or broad Service Worker routing parity until automated browser smoke proves those semantics. Cause: ' + causeText);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function requestRedirectModeUnsupportedError(request) {
|
|
94
|
+
return new Error('ERR_WC_SERVICE_WORKER_REQUEST_REDIRECT_UNSUPPORTED: Runtime Service Worker routing only serializes requests with redirect mode follow. Refusing ' + request.method + ' ' + request.url + ' because redirect mode ' + request.redirect + ' is not represented by the current request/response protocol; do not claim redirect handling parity, browser UX parity, production Service Worker deployment readiness, or broad Service Worker routing parity until automated browser smoke proves manual/error redirect semantics.');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function requestCacheModeUnsupportedError(request) {
|
|
98
|
+
return new Error('ERR_WC_SERVICE_WORKER_REQUEST_CACHE_UNSUPPORTED: Runtime Service Worker routing only serializes requests with cache mode default. Refusing ' + request.method + ' ' + request.url + ' because cache mode ' + request.cache + ' is not represented by the current request/response protocol; do not claim browser cache routing parity, Cache API parity, offline readiness, browser UX parity, production Service Worker deployment readiness, or broad Service Worker routing parity until automated browser smoke proves cache-mode semantics.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function requestModeUnsupportedError(request) {
|
|
102
|
+
return new Error('ERR_WC_SERVICE_WORKER_REQUEST_MODE_UNSUPPORTED: Runtime Service Worker routing does not serialize no-cors opaque request semantics. Refusing ' + request.method + ' ' + request.url + ' because request mode ' + request.mode + ' is not represented by the current request/response protocol; do not claim opaque request, browser fetch mode, cross-origin preview routing, or broad Service Worker routing parity until automated browser smoke proves these semantics.');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function requestCredentialsUnsupportedError(request) {
|
|
106
|
+
return new Error('ERR_WC_SERVICE_WORKER_REQUEST_CREDENTIALS_UNSUPPORTED: Runtime Service Worker routing does not serialize credentials mode omit. Refusing ' + request.method + ' ' + request.url + ' because credentials mode ' + request.credentials + ' is not represented by the current request/response protocol; do not claim browser cookie, credential forwarding, Fetch credentials, cross-origin preview routing, or broad Service Worker request serialization parity until automated browser smoke proves these semantics.');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function serviceWorkerPostMessageUnsupportedError(request, cause) {
|
|
110
|
+
const causeText = cause && cause.name ? cause.name + ': ' + cause.message : String(cause);
|
|
111
|
+
return new Error('ERR_WC_SERVICE_WORKER_POSTMESSAGE_UNSUPPORTED: Runtime Service Worker routing failed to post the serialized request for ' + request.method + ' ' + request.url + ' to the controlling client. Service Worker routing transfer lists must contain the response MessagePort and any ArrayBuffer body as valid transferables; do not claim Service Worker routing transfer-list readiness, arbitrary request/response serialization parity, browser UX parity, production Service Worker deployment readiness, or broad Service Worker routing parity until automated browser smoke proves this boundary. Cause: ' + causeText);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function assertSupportedRequestRedirectMode(request) {
|
|
115
|
+
if (request.redirect !== 'follow') throw requestRedirectModeUnsupportedError(request);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function assertSupportedRequestCacheMode(request) {
|
|
119
|
+
if (request.cache !== 'default') throw requestCacheModeUnsupportedError(request);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isSameOriginRequest(request) {
|
|
123
|
+
try {
|
|
124
|
+
return new URL(request.url).origin === self.location.origin;
|
|
125
|
+
} catch (_) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function assertSupportedRequestMode(request) {
|
|
131
|
+
// no-cors is only problematic CROSS-origin, where the response would be opaque
|
|
132
|
+
// and unreadable. For SAME-origin requests (every preview subresource — the
|
|
133
|
+
// browser issues classic <script src> chunk loads with mode 'no-cors') the
|
|
134
|
+
// response is a normal, fully-readable response, so routing it is correct and
|
|
135
|
+
// not an opaque/cross-origin claim. Refuse only cross-origin no-cors.
|
|
136
|
+
if (request.mode === 'no-cors' && !isSameOriginRequest(request)) throw requestModeUnsupportedError(request);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function assertSupportedRequestCredentials(request) {
|
|
140
|
+
if (request.credentials === 'omit') throw requestCredentialsUnsupportedError(request);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function serializeRequestBody(request) {
|
|
144
|
+
const hasRequestBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
145
|
+
if (!hasRequestBody) return {};
|
|
146
|
+
const contentType = request.headers.get('content-type') || '';
|
|
147
|
+
const useTextBody = contentType === '' || /^(text\/)|json|javascript|xml|x-www-form-urlencoded/.test(contentType);
|
|
148
|
+
try {
|
|
149
|
+
if (request.bodyUsed) throw new Error('request body has already been consumed before Service Worker routing serialization');
|
|
150
|
+
const body = useTextBody ? await request.clone().text() : await request.clone().arrayBuffer();
|
|
151
|
+
return useTextBody ? { body } : { body, bodyEncoding: 'arrayBuffer' };
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw requestBodySerializationError(request, error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Route a fetch event through a window client over the MessagePort protocol. The
|
|
158
|
+
// same path serves subresources (client = the iframe itself) and — when navigation
|
|
159
|
+
// proxying is opted in — the iframe NAVIGATION (client = the registered nav relay,
|
|
160
|
+
// isNavigate=true so we strip on-the-wire framing headers and tell the relay this is
|
|
161
|
+
// a document request).
|
|
162
|
+
async function routeThroughClient(event, client, isNavigate) {
|
|
163
|
+
const id = String(++runtimeRequestId);
|
|
164
|
+
const channel = new MessageChannel();
|
|
165
|
+
let serializedRequestBody;
|
|
166
|
+
try {
|
|
167
|
+
// A navigation request intercepted by a Service Worker carries redirect mode
|
|
168
|
+
// 'manual' (the browser handles document redirects itself), request mode
|
|
169
|
+
// 'navigate', and — for a reload navigation — cache mode 'reload'/'no-cache'.
|
|
170
|
+
// All three are correct for a document load and must not be refused by the
|
|
171
|
+
// subresource-oriented guards (refusing a reload navigation rejects respondWith →
|
|
172
|
+
// net::ERR_FAILED → the iframe lands on a browser error page). We still serialize
|
|
173
|
+
// the body normally (a navigation is GET, so there is none). Subresources keep the
|
|
174
|
+
// full guard set unchanged.
|
|
175
|
+
if (!isNavigate) {
|
|
176
|
+
assertSupportedRequestRedirectMode(event.request);
|
|
177
|
+
assertSupportedRequestMode(event.request);
|
|
178
|
+
assertSupportedRequestCacheMode(event.request);
|
|
179
|
+
}
|
|
180
|
+
assertSupportedRequestCredentials(event.request);
|
|
181
|
+
serializedRequestBody = await serializeRequestBody(event.request);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
channel.port1.close();
|
|
184
|
+
channel.port2.close();
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
const body = serializedRequestBody.body;
|
|
188
|
+
const bodyEncoding = serializedRequestBody.bodyEncoding;
|
|
189
|
+
const response = new Promise((resolve, reject) => {
|
|
190
|
+
channel.port1.onmessage = (portEvent) => {
|
|
191
|
+
try {
|
|
192
|
+
resolve(responseFromMessage(portEvent.data, id, isNavigate));
|
|
193
|
+
} catch (error) {
|
|
194
|
+
reject(error);
|
|
195
|
+
} finally {
|
|
196
|
+
channel.port1.close();
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
channel.port1.start();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const transfer = bodyEncoding === 'arrayBuffer' ? [channel.port2, body] : [channel.port2];
|
|
203
|
+
try {
|
|
204
|
+
client.postMessage({
|
|
205
|
+
type: 'wc:runtime-http-request',
|
|
206
|
+
id,
|
|
207
|
+
url: event.request.url,
|
|
208
|
+
method: event.request.method,
|
|
209
|
+
headers: [...event.request.headers.entries()],
|
|
210
|
+
body,
|
|
211
|
+
bodyEncoding,
|
|
212
|
+
navigate: isNavigate === true,
|
|
213
|
+
}, transfer);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
channel.port1.close();
|
|
216
|
+
channel.port2.close();
|
|
217
|
+
throw serviceWorkerPostMessageUnsupportedError(event.request, error);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return response;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
self.addEventListener('fetch', (event) => {
|
|
224
|
+
const clientId = event.clientId || event.resultingClientId;
|
|
225
|
+
event.respondWith((async () => {
|
|
226
|
+
const isNavigate = event.request.mode === 'navigate';
|
|
227
|
+
|
|
228
|
+
if (isNavigate && !NAV_PROXY_ENABLED) {
|
|
229
|
+
// Vite (and every non-opted-in registration) keeps the byte-identical bypass:
|
|
230
|
+
// its bootstrap shell is served statically and it injects its own module entry.
|
|
231
|
+
return fetch(event.request);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (NAV_PROXY_ENABLED) {
|
|
235
|
+
// In nav-proxy mode the iframe document IS the runtime's real HTML (so React can
|
|
236
|
+
// hydrate the whole document), which means it carries no relay script. Route BOTH
|
|
237
|
+
// the navigation AND every subresource it then requests through the registered
|
|
238
|
+
// nav relay (the parent page that owns the runtime worker). Navigations strip the
|
|
239
|
+
// on-the-wire framing headers so the native document loader frames the body.
|
|
240
|
+
let inScope = false;
|
|
241
|
+
try {
|
|
242
|
+
const reqUrl = new URL(event.request.url);
|
|
243
|
+
const scopePath = self.registration.scope.indexOf(self.location.origin) === 0
|
|
244
|
+
? self.registration.scope.slice(self.location.origin.length)
|
|
245
|
+
: self.registration.scope;
|
|
246
|
+
inScope = reqUrl.origin === self.location.origin && reqUrl.pathname.indexOf(scopePath) === 0;
|
|
247
|
+
} catch (_) {
|
|
248
|
+
inScope = false;
|
|
249
|
+
}
|
|
250
|
+
if (!inScope) return fetch(event.request);
|
|
251
|
+
const relay = navRelayClientId ? await self.clients.get(navRelayClientId) : undefined;
|
|
252
|
+
if (!relay) {
|
|
253
|
+
// No relay registered yet: navigations must not silently fall through to the
|
|
254
|
+
// static shell (that would defeat document-level hydration), but a subresource
|
|
255
|
+
// can still try the controlling client.
|
|
256
|
+
if (isNavigate) return fetch(event.request);
|
|
257
|
+
const fallback = clientId ? await self.clients.get(clientId) : undefined;
|
|
258
|
+
if (!fallback) return fetch(event.request);
|
|
259
|
+
return routeThroughClient(event, fallback, false);
|
|
260
|
+
}
|
|
261
|
+
return routeThroughClient(event, relay, isNavigate);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const client = clientId ? await self.clients.get(clientId) : undefined;
|
|
265
|
+
if (!client) return fetch(event.request);
|
|
266
|
+
|
|
267
|
+
return routeThroughClient(event, client, false);
|
|
268
|
+
})());
|
|
269
|
+
});
|