@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,1805 @@
|
|
|
1
|
+
// ../fs/src/index.ts
|
|
2
|
+
var now = () => Date.now();
|
|
3
|
+
var clean = (path) => {
|
|
4
|
+
const parts = [];
|
|
5
|
+
for (const part of path.replace(/\\+/g, "/").split("/")) {
|
|
6
|
+
if (!part || part === ".")
|
|
7
|
+
continue;
|
|
8
|
+
if (part === "..") {
|
|
9
|
+
parts.pop();
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
parts.push(part);
|
|
13
|
+
}
|
|
14
|
+
return `/${parts.join("/")}`;
|
|
15
|
+
};
|
|
16
|
+
var basename = (path) => clean(path).split("/").pop() ?? "";
|
|
17
|
+
var dirname = (path) => clean(path).split("/").slice(0, -1).join("/") || "/";
|
|
18
|
+
var bytesToBase64 = (bytes) => {
|
|
19
|
+
let binary = "";
|
|
20
|
+
for (const byte of bytes)
|
|
21
|
+
binary += String.fromCharCode(byte);
|
|
22
|
+
return btoa(binary);
|
|
23
|
+
};
|
|
24
|
+
var base64ToBytes = (content) => {
|
|
25
|
+
const binary = atob(content);
|
|
26
|
+
const bytes = new Uint8Array(binary.length);
|
|
27
|
+
for (let i = 0;i < binary.length; i += 1)
|
|
28
|
+
bytes[i] = binary.charCodeAt(i);
|
|
29
|
+
return bytes;
|
|
30
|
+
};
|
|
31
|
+
var isPlainRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
32
|
+
class MemoryFS {
|
|
33
|
+
root = { kind: "dir", children: new Map, mtime: now() };
|
|
34
|
+
watchers = new Set;
|
|
35
|
+
suppressWatchEvents = false;
|
|
36
|
+
pendingWatchEvents = new Map;
|
|
37
|
+
watchFlushQueued = false;
|
|
38
|
+
constructor(seed = {}) {
|
|
39
|
+
for (const [p, c] of Object.entries(seed))
|
|
40
|
+
this.writeFile(p, c);
|
|
41
|
+
}
|
|
42
|
+
walk(path, create = false, followFinal = true, seen = new Set) {
|
|
43
|
+
const requestedPath = clean(path);
|
|
44
|
+
const parts = requestedPath.split("/").filter(Boolean);
|
|
45
|
+
let cur = this.root;
|
|
46
|
+
let currentPath = "";
|
|
47
|
+
for (let index = 0;index < parts.length; index += 1) {
|
|
48
|
+
const part = parts[index];
|
|
49
|
+
currentPath = `${currentPath}/${part}`;
|
|
50
|
+
if (cur.kind !== "dir")
|
|
51
|
+
throw new Error(`ENOTDIR: ${requestedPath}`);
|
|
52
|
+
let next = cur.children.get(part);
|
|
53
|
+
if (!next) {
|
|
54
|
+
if (!create)
|
|
55
|
+
throw new Error(`ENOENT: ${requestedPath}`);
|
|
56
|
+
next = { kind: "dir", children: new Map, mtime: now() };
|
|
57
|
+
cur.children.set(part, next);
|
|
58
|
+
}
|
|
59
|
+
const isFinal = index === parts.length - 1;
|
|
60
|
+
if (next.kind === "symlink" && (followFinal || !isFinal)) {
|
|
61
|
+
if (seen.has(currentPath))
|
|
62
|
+
throw new Error(`ELOOP: ${requestedPath}`);
|
|
63
|
+
seen.add(currentPath);
|
|
64
|
+
const remaining = parts.slice(index + 1).join("/");
|
|
65
|
+
const target = this.resolveSymlinkTarget(dirname(currentPath), next.target);
|
|
66
|
+
return this.walk(remaining ? `${target}/${remaining}` : target, false, followFinal, seen);
|
|
67
|
+
}
|
|
68
|
+
cur = next;
|
|
69
|
+
}
|
|
70
|
+
return cur;
|
|
71
|
+
}
|
|
72
|
+
mkdirp(path) {
|
|
73
|
+
const p = clean(path);
|
|
74
|
+
const parts = p.split("/").filter(Boolean);
|
|
75
|
+
let currentPath = "";
|
|
76
|
+
for (const part of parts) {
|
|
77
|
+
currentPath = `${currentPath}/${part}`;
|
|
78
|
+
const existed = this.exists(currentPath);
|
|
79
|
+
this.walk(currentPath, true);
|
|
80
|
+
if (!existed)
|
|
81
|
+
this.emitWatch(currentPath, "rename");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
writeFile(path, content) {
|
|
85
|
+
const p = clean(path);
|
|
86
|
+
const dirs = p.split("/").slice(0, -1).join("/") || "/";
|
|
87
|
+
const name = p.split("/").pop();
|
|
88
|
+
const existed = this.exists(p);
|
|
89
|
+
if (!this.exists(dirs))
|
|
90
|
+
this.mkdirp(dirs);
|
|
91
|
+
const dir = this.walk(dirs);
|
|
92
|
+
if (dir.kind !== "dir")
|
|
93
|
+
throw new Error(`ENOTDIR: ${dirs}`);
|
|
94
|
+
const current = dir.children.get(name);
|
|
95
|
+
if (current?.kind === "dir")
|
|
96
|
+
throw new Error(`EISDIR: ${p}`);
|
|
97
|
+
dir.children.set(name, { kind: "file", content: this.cloneContent(content), mtime: now() });
|
|
98
|
+
this.emitWatch(p, existed ? "change" : "rename");
|
|
99
|
+
}
|
|
100
|
+
symlink(target, path) {
|
|
101
|
+
const p = clean(path);
|
|
102
|
+
const dirs = dirname(p);
|
|
103
|
+
const name = basename(p);
|
|
104
|
+
if (!this.exists(dirs))
|
|
105
|
+
this.mkdirp(dirs);
|
|
106
|
+
const dir = this.walk(dirs);
|
|
107
|
+
if (dir.kind !== "dir")
|
|
108
|
+
throw new Error(`ENOTDIR: ${dirs}`);
|
|
109
|
+
if (dir.children.has(name))
|
|
110
|
+
throw new Error(`EEXIST: ${p}`);
|
|
111
|
+
dir.children.set(name, { kind: "symlink", target, mtime: now() });
|
|
112
|
+
dir.mtime = now();
|
|
113
|
+
this.emitWatch(p, "rename");
|
|
114
|
+
}
|
|
115
|
+
readlink(path) {
|
|
116
|
+
const entry = this.walk(path, false, false);
|
|
117
|
+
if (entry.kind !== "symlink")
|
|
118
|
+
throw new Error(`EINVAL: ${path}`);
|
|
119
|
+
return entry.target;
|
|
120
|
+
}
|
|
121
|
+
chmod(path, _mode) {
|
|
122
|
+
this.walk(path, false, false);
|
|
123
|
+
throw new Error(`ERR_WC_FS_PERMISSIONS_UNSUPPORTED: chmod(${clean(path)}) is not implemented by MemoryFS. Mode bits, ownership, ACLs, executable bits, umask, and POSIX permission enforcement remain unsupported; this guard prevents claiming Node/POSIX filesystem parity.`);
|
|
124
|
+
}
|
|
125
|
+
chown(path, _uid, _gid) {
|
|
126
|
+
this.walk(path, false, false);
|
|
127
|
+
throw new Error(`ERR_WC_FS_PERMISSIONS_UNSUPPORTED: chown(${clean(path)}) is not implemented by MemoryFS. uid/gid ownership, groups, ACLs, and POSIX permission enforcement remain unsupported; this guard prevents claiming Node/POSIX filesystem parity.`);
|
|
128
|
+
}
|
|
129
|
+
lstat(path) {
|
|
130
|
+
const entry = this.walk(path, false, false);
|
|
131
|
+
return { kind: entry.kind, mtime: entry.mtime };
|
|
132
|
+
}
|
|
133
|
+
mount(path, files) {
|
|
134
|
+
const mountPath = clean(path);
|
|
135
|
+
const normalizedFiles = new Map;
|
|
136
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
137
|
+
const normalizedPath = this.normalizeMountRelativePath(relativePath);
|
|
138
|
+
if (normalizedFiles.has(normalizedPath)) {
|
|
139
|
+
throw new Error(`EINVAL: duplicate mount path after normalization: ${relativePath}`);
|
|
140
|
+
}
|
|
141
|
+
normalizedFiles.set(normalizedPath, content);
|
|
142
|
+
}
|
|
143
|
+
const existingMountEntry = this.getEntryNoFollow(mountPath);
|
|
144
|
+
if (existingMountEntry?.kind === "symlink") {
|
|
145
|
+
throw new Error(`ERR_WC_FS_MOUNT_SYMLINK_UNSUPPORTED: mounting onto a symlink is not implemented by MemoryFS: ${mountPath}. The current mount evidence only covers replacing concrete file/directory entries at the mount path; realpath resolution, mount namespaces, bind mounts, symlink overwrite semantics, and full Node/POSIX filesystem parity remain unsupported, so mount is rejected before mutating the tree.`);
|
|
146
|
+
}
|
|
147
|
+
const symlinkComponent = this.findSymlinkPathComponent(mountPath);
|
|
148
|
+
if (symlinkComponent) {
|
|
149
|
+
throw new Error(`ERR_WC_FS_MOUNT_SYMLINK_COMPONENT_UNSUPPORTED: mount paths containing symlink components are not implemented by MemoryFS: ${mountPath} crosses ${symlinkComponent}. The current mount evidence only covers concrete virtual directory components; realpath traversal, bind mounts, symlink component resolution, host filesystem semantics, and full Node/POSIX filesystem parity remain unsupported, so mount is rejected before mutating the tree.`);
|
|
150
|
+
}
|
|
151
|
+
if (existingMountEntry) {
|
|
152
|
+
if (existingMountEntry.kind === "dir")
|
|
153
|
+
this.rmdir(mountPath);
|
|
154
|
+
else
|
|
155
|
+
this.unlink(mountPath);
|
|
156
|
+
}
|
|
157
|
+
this.mkdirp(mountPath);
|
|
158
|
+
for (const [relativePath, content] of normalizedFiles) {
|
|
159
|
+
this.writeFile(`${mountPath}/${relativePath}`, content);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
unmount(path) {
|
|
163
|
+
this.rmdir(path);
|
|
164
|
+
}
|
|
165
|
+
readFile(path) {
|
|
166
|
+
const e = this.walk(path);
|
|
167
|
+
if (e.kind !== "file")
|
|
168
|
+
throw new Error(`EISDIR: ${path}`);
|
|
169
|
+
const content = e.content ?? "";
|
|
170
|
+
return typeof content === "string" ? content : new TextDecoder().decode(content);
|
|
171
|
+
}
|
|
172
|
+
readFileBuffer(path) {
|
|
173
|
+
const e = this.walk(path);
|
|
174
|
+
if (e.kind !== "file")
|
|
175
|
+
throw new Error(`EISDIR: ${path}`);
|
|
176
|
+
const content = e.content ?? "";
|
|
177
|
+
return typeof content === "string" ? new TextEncoder().encode(content) : new Uint8Array(content);
|
|
178
|
+
}
|
|
179
|
+
unlink(path) {
|
|
180
|
+
const p = clean(path);
|
|
181
|
+
const dirs = p.split("/").slice(0, -1).join("/") || "/";
|
|
182
|
+
const name = p.split("/").pop();
|
|
183
|
+
const dir = this.walk(dirs);
|
|
184
|
+
if (dir.kind !== "dir")
|
|
185
|
+
throw new Error(`ENOTDIR: ${dirs}`);
|
|
186
|
+
const entry = dir.children.get(name);
|
|
187
|
+
if (!entry)
|
|
188
|
+
throw new Error(`ENOENT: ${path}`);
|
|
189
|
+
if (entry.kind === "dir")
|
|
190
|
+
throw new Error(`EISDIR: ${path}`);
|
|
191
|
+
dir.children.delete(name);
|
|
192
|
+
dir.mtime = now();
|
|
193
|
+
this.emitWatch(p, "rename");
|
|
194
|
+
this.closeDeletedPathWatchers(p);
|
|
195
|
+
}
|
|
196
|
+
rmdir(path) {
|
|
197
|
+
const p = clean(path);
|
|
198
|
+
if (p === "/")
|
|
199
|
+
throw new Error("EBUSY: /");
|
|
200
|
+
const dirs = p.split("/").slice(0, -1).join("/") || "/";
|
|
201
|
+
const name = p.split("/").pop();
|
|
202
|
+
const dir = this.walk(dirs);
|
|
203
|
+
if (dir.kind !== "dir")
|
|
204
|
+
throw new Error(`ENOTDIR: ${dirs}`);
|
|
205
|
+
const entry = dir.children.get(name);
|
|
206
|
+
if (!entry)
|
|
207
|
+
throw new Error(`ENOENT: ${path}`);
|
|
208
|
+
if (entry.kind !== "dir")
|
|
209
|
+
throw new Error(`ENOTDIR: ${path}`);
|
|
210
|
+
const removedPaths = this.entryPaths(entry);
|
|
211
|
+
dir.children.delete(name);
|
|
212
|
+
dir.mtime = now();
|
|
213
|
+
for (const suffix of removedPaths)
|
|
214
|
+
this.emitWatch(`${p}${suffix}`, "rename");
|
|
215
|
+
this.closeDeletedPathWatchers(p);
|
|
216
|
+
}
|
|
217
|
+
rename(from, to) {
|
|
218
|
+
const src = clean(from);
|
|
219
|
+
const dest = clean(to);
|
|
220
|
+
if (src === "/" || dest === "/")
|
|
221
|
+
throw new Error("EBUSY: /");
|
|
222
|
+
const srcDirPath = dirname(src);
|
|
223
|
+
const srcName = basename(src);
|
|
224
|
+
const destDirPath = dirname(dest);
|
|
225
|
+
const destName = basename(dest);
|
|
226
|
+
const srcDir = this.walk(srcDirPath);
|
|
227
|
+
if (srcDir.kind !== "dir")
|
|
228
|
+
throw new Error(`ENOTDIR: ${srcDirPath}`);
|
|
229
|
+
const entry = srcDir.children.get(srcName);
|
|
230
|
+
if (!entry)
|
|
231
|
+
throw new Error(`ENOENT: ${from}`);
|
|
232
|
+
if (entry.kind === "dir" && this.isDescendant(src, dest))
|
|
233
|
+
throw new Error(`EINVAL: ${src} -> ${dest}`);
|
|
234
|
+
const destDir = this.walk(destDirPath);
|
|
235
|
+
if (destDir.kind !== "dir")
|
|
236
|
+
throw new Error(`ENOTDIR: ${destDirPath}`);
|
|
237
|
+
const existingDest = destDir.children.get(destName);
|
|
238
|
+
if (existingDest && existingDest.kind !== entry.kind)
|
|
239
|
+
throw new Error(`${existingDest.kind === "dir" ? "EISDIR" : "ENOTDIR"}: ${dest}`);
|
|
240
|
+
if (existingDest?.kind === "dir" && existingDest.children.size > 0)
|
|
241
|
+
throw new Error(`ENOTEMPTY: ${dest}`);
|
|
242
|
+
const renamedPaths = this.entryPaths(entry);
|
|
243
|
+
srcDir.children.delete(srcName);
|
|
244
|
+
srcDir.mtime = now();
|
|
245
|
+
destDir.children.set(destName, entry);
|
|
246
|
+
destDir.mtime = now();
|
|
247
|
+
entry.mtime = now();
|
|
248
|
+
for (const suffix of renamedPaths)
|
|
249
|
+
this.emitWatch(`${src}${suffix}`, "rename");
|
|
250
|
+
this.closeDeletedPathWatchers(src);
|
|
251
|
+
for (const suffix of renamedPaths)
|
|
252
|
+
this.emitWatch(`${dest}${suffix}`, "rename");
|
|
253
|
+
}
|
|
254
|
+
exists(path) {
|
|
255
|
+
try {
|
|
256
|
+
this.walk(path);
|
|
257
|
+
return true;
|
|
258
|
+
} catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
list(path = "/") {
|
|
263
|
+
const e = this.walk(path);
|
|
264
|
+
if (e.kind !== "dir")
|
|
265
|
+
throw new Error(`ENOTDIR: ${path}`);
|
|
266
|
+
return [...e.children.entries()].map(([name, entry]) => ({ name, kind: entry.kind, mtime: entry.mtime })).sort((a, b) => a.name.localeCompare(b.name));
|
|
267
|
+
}
|
|
268
|
+
snapshot() {
|
|
269
|
+
const files = {};
|
|
270
|
+
const dirs = [];
|
|
271
|
+
const symlinks = {};
|
|
272
|
+
const visit = (path, e) => {
|
|
273
|
+
if (e.kind === "file") {
|
|
274
|
+
const content = e.content ?? "";
|
|
275
|
+
files[path] = typeof content === "string" ? content : { encoding: "base64", content: bytesToBase64(content) };
|
|
276
|
+
} else if (e.kind === "symlink") {
|
|
277
|
+
symlinks[path] = e.target;
|
|
278
|
+
} else {
|
|
279
|
+
dirs.push(path);
|
|
280
|
+
for (const [n, c] of e.children)
|
|
281
|
+
visit(path === "/" ? `/${n}` : `${path}/${n}`, c);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
visit("/", this.root);
|
|
285
|
+
return { version: 1, files, dirs, symlinks };
|
|
286
|
+
}
|
|
287
|
+
restore(s) {
|
|
288
|
+
this.assertSupportedSnapshot(s);
|
|
289
|
+
const before = this.snapshot();
|
|
290
|
+
this.suppressWatchEvents = true;
|
|
291
|
+
try {
|
|
292
|
+
this.root = { kind: "dir", children: new Map, mtime: now() };
|
|
293
|
+
for (const d of s.dirs)
|
|
294
|
+
this.mkdirp(d);
|
|
295
|
+
for (const [p, c] of Object.entries(s.files))
|
|
296
|
+
this.writeFile(p, typeof c === "string" ? c : base64ToBytes(c.content));
|
|
297
|
+
for (const [p, target] of Object.entries(s.symlinks ?? {}))
|
|
298
|
+
this.symlink(target, p);
|
|
299
|
+
} finally {
|
|
300
|
+
this.suppressWatchEvents = false;
|
|
301
|
+
}
|
|
302
|
+
this.emitSnapshotRestoreWatchEvents(before, s);
|
|
303
|
+
}
|
|
304
|
+
watch(path, optionsOrListener, maybeListener) {
|
|
305
|
+
const options = typeof optionsOrListener === "function" ? {} : optionsOrListener;
|
|
306
|
+
const listener = typeof optionsOrListener === "function" ? optionsOrListener : maybeListener;
|
|
307
|
+
if (!listener)
|
|
308
|
+
throw new Error("watch listener is required");
|
|
309
|
+
if (options.encoding && options.encoding !== "utf8") {
|
|
310
|
+
throw new Error(`ENOTSUP: fs.watch encoding ${options.encoding} is not supported`);
|
|
311
|
+
}
|
|
312
|
+
const watchPath = clean(path);
|
|
313
|
+
const watchedEntryNoFollow = this.walk(watchPath, false, false);
|
|
314
|
+
const watchedEntry = this.walk(watchPath);
|
|
315
|
+
if (options.recursive === true && watchedEntryNoFollow.kind === "symlink") {
|
|
316
|
+
throw new Error(`ERR_WC_FS_RECURSIVE_SYMLINK_WATCH_UNSUPPORTED: recursive fs.watch through symlink targets has no automated evidence in MemoryFS: ${watchPath}. Symlink target traversal, target-vs-link event path identity, symlink retargeting/deletion lifecycle, platform-specific Node fs.watch behavior, POSIX/Linux filesystem behavior, and full Node fs.watch parity remain unsupported, so no watcher is registered.`);
|
|
317
|
+
}
|
|
318
|
+
if (watchedEntryNoFollow.kind === "symlink") {
|
|
319
|
+
throw new Error(`ERR_WC_FS_SYMLINK_WATCH_UNSUPPORTED: fs.watch through symlink targets has no automated evidence in MemoryFS: ${watchPath}. target-vs-link event path identity, symlink retargeting/deletion lifecycle, symlink traversal, platform-specific Node fs.watch behavior, POSIX/Linux filesystem behavior, and full Node fs.watch parity remain unsupported, so no watcher is registered.`);
|
|
320
|
+
}
|
|
321
|
+
if (options.recursive === true && watchedEntry.kind !== "dir") {
|
|
322
|
+
throw new Error(`ERR_WC_FS_RECURSIVE_FILE_WATCH_UNSUPPORTED: recursive fs.watch only has automated evidence for directory trees; ${watchPath} is a ${watchedEntry.kind}. Recursive file-target semantics, platform-specific Node fs.watch behavior, symlink target traversal, and polling watcher parity remain unsupported, so no watcher is registered.`);
|
|
323
|
+
}
|
|
324
|
+
const watcher = { path: watchPath, listener, recursive: options.recursive === true, signal: options.signal };
|
|
325
|
+
if (options.signal?.aborted)
|
|
326
|
+
return { close: () => {} };
|
|
327
|
+
this.watchers.add(watcher);
|
|
328
|
+
watcher.abortHandler = () => this.closeWatcher(watcher);
|
|
329
|
+
options.signal?.addEventListener("abort", watcher.abortHandler, { once: true });
|
|
330
|
+
return { close: watcher.abortHandler };
|
|
331
|
+
}
|
|
332
|
+
watchFile(path, optionsOrListener, maybeListener) {
|
|
333
|
+
const listener = typeof optionsOrListener === "function" ? optionsOrListener : maybeListener;
|
|
334
|
+
if (!listener)
|
|
335
|
+
throw new Error("watchFile listener is required");
|
|
336
|
+
this.walk(path, false, false);
|
|
337
|
+
throw new Error(`ERR_WC_FS_WATCHFILE_UNSUPPORTED: fs.watchFile(${clean(path)}) is not implemented by MemoryFS. The runtime currently supports event-driven fs.watch only for a narrow virtual filesystem subset; polling stat watchers, curr/prev Stats snapshots, interval scheduling, persistent handles, and Node fs.watchFile parity remain unsupported. This guard prevents fake polling watchers or broad Node fs parity claims.`);
|
|
338
|
+
}
|
|
339
|
+
unwatchFile(path, _listener) {
|
|
340
|
+
throw new Error(`ERR_WC_FS_WATCHFILE_UNSUPPORTED: fs.unwatchFile(${clean(path)}) is not implemented by MemoryFS because fs.watchFile polling registrations are unsupported. Polling watcher bookkeeping, listener-specific removal, curr/prev Stats snapshots, and Node fs.watchFile/unwatchFile parity remain unsupported.`);
|
|
341
|
+
}
|
|
342
|
+
emitWatch(path, eventType) {
|
|
343
|
+
if (this.suppressWatchEvents)
|
|
344
|
+
return;
|
|
345
|
+
const p = clean(path);
|
|
346
|
+
const matchingWatchers = new Set([...this.watchers].filter((watcher) => this.watchMatches(watcher, p)));
|
|
347
|
+
if (matchingWatchers.size === 0)
|
|
348
|
+
return;
|
|
349
|
+
const key = `${p}\x00${eventType}`;
|
|
350
|
+
const pending = this.pendingWatchEvents.get(key);
|
|
351
|
+
if (pending) {
|
|
352
|
+
for (const watcher of matchingWatchers)
|
|
353
|
+
pending.watchers.add(watcher);
|
|
354
|
+
} else {
|
|
355
|
+
this.pendingWatchEvents.set(key, { path: p, eventType, watchers: matchingWatchers });
|
|
356
|
+
}
|
|
357
|
+
if (this.watchFlushQueued)
|
|
358
|
+
return;
|
|
359
|
+
this.watchFlushQueued = true;
|
|
360
|
+
queueMicrotask(() => {
|
|
361
|
+
const events = [...this.pendingWatchEvents.values()];
|
|
362
|
+
this.pendingWatchEvents.clear();
|
|
363
|
+
this.watchFlushQueued = false;
|
|
364
|
+
for (const { path: eventPath, eventType: pendingEventType, watchers } of events) {
|
|
365
|
+
for (const watcher of watchers) {
|
|
366
|
+
watcher.listener(pendingEventType, this.watchFilename(watcher.path, eventPath));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
watchMatches(watcher, eventPath) {
|
|
372
|
+
return watcher.path === eventPath || watcher.path === dirname(eventPath) || watcher.recursive && this.isDescendant(watcher.path, eventPath);
|
|
373
|
+
}
|
|
374
|
+
closeWatcher(watcher) {
|
|
375
|
+
this.watchers.delete(watcher);
|
|
376
|
+
if (watcher.abortHandler)
|
|
377
|
+
watcher.signal?.removeEventListener("abort", watcher.abortHandler);
|
|
378
|
+
}
|
|
379
|
+
closeDeletedPathWatchers(deletedPath) {
|
|
380
|
+
for (const watcher of [...this.watchers]) {
|
|
381
|
+
if (watcher.path === deletedPath || this.isDescendant(deletedPath, watcher.path))
|
|
382
|
+
this.closeWatcher(watcher);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
watchFilename(watchPath, eventPath) {
|
|
386
|
+
if (watchPath === dirname(eventPath) || watchPath === eventPath)
|
|
387
|
+
return basename(eventPath);
|
|
388
|
+
return eventPath.slice(watchPath === "/" ? 1 : watchPath.length + 1);
|
|
389
|
+
}
|
|
390
|
+
emitSnapshotRestoreWatchEvents(before, after) {
|
|
391
|
+
const beforeEntries = this.snapshotEntries(before);
|
|
392
|
+
const afterEntries = this.snapshotEntries(after);
|
|
393
|
+
const paths = [...new Set([...beforeEntries.keys(), ...afterEntries.keys()])].filter((path) => path !== "/").sort((a, b) => a.localeCompare(b));
|
|
394
|
+
const deletedPaths = [];
|
|
395
|
+
for (const path of paths) {
|
|
396
|
+
const previous = beforeEntries.get(path);
|
|
397
|
+
const next = afterEntries.get(path);
|
|
398
|
+
if (!previous && next) {
|
|
399
|
+
this.emitWatch(path, "rename");
|
|
400
|
+
} else if (previous && !next) {
|
|
401
|
+
this.emitWatch(path, "rename");
|
|
402
|
+
deletedPaths.push(path);
|
|
403
|
+
} else if (previous && next && previous !== next) {
|
|
404
|
+
this.emitWatch(path, previous.split("\x00", 1)[0] === next.split("\x00", 1)[0] ? "change" : "rename");
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
for (const path of deletedPaths)
|
|
408
|
+
this.closeDeletedPathWatchers(path);
|
|
409
|
+
}
|
|
410
|
+
snapshotEntries(snapshot) {
|
|
411
|
+
const entries = new Map;
|
|
412
|
+
for (const dir of snapshot.dirs)
|
|
413
|
+
entries.set(dir, "dir");
|
|
414
|
+
for (const [path, file] of Object.entries(snapshot.files)) {
|
|
415
|
+
entries.set(path, `file\x00${typeof file === "string" ? `text\x00${file}` : `base64\x00${file.content}`}`);
|
|
416
|
+
}
|
|
417
|
+
for (const [path, target] of Object.entries(snapshot.symlinks ?? {}))
|
|
418
|
+
entries.set(path, `symlink\x00${target}`);
|
|
419
|
+
return entries;
|
|
420
|
+
}
|
|
421
|
+
entryPaths(entry, prefix = "") {
|
|
422
|
+
if (entry.kind === "file" || entry.kind === "symlink")
|
|
423
|
+
return [prefix];
|
|
424
|
+
const paths = [prefix];
|
|
425
|
+
for (const [name, child] of entry.children) {
|
|
426
|
+
paths.push(...this.entryPaths(child, `${prefix}/${name}`));
|
|
427
|
+
}
|
|
428
|
+
return paths;
|
|
429
|
+
}
|
|
430
|
+
isDescendant(parentPath, childPath) {
|
|
431
|
+
return parentPath !== childPath && childPath.startsWith(parentPath === "/" ? "/" : `${parentPath}/`);
|
|
432
|
+
}
|
|
433
|
+
getEntryNoFollow(path) {
|
|
434
|
+
try {
|
|
435
|
+
return this.walk(path, false, false);
|
|
436
|
+
} catch {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
findSymlinkPathComponent(path) {
|
|
441
|
+
const parts = clean(path).split("/").filter(Boolean);
|
|
442
|
+
let currentPath = "";
|
|
443
|
+
for (let index = 0;index < parts.length - 1; index += 1) {
|
|
444
|
+
currentPath = `${currentPath}/${parts[index]}`;
|
|
445
|
+
const entry = this.getEntryNoFollow(currentPath);
|
|
446
|
+
if (entry?.kind === "symlink")
|
|
447
|
+
return currentPath;
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
normalizeMountRelativePath(path) {
|
|
452
|
+
const parts = [];
|
|
453
|
+
for (const part of path.replace(/\\+/g, "/").split("/")) {
|
|
454
|
+
if (!part || part === ".")
|
|
455
|
+
continue;
|
|
456
|
+
if (part === "..")
|
|
457
|
+
throw new Error(`EINVAL: mount paths must stay within the mount root: ${path}`);
|
|
458
|
+
parts.push(part);
|
|
459
|
+
}
|
|
460
|
+
if (parts.length === 0)
|
|
461
|
+
throw new Error(`EINVAL: mount path must name a file: ${path}`);
|
|
462
|
+
return parts.join("/");
|
|
463
|
+
}
|
|
464
|
+
assertSupportedSnapshot(snapshot) {
|
|
465
|
+
if (!snapshot || snapshot.version !== 1) {
|
|
466
|
+
const version = snapshot && typeof snapshot === "object" && "version" in snapshot ? String(snapshot.version) : "missing";
|
|
467
|
+
throw new Error(`ERR_WC_SNAPSHOT_VERSION_UNSUPPORTED: MemoryFS cannot restore snapshot version ${version}; only MemoryFS snapshot version 1 has automated restore evidence. Snapshot migration, inode metadata, hardlink identity, permissions/mode bits, and durable persistence parity remain unsupported, so restore is rejected before mutating the existing filesystem tree.`);
|
|
468
|
+
}
|
|
469
|
+
const candidate = snapshot;
|
|
470
|
+
if (!isPlainRecord(candidate.files)) {
|
|
471
|
+
throw this.snapshotMetadataError("snapshot files metadata is missing or not an object");
|
|
472
|
+
}
|
|
473
|
+
if (!Array.isArray(candidate.dirs)) {
|
|
474
|
+
throw this.snapshotMetadataError("snapshot dirs metadata must be an array of paths");
|
|
475
|
+
}
|
|
476
|
+
for (const [index, dir] of candidate.dirs.entries()) {
|
|
477
|
+
if (typeof dir !== "string") {
|
|
478
|
+
throw this.snapshotDirMetadataError(index);
|
|
479
|
+
}
|
|
480
|
+
this.assertCanonicalSnapshotPath(dir, "dir");
|
|
481
|
+
}
|
|
482
|
+
const pathKinds = new Map;
|
|
483
|
+
for (const dir of candidate.dirs)
|
|
484
|
+
pathKinds.set(dir, "dir");
|
|
485
|
+
for (const [path, file] of Object.entries(candidate.files)) {
|
|
486
|
+
this.assertCanonicalSnapshotPath(path, "file");
|
|
487
|
+
this.assertSnapshotPathKind(pathKinds, path, "file");
|
|
488
|
+
const validText = typeof file === "string";
|
|
489
|
+
const validBinary = isPlainRecord(file) && file.encoding === "base64" && typeof file.content === "string";
|
|
490
|
+
if (!validText && !validBinary) {
|
|
491
|
+
throw this.snapshotMetadataError(`snapshot file metadata for ${path} is not a supported version-1 text or base64 binary payload`);
|
|
492
|
+
}
|
|
493
|
+
if (validBinary) {
|
|
494
|
+
this.assertValidSnapshotBase64(path, file.content);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (candidate.symlinks !== undefined) {
|
|
498
|
+
if (!isPlainRecord(candidate.symlinks)) {
|
|
499
|
+
throw this.snapshotMetadataError("snapshot symlinks metadata must be an object when present");
|
|
500
|
+
}
|
|
501
|
+
for (const [path, target] of Object.entries(candidate.symlinks)) {
|
|
502
|
+
this.assertCanonicalSnapshotPath(path, "symlink");
|
|
503
|
+
this.assertSnapshotPathKind(pathKinds, path, "symlink");
|
|
504
|
+
if (typeof target !== "string") {
|
|
505
|
+
throw this.snapshotMetadataError(`snapshot symlink metadata for ${path} must be a string target`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
for (const dir of candidate.dirs)
|
|
510
|
+
this.assertSnapshotParents(pathKinds, dir, "dir");
|
|
511
|
+
for (const path of Object.keys(candidate.files))
|
|
512
|
+
this.assertSnapshotParents(pathKinds, path, "file");
|
|
513
|
+
for (const path of Object.keys(candidate.symlinks ?? {}))
|
|
514
|
+
this.assertSnapshotParents(pathKinds, path, "symlink");
|
|
515
|
+
}
|
|
516
|
+
assertCanonicalSnapshotPath(path, kind) {
|
|
517
|
+
if (!path.startsWith("/") || clean(path) !== path) {
|
|
518
|
+
throw this.snapshotMetadataError(`snapshot ${kind} path ${path} must be an absolute canonical virtual path`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
assertSnapshotPathKind(pathKinds, path, kind) {
|
|
522
|
+
const existingKind = pathKinds.get(path);
|
|
523
|
+
if (existingKind && existingKind !== kind) {
|
|
524
|
+
throw this.snapshotKindCollisionError(path, existingKind, kind);
|
|
525
|
+
}
|
|
526
|
+
pathKinds.set(path, kind);
|
|
527
|
+
}
|
|
528
|
+
assertSnapshotParents(pathKinds, path, kind) {
|
|
529
|
+
if (path === "/")
|
|
530
|
+
return;
|
|
531
|
+
const parts = path.split("/").filter(Boolean);
|
|
532
|
+
let parent = "";
|
|
533
|
+
for (let index = 0;index < parts.length - 1; index += 1) {
|
|
534
|
+
parent = `${parent}/${parts[index]}`;
|
|
535
|
+
if (pathKinds.get(parent) !== "dir")
|
|
536
|
+
throw this.snapshotMissingParentError(path, kind, parent);
|
|
537
|
+
}
|
|
538
|
+
if (pathKinds.get("/") !== "dir")
|
|
539
|
+
throw this.snapshotMissingParentError(path, kind, "/");
|
|
540
|
+
}
|
|
541
|
+
assertValidSnapshotBase64(path, content) {
|
|
542
|
+
try {
|
|
543
|
+
base64ToBytes(content);
|
|
544
|
+
} catch {
|
|
545
|
+
throw this.snapshotMetadataError(`snapshot binary payload for ${path} is not valid base64`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
snapshotMetadataError(detail) {
|
|
549
|
+
return new Error(`ERR_WC_SNAPSHOT_METADATA_UNSUPPORTED: ${detail}; restore is rejected before mutating the existing filesystem tree. Only MemoryFS snapshot version 1 with absolute canonical virtual paths, string text files, { encoding: 'base64', content: string } binary files, string directory paths, and string symlink targets has automated restore evidence. Snapshot path normalization/repair, host filesystem path interpretation, realpath resolution, and durable persistence migration remain unsupported; Snapshot metadata repair, migration, inode/mode-bit metadata, and hardlink identity remain unsupported.`);
|
|
550
|
+
}
|
|
551
|
+
snapshotKindCollisionError(path, existingKind, kind) {
|
|
552
|
+
return new Error(`ERR_WC_SNAPSHOT_METADATA_UNSUPPORTED: snapshot path ${path} is recorded as both ${existingKind} and ${kind} metadata; restore is rejected before mutating the existing filesystem tree. Only MemoryFS snapshot version 1 with one entry kind per absolute canonical virtual path has automated restore evidence. Snapshot kind-collision repair, inode/mode-bit metadata reconciliation, hardlink identity, and durable persistence migration remain unsupported; POSIX/Linux filesystem parity and full Node fs metadata parity remain unsupported.`);
|
|
553
|
+
}
|
|
554
|
+
snapshotMissingParentError(path, kind, parent) {
|
|
555
|
+
return new Error(`ERR_WC_SNAPSHOT_METADATA_UNSUPPORTED: snapshot ${kind} path ${path} is missing parent dir metadata for ${parent}; restore is rejected before mutating the existing filesystem tree. Only MemoryFS snapshot version 1 with explicit directory metadata for every virtual parent path has automated restore evidence. Implicit parent directory reconstruction, snapshot repair, path normalization, and durable persistence migration remain unsupported; POSIX/Linux filesystem parity and full Node fs metadata parity remain unsupported.`);
|
|
556
|
+
}
|
|
557
|
+
snapshotDirMetadataError(index) {
|
|
558
|
+
return new Error(`ERR_WC_SNAPSHOT_METADATA_UNSUPPORTED: snapshot dir metadata at index ${index} is not a supported version-1 string path; restore is rejected before mutating the existing filesystem tree. Only MemoryFS snapshot version 1 stores dirs as absolute canonical virtual path strings. Directory mtimes, inode/device metadata, mode bits, xattrs, and snapshot metadata migration remain unsupported; host filesystem path interpretation, durable persistence migration, POSIX/Linux filesystem parity, and full Node fs metadata parity remain unsupported.`);
|
|
559
|
+
}
|
|
560
|
+
cloneContent(content) {
|
|
561
|
+
return typeof content === "string" ? content : new Uint8Array(content);
|
|
562
|
+
}
|
|
563
|
+
resolveSymlinkTarget(linkParentPath, target) {
|
|
564
|
+
return target.startsWith("/") ? clean(target) : clean(`${linkParentPath}/${target}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ../kernel/src/protocol.ts
|
|
569
|
+
var FS_OP = {
|
|
570
|
+
ReadFile: 1,
|
|
571
|
+
WriteFile: 2,
|
|
572
|
+
Readdir: 3,
|
|
573
|
+
Stat: 4,
|
|
574
|
+
Exists: 5,
|
|
575
|
+
Mkdirp: 6,
|
|
576
|
+
Unlink: 7,
|
|
577
|
+
Readlink: 8,
|
|
578
|
+
Rename: 9
|
|
579
|
+
};
|
|
580
|
+
var HEADER_LEN_BYTES = 4;
|
|
581
|
+
var encoder = new TextEncoder;
|
|
582
|
+
var decoder = new TextDecoder;
|
|
583
|
+
function decodeFrame(bytes) {
|
|
584
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
585
|
+
const headerLen = view.getUint32(0, true);
|
|
586
|
+
const headerStart = HEADER_LEN_BYTES;
|
|
587
|
+
const headerEnd = headerStart + headerLen;
|
|
588
|
+
const meta = headerLen === 0 ? {} : JSON.parse(decoder.decode(bytes.subarray(headerStart, headerEnd)));
|
|
589
|
+
const tail = bytes.subarray(headerEnd);
|
|
590
|
+
return { meta, tail };
|
|
591
|
+
}
|
|
592
|
+
function encodeText(text) {
|
|
593
|
+
return encoder.encode(text);
|
|
594
|
+
}
|
|
595
|
+
// ../syscall/src/index.ts
|
|
596
|
+
var SLOT_STATE = 0;
|
|
597
|
+
var SLOT_OPCODE = 1;
|
|
598
|
+
var SLOT_REQ_LEN = 2;
|
|
599
|
+
var SLOT_RES_LEN = 3;
|
|
600
|
+
var SLOT_RES_STATUS = 4;
|
|
601
|
+
var CONTROL_SLOTS = 8;
|
|
602
|
+
var CONTROL_BYTES = CONTROL_SLOTS * 4;
|
|
603
|
+
var STATE_REQUEST = 1;
|
|
604
|
+
var STATE_RESPONSE = 2;
|
|
605
|
+
var STATUS_OK = 0;
|
|
606
|
+
var STATUS_ERROR = 1;
|
|
607
|
+
var DEFAULT_DATA_BYTES = 1 << 20;
|
|
608
|
+
function createSyscallRing(dataBytes = DEFAULT_DATA_BYTES) {
|
|
609
|
+
return new SharedArrayBuffer(CONTROL_BYTES + dataBytes);
|
|
610
|
+
}
|
|
611
|
+
function ringViews(sab) {
|
|
612
|
+
const control = new Int32Array(sab, 0, CONTROL_SLOTS);
|
|
613
|
+
const data = new Uint8Array(sab, CONTROL_BYTES);
|
|
614
|
+
return { control, data };
|
|
615
|
+
}
|
|
616
|
+
class SyscallServicer {
|
|
617
|
+
handler;
|
|
618
|
+
control;
|
|
619
|
+
data;
|
|
620
|
+
constructor(sab, handler) {
|
|
621
|
+
this.handler = handler;
|
|
622
|
+
const v = ringViews(sab);
|
|
623
|
+
this.control = v.control;
|
|
624
|
+
this.data = v.data;
|
|
625
|
+
}
|
|
626
|
+
async serviceOnce() {
|
|
627
|
+
if (Atomics.load(this.control, SLOT_STATE) !== STATE_REQUEST)
|
|
628
|
+
return false;
|
|
629
|
+
const opcode = Atomics.load(this.control, SLOT_OPCODE);
|
|
630
|
+
const reqLen = Atomics.load(this.control, SLOT_REQ_LEN);
|
|
631
|
+
const request = this.data.slice(0, reqLen);
|
|
632
|
+
try {
|
|
633
|
+
const result = await this.handler(opcode, request);
|
|
634
|
+
this.writeResponse(result.status, result.bytes);
|
|
635
|
+
} catch (err) {
|
|
636
|
+
const message = new TextEncoder().encode(err instanceof Error ? err.message : String(err));
|
|
637
|
+
this.writeResponse(STATUS_ERROR, message);
|
|
638
|
+
}
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
writeResponse(status, bytes) {
|
|
642
|
+
if (bytes.byteLength > this.data.byteLength) {
|
|
643
|
+
const message = new TextEncoder().encode(`syscall response of ${bytes.byteLength} bytes exceeds ring capacity ` + `${this.data.byteLength}; payload chunking is not implemented yet`);
|
|
644
|
+
this.data.set(message, 0);
|
|
645
|
+
Atomics.store(this.control, SLOT_RES_LEN, message.byteLength);
|
|
646
|
+
Atomics.store(this.control, SLOT_RES_STATUS, STATUS_ERROR);
|
|
647
|
+
} else {
|
|
648
|
+
this.data.set(bytes, 0);
|
|
649
|
+
Atomics.store(this.control, SLOT_RES_LEN, bytes.byteLength);
|
|
650
|
+
Atomics.store(this.control, SLOT_RES_STATUS, status);
|
|
651
|
+
}
|
|
652
|
+
Atomics.store(this.control, SLOT_STATE, STATE_RESPONSE);
|
|
653
|
+
Atomics.notify(this.control, SLOT_STATE);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ../kernel/src/fs-servicer.ts
|
|
658
|
+
var EMPTY = new Uint8Array(0);
|
|
659
|
+
var ok = (bytes) => ({ status: STATUS_OK, bytes });
|
|
660
|
+
var fail = (message) => ({ status: STATUS_ERROR, bytes: encodeText(message) });
|
|
661
|
+
function createFsHandler(fs) {
|
|
662
|
+
return (opcode, request) => {
|
|
663
|
+
const { meta, tail } = decodeFrame(request);
|
|
664
|
+
try {
|
|
665
|
+
switch (opcode) {
|
|
666
|
+
case FS_OP.ReadFile:
|
|
667
|
+
return ok(fs.readFileBuffer(meta.path));
|
|
668
|
+
case FS_OP.WriteFile:
|
|
669
|
+
fs.writeFile(meta.path, tail);
|
|
670
|
+
return ok(EMPTY);
|
|
671
|
+
case FS_OP.Readdir:
|
|
672
|
+
return ok(encodeText(JSON.stringify(fs.list(meta.path).map((entry) => entry.name))));
|
|
673
|
+
case FS_OP.Stat: {
|
|
674
|
+
const stat = fs.lstat(meta.path);
|
|
675
|
+
let size = 0;
|
|
676
|
+
if (stat.kind === "file")
|
|
677
|
+
size = fs.readFileBuffer(meta.path).byteLength;
|
|
678
|
+
const wire = { kind: stat.kind, size, mtimeMs: stat.mtime };
|
|
679
|
+
return ok(encodeText(JSON.stringify(wire)));
|
|
680
|
+
}
|
|
681
|
+
case FS_OP.Exists:
|
|
682
|
+
return ok(encodeText(JSON.stringify(fs.exists(meta.path))));
|
|
683
|
+
case FS_OP.Mkdirp:
|
|
684
|
+
fs.mkdirp(meta.path);
|
|
685
|
+
return ok(EMPTY);
|
|
686
|
+
case FS_OP.Unlink:
|
|
687
|
+
fs.unlink(meta.path);
|
|
688
|
+
return ok(EMPTY);
|
|
689
|
+
case FS_OP.Readlink:
|
|
690
|
+
return ok(encodeText(fs.readlink(meta.path)));
|
|
691
|
+
case FS_OP.Rename:
|
|
692
|
+
fs.rename(meta.from, meta.to);
|
|
693
|
+
return ok(EMPTY);
|
|
694
|
+
default:
|
|
695
|
+
return fail(`EINVAL: unknown fs opcode ${opcode}`);
|
|
696
|
+
}
|
|
697
|
+
} catch (err) {
|
|
698
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
// ../kernel/src/syscall-memfs.ts
|
|
703
|
+
var encoder2 = new TextEncoder;
|
|
704
|
+
// ../kernel/src/streaming-spawn.ts
|
|
705
|
+
function spawnStreamingNodeProcess(fs, opts) {
|
|
706
|
+
const sab = createSyscallRing(opts.ringBytes);
|
|
707
|
+
const servicer = new SyscallServicer(sab, createFsHandler(fs));
|
|
708
|
+
const WorkerCtor = globalThis.Worker;
|
|
709
|
+
const worker = new WorkerCtor(opts.workerUrl, { type: "module" });
|
|
710
|
+
let resolveExit;
|
|
711
|
+
const exit = new Promise((resolve) => {
|
|
712
|
+
resolveExit = resolve;
|
|
713
|
+
});
|
|
714
|
+
let settled = false;
|
|
715
|
+
const settle = (code) => {
|
|
716
|
+
if (settled)
|
|
717
|
+
return;
|
|
718
|
+
settled = true;
|
|
719
|
+
try {
|
|
720
|
+
worker.terminate();
|
|
721
|
+
} catch {}
|
|
722
|
+
resolveExit(code);
|
|
723
|
+
};
|
|
724
|
+
worker.onerror = (event) => {
|
|
725
|
+
opts.onStderr?.((event?.message ?? "worker error") + `
|
|
726
|
+
`);
|
|
727
|
+
settle(1);
|
|
728
|
+
};
|
|
729
|
+
worker.onmessage = (event) => {
|
|
730
|
+
const msg = event.data;
|
|
731
|
+
if (!msg)
|
|
732
|
+
return;
|
|
733
|
+
if (msg.type === "doorbell") {
|
|
734
|
+
servicer.serviceOnce();
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (msg.type === "stdout") {
|
|
738
|
+
opts.onStdout?.(msg.chunk);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (msg.type === "stderr") {
|
|
742
|
+
opts.onStderr?.(msg.chunk);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (msg.type === "exit") {
|
|
746
|
+
settle(msg.code);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
worker.postMessage({
|
|
751
|
+
type: "init",
|
|
752
|
+
sab,
|
|
753
|
+
projectRoot: opts.projectRoot,
|
|
754
|
+
entry: opts.entry,
|
|
755
|
+
argv: opts.argv ?? [],
|
|
756
|
+
env: opts.env ?? {}
|
|
757
|
+
});
|
|
758
|
+
return {
|
|
759
|
+
exit,
|
|
760
|
+
writeStdin(chunk) {
|
|
761
|
+
try {
|
|
762
|
+
worker.postMessage({ type: "stdin", data: chunk });
|
|
763
|
+
} catch {}
|
|
764
|
+
},
|
|
765
|
+
kill() {
|
|
766
|
+
settle(143);
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
// ../kernel/src/shell-spawn.ts
|
|
771
|
+
function spawnShellProcess(fs, opts) {
|
|
772
|
+
const WorkerCtor = globalThis.Worker;
|
|
773
|
+
const worker = new WorkerCtor(opts.workerUrl, { type: "module" });
|
|
774
|
+
let resolveExit;
|
|
775
|
+
const exit = new Promise((resolve) => {
|
|
776
|
+
resolveExit = resolve;
|
|
777
|
+
});
|
|
778
|
+
let settled = false;
|
|
779
|
+
const settle = (code) => {
|
|
780
|
+
if (settled)
|
|
781
|
+
return;
|
|
782
|
+
settled = true;
|
|
783
|
+
try {
|
|
784
|
+
worker.terminate();
|
|
785
|
+
} catch {}
|
|
786
|
+
resolveExit(code);
|
|
787
|
+
};
|
|
788
|
+
worker.onerror = (event) => {
|
|
789
|
+
opts.onStderr?.((event?.message ?? "shell worker error") + `
|
|
790
|
+
`);
|
|
791
|
+
settle(1);
|
|
792
|
+
};
|
|
793
|
+
worker.onmessage = (event) => {
|
|
794
|
+
const msg = event.data;
|
|
795
|
+
if (!msg)
|
|
796
|
+
return;
|
|
797
|
+
if (msg.type === "stdout") {
|
|
798
|
+
opts.onStdout?.(msg.chunk);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (msg.type === "stderr") {
|
|
802
|
+
opts.onStderr?.(msg.chunk);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
if (msg.type === "exit") {
|
|
806
|
+
try {
|
|
807
|
+
if (msg.snapshot)
|
|
808
|
+
fs.restore(msg.snapshot);
|
|
809
|
+
} catch {}
|
|
810
|
+
settle(msg.code);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
worker.postMessage({
|
|
815
|
+
type: "init",
|
|
816
|
+
snapshot: fs.snapshot(),
|
|
817
|
+
commandLine: opts.commandLine,
|
|
818
|
+
cwd: opts.cwd,
|
|
819
|
+
env: opts.env ?? {}
|
|
820
|
+
});
|
|
821
|
+
return {
|
|
822
|
+
exit,
|
|
823
|
+
writeStdin() {},
|
|
824
|
+
kill() {
|
|
825
|
+
settle(143);
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
// ../network/src/index.ts
|
|
830
|
+
class VirtualNetwork {
|
|
831
|
+
handlers = new Map;
|
|
832
|
+
listen(port, handler) {
|
|
833
|
+
if (this.handlers.has(port))
|
|
834
|
+
throw new Error(`EADDRINUSE: ${port}`);
|
|
835
|
+
this.handlers.set(port, handler);
|
|
836
|
+
return { port, close: () => this.handlers.delete(port), url: `http://localhost:${port}` };
|
|
837
|
+
}
|
|
838
|
+
async fetch(url, init) {
|
|
839
|
+
const u = new URL(url);
|
|
840
|
+
const h = this.handlers.get(Number(u.port || 80));
|
|
841
|
+
if (!h)
|
|
842
|
+
throw new Error(`ECONNREFUSED: ${u.port}`);
|
|
843
|
+
return h(new Request(url, init));
|
|
844
|
+
}
|
|
845
|
+
ports() {
|
|
846
|
+
return [...this.handlers.keys()].sort((a, b) => a - b);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
var LOOPBACK_HOSTS = new Set([
|
|
850
|
+
"localhost",
|
|
851
|
+
"127.0.0.1",
|
|
852
|
+
"0.0.0.0",
|
|
853
|
+
"::1",
|
|
854
|
+
"[::1]",
|
|
855
|
+
"[::ffff:127.0.0.1]"
|
|
856
|
+
]);
|
|
857
|
+
var RELAY_PROXY_PATH = "/__wc_relay_proxy";
|
|
858
|
+
|
|
859
|
+
// src/command-resolver.ts
|
|
860
|
+
var NOT_IMPLEMENTED = "ERR_WC_NOT_IMPLEMENTED";
|
|
861
|
+
function cleanPath(p) {
|
|
862
|
+
const parts = [];
|
|
863
|
+
for (const part of p.replace(/\\+/g, "/").split("/")) {
|
|
864
|
+
if (!part || part === ".")
|
|
865
|
+
continue;
|
|
866
|
+
if (part === "..") {
|
|
867
|
+
parts.pop();
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
parts.push(part);
|
|
871
|
+
}
|
|
872
|
+
return `/${parts.join("/")}`;
|
|
873
|
+
}
|
|
874
|
+
function resolvePath(base, p) {
|
|
875
|
+
if (p.startsWith("/"))
|
|
876
|
+
return cleanPath(p);
|
|
877
|
+
return cleanPath(`${base}/${p}`);
|
|
878
|
+
}
|
|
879
|
+
var DEV_SCRIPT_HINTS = new Set(["dev", "start:dev", "serve"]);
|
|
880
|
+
function isDevServerCommand(command, args) {
|
|
881
|
+
if (command === "vite" && args.length === 0)
|
|
882
|
+
return true;
|
|
883
|
+
if ((command === "npm" || command === "pnpm" || command === "yarn") && (args[0] === "dev" || args[0] === "run" && args[1] === "dev")) {
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
if (command === "pnpm" && args[0] === "dev")
|
|
887
|
+
return true;
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
function readScripts(fs, cwd) {
|
|
891
|
+
const pkgPath = `${cwd}/package.json`;
|
|
892
|
+
if (!fs.exists(pkgPath))
|
|
893
|
+
return {};
|
|
894
|
+
try {
|
|
895
|
+
const pkg = JSON.parse(fs.readFile(pkgPath));
|
|
896
|
+
return pkg.scripts ?? {};
|
|
897
|
+
} catch {
|
|
898
|
+
return {};
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function tokenize(commandLine) {
|
|
902
|
+
const tokens = [];
|
|
903
|
+
let current = "";
|
|
904
|
+
let quote = null;
|
|
905
|
+
let has = false;
|
|
906
|
+
for (let i = 0;i < commandLine.length; i += 1) {
|
|
907
|
+
const ch = commandLine[i];
|
|
908
|
+
if (quote) {
|
|
909
|
+
if (ch === quote)
|
|
910
|
+
quote = null;
|
|
911
|
+
else
|
|
912
|
+
current += ch;
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
if (ch === '"' || ch === "'") {
|
|
916
|
+
quote = ch;
|
|
917
|
+
has = true;
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
if (ch === " " || ch === "\t") {
|
|
921
|
+
if (has) {
|
|
922
|
+
tokens.push(current);
|
|
923
|
+
current = "";
|
|
924
|
+
has = false;
|
|
925
|
+
}
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
if ("|&;<>".includes(ch)) {
|
|
929
|
+
throw new Error(`${NOT_IMPLEMENTED}: the script command line ${JSON.stringify(commandLine)} uses a shell control operator (${ch}); ` + `the resolver runs a single command + args directly (no pipelines/redirections/chaining yet). ` + `Run it through spawn('jsh', ['-c', <line>]) — the real @wc/shell (just-bash) — for full shell semantics.`);
|
|
930
|
+
}
|
|
931
|
+
current += ch;
|
|
932
|
+
has = true;
|
|
933
|
+
}
|
|
934
|
+
if (has)
|
|
935
|
+
tokens.push(current);
|
|
936
|
+
return tokens;
|
|
937
|
+
}
|
|
938
|
+
function splitEnvPrefix(tokens) {
|
|
939
|
+
const env = {};
|
|
940
|
+
let i = 0;
|
|
941
|
+
for (;i < tokens.length; i += 1) {
|
|
942
|
+
const m = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(tokens[i]);
|
|
943
|
+
if (!m)
|
|
944
|
+
break;
|
|
945
|
+
env[m[1]] = m[2];
|
|
946
|
+
}
|
|
947
|
+
return { env, rest: tokens.slice(i) };
|
|
948
|
+
}
|
|
949
|
+
function resolveBin(fs, cwd, cmd) {
|
|
950
|
+
let dir = cleanPath(cwd);
|
|
951
|
+
for (;; ) {
|
|
952
|
+
const binLink = `${dir}/node_modules/.bin/${cmd}`;
|
|
953
|
+
if (fs.exists(binLink)) {
|
|
954
|
+
let target = binLink;
|
|
955
|
+
try {
|
|
956
|
+
if (fs.lstat(binLink).kind === "symlink") {
|
|
957
|
+
const link = fs.readlink(binLink);
|
|
958
|
+
target = resolvePath(`${dir}/node_modules/.bin`, link);
|
|
959
|
+
}
|
|
960
|
+
} catch {}
|
|
961
|
+
if (fs.exists(target) && fs.lstat(target).kind === "file")
|
|
962
|
+
return target;
|
|
963
|
+
}
|
|
964
|
+
const pkgJson = `${dir}/node_modules/${cmd}/package.json`;
|
|
965
|
+
if (fs.exists(pkgJson)) {
|
|
966
|
+
try {
|
|
967
|
+
const pkg = JSON.parse(fs.readFile(pkgJson));
|
|
968
|
+
let rel;
|
|
969
|
+
if (typeof pkg.bin === "string")
|
|
970
|
+
rel = pkg.bin;
|
|
971
|
+
else if (pkg.bin && typeof pkg.bin === "object")
|
|
972
|
+
rel = pkg.bin[cmd] ?? Object.values(pkg.bin)[0];
|
|
973
|
+
if (rel) {
|
|
974
|
+
const entry = resolvePath(`${dir}/node_modules/${cmd}`, rel);
|
|
975
|
+
if (fs.exists(entry))
|
|
976
|
+
return entry;
|
|
977
|
+
}
|
|
978
|
+
} catch {}
|
|
979
|
+
}
|
|
980
|
+
const parent = dir.split("/").slice(0, -1).join("/") || "/";
|
|
981
|
+
if (parent === dir)
|
|
982
|
+
break;
|
|
983
|
+
dir = parent;
|
|
984
|
+
}
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
function resolveSpawn(fs, cwd, command, args) {
|
|
988
|
+
const base = cleanPath(cwd);
|
|
989
|
+
if (isDevServerCommand(command, args)) {
|
|
990
|
+
return { kind: "dev-server", label: `${command} ${args.join(" ")}`.trim() };
|
|
991
|
+
}
|
|
992
|
+
if (command === "jsh" || command === "bash" || command === "sh") {
|
|
993
|
+
if (args[0] === "-c" && typeof args[1] === "string") {
|
|
994
|
+
return { kind: "shell", commandLine: args[1], cwd: base, label: `${command} -c` };
|
|
995
|
+
}
|
|
996
|
+
if (args.length === 0) {
|
|
997
|
+
return { kind: "shell", cwd: base, label: command };
|
|
998
|
+
}
|
|
999
|
+
const scriptPath = resolvePath(base, args[0]);
|
|
1000
|
+
if (fs.exists(scriptPath)) {
|
|
1001
|
+
return { kind: "shell", commandLine: fs.readFile(scriptPath), cwd: base, label: `${command} ${args[0]}` };
|
|
1002
|
+
}
|
|
1003
|
+
return { kind: "shell", commandLine: [command, ...args].join(" "), cwd: base, label: `${command} ${args.join(" ")}` };
|
|
1004
|
+
}
|
|
1005
|
+
if (command === "node") {
|
|
1006
|
+
const file = args[0];
|
|
1007
|
+
if (!file) {
|
|
1008
|
+
return {
|
|
1009
|
+
kind: "unsupported",
|
|
1010
|
+
message: `${NOT_IMPLEMENTED}: spawn('node') with no entry (a Node REPL) is not supported in the browser runtime. Pass a file: spawn('node', ['script.js']).`
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
const entry = resolvePath(base, file);
|
|
1014
|
+
if (!fs.exists(entry)) {
|
|
1015
|
+
return {
|
|
1016
|
+
kind: "unsupported",
|
|
1017
|
+
message: `ERR_WC_ENOENT: node entry ${JSON.stringify(file)} resolves to ${entry}, which does not exist in the mounted filesystem.`
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
return { kind: "node", entry, args: args.slice(1), cwd: base, label: `node ${file}` };
|
|
1021
|
+
}
|
|
1022
|
+
if (command === "npm" || command === "pnpm" || command === "yarn") {
|
|
1023
|
+
const sub = args[0] ?? "";
|
|
1024
|
+
if (sub === "install" || sub === "i" || sub === "ci" || sub === "add") {
|
|
1025
|
+
return {
|
|
1026
|
+
kind: "unsupported",
|
|
1027
|
+
message: `${NOT_IMPLEMENTED}: spawn('${command}', ['${sub}', …]) is an honest stub. Dependencies are pre-vendored into the mounted closure (the host page mounts the package closure); a real in-browser installer is a separate workstream (@wc/package-manager / WS5). This never fabricates a fake install.`
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
let scriptName;
|
|
1031
|
+
let scriptArgs = [];
|
|
1032
|
+
if (sub === "run" || command !== "npm" && sub !== "test" && sub !== "start" && args.length > 0 && !sub.startsWith("-")) {
|
|
1033
|
+
scriptName = sub === "run" ? args[1] : sub;
|
|
1034
|
+
scriptArgs = sub === "run" ? args.slice(2) : args.slice(1);
|
|
1035
|
+
} else if (sub === "test") {
|
|
1036
|
+
scriptName = "test";
|
|
1037
|
+
scriptArgs = args.slice(1);
|
|
1038
|
+
} else if (sub === "start") {
|
|
1039
|
+
scriptName = "start";
|
|
1040
|
+
scriptArgs = args.slice(1);
|
|
1041
|
+
}
|
|
1042
|
+
if (!scriptName) {
|
|
1043
|
+
return {
|
|
1044
|
+
kind: "unsupported",
|
|
1045
|
+
message: `${NOT_IMPLEMENTED}: spawn('${command}', ${JSON.stringify(args)}) is not supported. The resolver handles 'run <script>', 'test', 'start', and 'dev' (real dev server); 'install' is an honest stub.`
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
const scripts = readScripts(fs, base);
|
|
1049
|
+
const scriptLine = scripts[scriptName];
|
|
1050
|
+
if (scriptLine === undefined) {
|
|
1051
|
+
return {
|
|
1052
|
+
kind: "unsupported",
|
|
1053
|
+
message: `ERR_WC_NO_SCRIPT: package.json at ${base}/package.json has no "${scriptName}" script. Mounted scripts: ${JSON.stringify(Object.keys(scripts))}.`
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
let tokens;
|
|
1057
|
+
try {
|
|
1058
|
+
tokens = tokenize(scriptLine);
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
return { kind: "unsupported", message: err instanceof Error ? err.message : String(err) };
|
|
1061
|
+
}
|
|
1062
|
+
const { rest } = splitEnvPrefix(tokens);
|
|
1063
|
+
if (rest.length === 0) {
|
|
1064
|
+
return { kind: "unsupported", message: `ERR_WC_EMPTY_SCRIPT: the "${scriptName}" script is empty.` };
|
|
1065
|
+
}
|
|
1066
|
+
const innerCmd = rest[0];
|
|
1067
|
+
const innerArgs = [...rest.slice(1), ...scriptArgs];
|
|
1068
|
+
if (isDevServerCommand(innerCmd, innerArgs) || DEV_SCRIPT_HINTS.has(scriptName) && innerCmd === "vite") {
|
|
1069
|
+
return { kind: "dev-server", label: `${command} run ${scriptName} → ${scriptLine}` };
|
|
1070
|
+
}
|
|
1071
|
+
const inner = resolveSpawn(fs, base, innerCmd, innerArgs);
|
|
1072
|
+
if (inner.kind === "node")
|
|
1073
|
+
return { ...inner, label: `${command} run ${scriptName} → ${inner.label}` };
|
|
1074
|
+
if (inner.kind === "dev-server")
|
|
1075
|
+
return { ...inner, label: `${command} run ${scriptName} → ${inner.label}` };
|
|
1076
|
+
if (inner.kind === "shell")
|
|
1077
|
+
return inner;
|
|
1078
|
+
return inner;
|
|
1079
|
+
}
|
|
1080
|
+
if (command === "npx") {
|
|
1081
|
+
const bin = args[0];
|
|
1082
|
+
if (!bin) {
|
|
1083
|
+
return { kind: "unsupported", message: `${NOT_IMPLEMENTED}: spawn('npx') with no command. Pass a bin: spawn('npx', ['vite']).` };
|
|
1084
|
+
}
|
|
1085
|
+
if (bin === "node") {
|
|
1086
|
+
return resolveSpawn(fs, base, "node", args.slice(1));
|
|
1087
|
+
}
|
|
1088
|
+
const entry = resolveBin(fs, base, bin);
|
|
1089
|
+
if (!entry) {
|
|
1090
|
+
return {
|
|
1091
|
+
kind: "unsupported",
|
|
1092
|
+
message: `ERR_WC_BIN_NOT_FOUND: npx could not resolve "${bin}" to node_modules/.bin/${bin} or a package bin field under ${base}. ` + `Dependencies are pre-vendored; remote-fetch of an uninstalled bin (npx's download path) is not supported in the browser runtime.`
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
return { kind: "node", entry, args: args.slice(1), cwd: base, label: `npx ${bin}` };
|
|
1096
|
+
}
|
|
1097
|
+
const direct = resolveBin(fs, base, command);
|
|
1098
|
+
if (direct) {
|
|
1099
|
+
return { kind: "node", entry: direct, args, cwd: base, label: `${command} ${args.join(" ")}`.trim() };
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
kind: "unsupported",
|
|
1103
|
+
message: `${NOT_IMPLEMENTED}: spawn(${JSON.stringify(command)}, ${JSON.stringify(args)}) could not be resolved. ` + `Supported: node <file>, npm/pnpm/yarn run|test|start (real scripts), npm run dev / vite (real dev server), npx <bin> and direct local bins (node_modules/.bin resolution), jsh/bash/sh (real @wc/shell). ` + `npm install is an honest stub. No output is ever faked.`
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
// src/index.ts
|
|
1107
|
+
function createSameOriginRelayTransport(fetchImpl = globalThis.fetch.bind(globalThis), proxyPath = RELAY_PROXY_PATH) {
|
|
1108
|
+
return {
|
|
1109
|
+
async send(request) {
|
|
1110
|
+
const res = await fetchImpl(proxyPath, {
|
|
1111
|
+
method: "POST",
|
|
1112
|
+
headers: { "Content-Type": "application/json" },
|
|
1113
|
+
body: JSON.stringify(request)
|
|
1114
|
+
});
|
|
1115
|
+
return await res.json();
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
var MAX_FILE_BYTES = 1024 * 1024;
|
|
1120
|
+
var NOT_IMPLEMENTED2 = "ERR_WC_NOT_IMPLEMENTED";
|
|
1121
|
+
var PreviewMessageType;
|
|
1122
|
+
((PreviewMessageType2) => {
|
|
1123
|
+
PreviewMessageType2["UncaughtException"] = "PREVIEW_UNCAUGHT_EXCEPTION";
|
|
1124
|
+
PreviewMessageType2["UnhandledRejection"] = "PREVIEW_UNHANDLED_REJECTION";
|
|
1125
|
+
PreviewMessageType2["ConsoleError"] = "PREVIEW_CONSOLE_ERROR";
|
|
1126
|
+
})(PreviewMessageType ||= {});
|
|
1127
|
+
var PREVIEW_MESSAGE_TYPES = new Set(Object.values(PreviewMessageType));
|
|
1128
|
+
function isPreviewMessage(data) {
|
|
1129
|
+
return typeof data === "object" && data !== null && "type" in data && typeof data.type === "string" && PREVIEW_MESSAGE_TYPES.has(data.type);
|
|
1130
|
+
}
|
|
1131
|
+
async function reloadPreview(preview, hardnessMs = 200) {
|
|
1132
|
+
if (!preview || typeof preview !== "object" || !("src" in preview)) {
|
|
1133
|
+
throw new TypeError("reloadPreview(preview): preview must be an HTMLIFrameElement");
|
|
1134
|
+
}
|
|
1135
|
+
const src = preview.src;
|
|
1136
|
+
preview.src = src;
|
|
1137
|
+
if (hardnessMs > 0) {
|
|
1138
|
+
await new Promise((resolve) => setTimeout(resolve, hardnessMs));
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
var AUTH_UNSUPPORTED = "ERR_WC_AUTH_UNSUPPORTED: @start.dev/container is an unhosted, pure-browser runtime; it has no StackBlitz " + "account/editor-origin auth model. The `auth` namespace exists for drop-in import compatibility but " + "does not implement a real session — this is an honest stub, not a fake login.";
|
|
1142
|
+
var auth = {
|
|
1143
|
+
init() {
|
|
1144
|
+
throw new Error(AUTH_UNSUPPORTED);
|
|
1145
|
+
},
|
|
1146
|
+
startAuthFlow() {
|
|
1147
|
+
throw new Error(AUTH_UNSUPPORTED);
|
|
1148
|
+
},
|
|
1149
|
+
logout() {
|
|
1150
|
+
throw new Error(AUTH_UNSUPPORTED);
|
|
1151
|
+
},
|
|
1152
|
+
loggedIn() {
|
|
1153
|
+
return Promise.reject(new Error(AUTH_UNSUPPORTED));
|
|
1154
|
+
},
|
|
1155
|
+
on() {
|
|
1156
|
+
return () => {};
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
function configureAPIKey(_apiKey) {}
|
|
1160
|
+
var textEncoder = new TextEncoder;
|
|
1161
|
+
var textDecoder = new TextDecoder;
|
|
1162
|
+
function cleanPath2(p) {
|
|
1163
|
+
const parts = [];
|
|
1164
|
+
for (const part of p.replace(/\\+/g, "/").split("/")) {
|
|
1165
|
+
if (!part || part === ".")
|
|
1166
|
+
continue;
|
|
1167
|
+
if (part === "..") {
|
|
1168
|
+
parts.pop();
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
parts.push(part);
|
|
1172
|
+
}
|
|
1173
|
+
return `/${parts.join("/")}`;
|
|
1174
|
+
}
|
|
1175
|
+
function resolvePath2(base, p) {
|
|
1176
|
+
if (p.startsWith("/"))
|
|
1177
|
+
return cleanPath2(p);
|
|
1178
|
+
return cleanPath2(`${base}/${p}`);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
class FileSystemFacade {
|
|
1182
|
+
fs;
|
|
1183
|
+
workdir;
|
|
1184
|
+
onMutate;
|
|
1185
|
+
trackWatcher;
|
|
1186
|
+
constructor(fs, workdir, onMutate, trackWatcher) {
|
|
1187
|
+
this.fs = fs;
|
|
1188
|
+
this.workdir = workdir;
|
|
1189
|
+
this.onMutate = onMutate;
|
|
1190
|
+
this.trackWatcher = trackWatcher;
|
|
1191
|
+
}
|
|
1192
|
+
resolve(p) {
|
|
1193
|
+
return resolvePath2(this.workdir(), p);
|
|
1194
|
+
}
|
|
1195
|
+
async readFile(path, encoding) {
|
|
1196
|
+
const abs = this.resolve(path);
|
|
1197
|
+
if (encoding === "utf8" || encoding === "utf-8")
|
|
1198
|
+
return this.fs.readFile(abs);
|
|
1199
|
+
return this.fs.readFileBuffer(abs);
|
|
1200
|
+
}
|
|
1201
|
+
async writeFile(path, data, options) {
|
|
1202
|
+
const abs = this.resolve(path);
|
|
1203
|
+
const byteLength = typeof data === "string" ? textEncoder.encode(data).byteLength : data.byteLength;
|
|
1204
|
+
if (byteLength > MAX_FILE_BYTES) {
|
|
1205
|
+
throw new Error(`ERR_WC_FS_FILE_TOO_LARGE: ${abs} is ${byteLength} bytes, exceeding the runtime's ${MAX_FILE_BYTES}-byte (1 MiB) per-file limit. ` + `The write is rejected rather than silently truncated; split the file or raise the limit when large-file persistence is proven.`);
|
|
1206
|
+
}
|
|
1207
|
+
this.fs.writeFile(abs, data);
|
|
1208
|
+
this.onMutate(abs, typeof data === "string" ? data : textDecoder.decode(data));
|
|
1209
|
+
}
|
|
1210
|
+
async readdir(path, options) {
|
|
1211
|
+
const abs = this.resolve(path);
|
|
1212
|
+
const entries = this.fs.list(abs);
|
|
1213
|
+
if (options?.withFileTypes) {
|
|
1214
|
+
return entries.map((e) => ({
|
|
1215
|
+
name: e.name,
|
|
1216
|
+
isFile: () => e.kind === "file",
|
|
1217
|
+
isDirectory: () => e.kind === "dir"
|
|
1218
|
+
}));
|
|
1219
|
+
}
|
|
1220
|
+
return entries.map((e) => e.name);
|
|
1221
|
+
}
|
|
1222
|
+
async mkdir(path, options) {
|
|
1223
|
+
const abs = this.resolve(path);
|
|
1224
|
+
if (options?.recursive) {
|
|
1225
|
+
this.fs.mkdirp(abs);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
const parent = abs.split("/").slice(0, -1).join("/") || "/";
|
|
1229
|
+
if (!this.fs.exists(parent))
|
|
1230
|
+
throw new Error(`ENOENT: ${abs}`);
|
|
1231
|
+
if (this.fs.exists(abs))
|
|
1232
|
+
throw new Error(`EEXIST: ${abs}`);
|
|
1233
|
+
this.fs.mkdirp(abs);
|
|
1234
|
+
}
|
|
1235
|
+
async rm(path, options) {
|
|
1236
|
+
const abs = this.resolve(path);
|
|
1237
|
+
if (!this.fs.exists(abs)) {
|
|
1238
|
+
if (options?.force)
|
|
1239
|
+
return;
|
|
1240
|
+
throw new Error(`ENOENT: ${abs}`);
|
|
1241
|
+
}
|
|
1242
|
+
const stat = this.fs.lstat(abs);
|
|
1243
|
+
if (stat.kind === "dir") {
|
|
1244
|
+
if (!options?.recursive)
|
|
1245
|
+
throw new Error(`EISDIR: ${abs} (use { recursive: true } to remove a directory)`);
|
|
1246
|
+
this.fs.rmdir(abs);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
this.fs.unlink(abs);
|
|
1250
|
+
}
|
|
1251
|
+
async rename(oldPath, newPath) {
|
|
1252
|
+
const to = this.resolve(newPath);
|
|
1253
|
+
this.fs.rename(this.resolve(oldPath), to);
|
|
1254
|
+
if (this.fs.exists(to) && this.fs.lstat(to).kind === "file") {
|
|
1255
|
+
this.onMutate(to, this.fs.readFile(to));
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
watch(path, optionsOrListener, maybeListener) {
|
|
1259
|
+
const abs = this.resolve(path);
|
|
1260
|
+
if (typeof optionsOrListener === "function") {
|
|
1261
|
+
const watcher2 = this.fs.watch(abs, optionsOrListener);
|
|
1262
|
+
this.trackWatcher(watcher2);
|
|
1263
|
+
return watcher2;
|
|
1264
|
+
}
|
|
1265
|
+
const options = optionsOrListener ?? {};
|
|
1266
|
+
if (maybeListener) {
|
|
1267
|
+
const watcher2 = this.fs.watch(abs, options, maybeListener);
|
|
1268
|
+
this.trackWatcher(watcher2);
|
|
1269
|
+
return watcher2;
|
|
1270
|
+
}
|
|
1271
|
+
const watcher = this.fs.watch(abs, options, () => {});
|
|
1272
|
+
this.trackWatcher(watcher);
|
|
1273
|
+
return watcher;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
class Emitter {
|
|
1278
|
+
listeners = new Map;
|
|
1279
|
+
on(event, listener) {
|
|
1280
|
+
let set = this.listeners.get(event);
|
|
1281
|
+
if (!set) {
|
|
1282
|
+
set = new Set;
|
|
1283
|
+
this.listeners.set(event, set);
|
|
1284
|
+
}
|
|
1285
|
+
set.add(listener);
|
|
1286
|
+
return () => set.delete(listener);
|
|
1287
|
+
}
|
|
1288
|
+
emit(event, ...args) {
|
|
1289
|
+
const set = this.listeners.get(event);
|
|
1290
|
+
if (!set)
|
|
1291
|
+
return;
|
|
1292
|
+
for (const listener of Array.from(set)) {
|
|
1293
|
+
try {
|
|
1294
|
+
listener(...args);
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
if (typeof console !== "undefined")
|
|
1297
|
+
console.error("[wc:container] listener threw:", err);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
clear() {
|
|
1302
|
+
this.listeners.clear();
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
var DEV_SERVER_PORT = 5173;
|
|
1306
|
+
function makeWorker(url) {
|
|
1307
|
+
const Ctor = globalThis.Worker;
|
|
1308
|
+
if (typeof Ctor !== "function") {
|
|
1309
|
+
throw new Error(`${NOT_IMPLEMENTED2}: @start.dev/container requires a browser Worker. No global Worker constructor is available in this context.`);
|
|
1310
|
+
}
|
|
1311
|
+
return new Ctor(url, { type: "module" });
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
class DevServerProcess {
|
|
1315
|
+
output;
|
|
1316
|
+
input;
|
|
1317
|
+
exit;
|
|
1318
|
+
#worker;
|
|
1319
|
+
#outputController;
|
|
1320
|
+
#resolveExit;
|
|
1321
|
+
#killed = false;
|
|
1322
|
+
constructor(worker) {
|
|
1323
|
+
this.#worker = worker;
|
|
1324
|
+
this.output = new ReadableStream({
|
|
1325
|
+
start: (controller) => {
|
|
1326
|
+
this.#outputController = controller;
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
this.input = new WritableStream({
|
|
1330
|
+
write: (chunk) => {
|
|
1331
|
+
try {
|
|
1332
|
+
this.#worker.postMessage({ type: "stdin", data: chunk });
|
|
1333
|
+
} catch {}
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
this.exit = new Promise((resolve) => {
|
|
1337
|
+
this.#resolveExit = resolve;
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
pushOutput(line) {
|
|
1341
|
+
try {
|
|
1342
|
+
this.#outputController.enqueue(line);
|
|
1343
|
+
} catch {}
|
|
1344
|
+
}
|
|
1345
|
+
resize() {}
|
|
1346
|
+
kill() {
|
|
1347
|
+
if (this.#killed)
|
|
1348
|
+
return;
|
|
1349
|
+
this.#killed = true;
|
|
1350
|
+
try {
|
|
1351
|
+
this.#worker.terminate();
|
|
1352
|
+
} catch {}
|
|
1353
|
+
try {
|
|
1354
|
+
this.#outputController.close();
|
|
1355
|
+
} catch {}
|
|
1356
|
+
this.#resolveExit(0);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
class StreamingWebContainerProcess {
|
|
1361
|
+
output;
|
|
1362
|
+
input;
|
|
1363
|
+
exit;
|
|
1364
|
+
#outputController;
|
|
1365
|
+
#handle;
|
|
1366
|
+
#closed = false;
|
|
1367
|
+
constructor() {
|
|
1368
|
+
this.output = new ReadableStream({
|
|
1369
|
+
start: (controller) => {
|
|
1370
|
+
this.#outputController = controller;
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
this.input = new WritableStream({
|
|
1374
|
+
write: (chunk) => {
|
|
1375
|
+
this.#handle?.writeStdin(chunk);
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
this.exit = new Promise((resolve) => {
|
|
1379
|
+
this.#resolveExit = resolve;
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
#resolveExit;
|
|
1383
|
+
push(chunk) {
|
|
1384
|
+
if (this.#closed)
|
|
1385
|
+
return;
|
|
1386
|
+
try {
|
|
1387
|
+
this.#outputController.enqueue(chunk);
|
|
1388
|
+
} catch {}
|
|
1389
|
+
}
|
|
1390
|
+
attach(handle) {
|
|
1391
|
+
this.#handle = handle;
|
|
1392
|
+
handle.exit.then((code) => {
|
|
1393
|
+
this.#close();
|
|
1394
|
+
this.#resolveExit(code);
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
#close() {
|
|
1398
|
+
if (this.#closed)
|
|
1399
|
+
return;
|
|
1400
|
+
this.#closed = true;
|
|
1401
|
+
try {
|
|
1402
|
+
this.#outputController.close();
|
|
1403
|
+
} catch {}
|
|
1404
|
+
}
|
|
1405
|
+
resize() {}
|
|
1406
|
+
kill() {
|
|
1407
|
+
this.#handle?.kill();
|
|
1408
|
+
this.#close();
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
var liveContainer;
|
|
1412
|
+
|
|
1413
|
+
class WebContainer {
|
|
1414
|
+
workdir;
|
|
1415
|
+
fs;
|
|
1416
|
+
routes = {};
|
|
1417
|
+
#memfs;
|
|
1418
|
+
#runtime;
|
|
1419
|
+
#emitter = new Emitter;
|
|
1420
|
+
#devServer;
|
|
1421
|
+
#worker;
|
|
1422
|
+
#serviceWorkerRegistration;
|
|
1423
|
+
#pageMessageHandler;
|
|
1424
|
+
#pendingHttp = new Map;
|
|
1425
|
+
#httpSeq = 0;
|
|
1426
|
+
#torn = false;
|
|
1427
|
+
#fsWatchers = new Set;
|
|
1428
|
+
constructor(workdir, memfs, runtime) {
|
|
1429
|
+
this.workdir = workdir;
|
|
1430
|
+
this.#memfs = memfs;
|
|
1431
|
+
this.#runtime = runtime;
|
|
1432
|
+
this.fs = new FileSystemFacade(memfs, () => this.workdir, (abs, contents) => this.#forwardFsMutation(abs, contents), (watcher) => this.#fsWatchers.add(watcher));
|
|
1433
|
+
}
|
|
1434
|
+
#forwardFsMutation(absPath, contents) {
|
|
1435
|
+
if (!this.#worker || this.#torn || contents === undefined)
|
|
1436
|
+
return;
|
|
1437
|
+
this.#worker.postMessage({ type: "hmr-edit", path: absPath, contents });
|
|
1438
|
+
}
|
|
1439
|
+
static async boot(options = {}) {
|
|
1440
|
+
if (liveContainer) {
|
|
1441
|
+
throw new Error("ERR_WC_ALREADY_BOOTED: a WebContainer is already running in this page. " + "@start.dev/container is a singleton (like @webcontainer/api): call teardown() on the live " + "instance before booting another.");
|
|
1442
|
+
}
|
|
1443
|
+
if (typeof crossOriginIsolated !== "undefined" && crossOriginIsolated === false) {
|
|
1444
|
+
throw new Error("ERR_WC_NOT_CROSS_ORIGIN_ISOLATED: WebContainer.boot() requires crossOriginIsolated === true " + "(serve the host page with COOP: same-origin + COEP: credentialless|require-corp). " + "SharedArrayBuffer + Atomics.wait — which the kernel needs — are unavailable otherwise.");
|
|
1445
|
+
}
|
|
1446
|
+
const runtime = options.runtime;
|
|
1447
|
+
if (!runtime) {
|
|
1448
|
+
throw new Error("ERR_WC_MISSING_RUNTIME_WIRING: WebContainer.boot({ runtime }) requires the browser-runtime wiring " + "(viteWorkerUrl, serviceWorkerUrl, previewBase, the WASM URLs/sources, and the dependencyClosure). " + "The host page builds these (mountPackageClosure is a Node-side op) and serves them; the facade stays browser-pure.");
|
|
1449
|
+
}
|
|
1450
|
+
const workdirName = options.workdirName ?? "proj";
|
|
1451
|
+
const workdir = cleanPath2(`/${workdirName}`);
|
|
1452
|
+
const memfs = new MemoryFS;
|
|
1453
|
+
memfs.mkdirp(workdir);
|
|
1454
|
+
const container = new WebContainer(workdir, memfs, runtime);
|
|
1455
|
+
liveContainer = container;
|
|
1456
|
+
try {
|
|
1457
|
+
await container.#registerServiceWorker();
|
|
1458
|
+
} catch (err) {
|
|
1459
|
+
liveContainer = undefined;
|
|
1460
|
+
throw err;
|
|
1461
|
+
}
|
|
1462
|
+
return container;
|
|
1463
|
+
}
|
|
1464
|
+
async mount(tree, options = {}) {
|
|
1465
|
+
this.#assertLive();
|
|
1466
|
+
const root = options.mountPoint ? cleanPath2(options.mountPoint) : this.workdir;
|
|
1467
|
+
this.#memfs.mkdirp(root);
|
|
1468
|
+
this.#writeTree(tree, root);
|
|
1469
|
+
}
|
|
1470
|
+
#writeTree(tree, dir) {
|
|
1471
|
+
for (const [name, node] of Object.entries(tree)) {
|
|
1472
|
+
const childPath = `${dir}/${name}`;
|
|
1473
|
+
if ("directory" in node) {
|
|
1474
|
+
this.#memfs.mkdirp(childPath);
|
|
1475
|
+
this.#writeTree(node.directory, childPath);
|
|
1476
|
+
} else if ("file" in node) {
|
|
1477
|
+
if ("symlink" in node.file) {
|
|
1478
|
+
throw new Error(`${NOT_IMPLEMENTED2}: symlink nodes in a FileSystemTree are not yet materialized by @start.dev/container ` + `(at ${childPath} -> ${node.file.symlink}). The current mount path writes regular files + directories only; ` + `symlink mount is a documented seam, not faked.`);
|
|
1479
|
+
}
|
|
1480
|
+
const contents = node.file.contents;
|
|
1481
|
+
const byteLength = typeof contents === "string" ? textEncoder.encode(contents).byteLength : contents.byteLength;
|
|
1482
|
+
if (byteLength > MAX_FILE_BYTES) {
|
|
1483
|
+
throw new Error(`ERR_WC_FS_FILE_TOO_LARGE: mounted file ${childPath} is ${byteLength} bytes, exceeding the ${MAX_FILE_BYTES}-byte (1 MiB) limit. ` + `The mount is rejected rather than silently truncated.`);
|
|
1484
|
+
}
|
|
1485
|
+
this.#memfs.writeFile(childPath, contents);
|
|
1486
|
+
} else {
|
|
1487
|
+
throw new Error(`EINVAL: FileSystemTree node at ${childPath} is neither a file nor a directory node.`);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
async spawn(command, args = [], options = {}) {
|
|
1492
|
+
this.#assertLive();
|
|
1493
|
+
const cwd = options.cwd ? cleanPath2(resolvePath2(this.workdir, options.cwd)) : this.workdir;
|
|
1494
|
+
const plan = resolveSpawn(this.#memfs, cwd, command, args);
|
|
1495
|
+
if (plan.kind === "unsupported") {
|
|
1496
|
+
throw new Error(plan.message);
|
|
1497
|
+
}
|
|
1498
|
+
if (plan.kind === "dev-server") {
|
|
1499
|
+
if (this.#devServer) {
|
|
1500
|
+
throw new Error("ERR_WC_DEV_SERVER_ALREADY_RUNNING: a dev server is already running in this WebContainer. " + "Multiple concurrent dev servers are not yet supported; kill the existing one first.");
|
|
1501
|
+
}
|
|
1502
|
+
return this.#startDevServer(options);
|
|
1503
|
+
}
|
|
1504
|
+
if (plan.kind === "node") {
|
|
1505
|
+
return this.#startNodeProcess(plan, options);
|
|
1506
|
+
}
|
|
1507
|
+
return this.#startShellProcess(plan, options);
|
|
1508
|
+
}
|
|
1509
|
+
on(event, listener) {
|
|
1510
|
+
return this.#emitter.on(event, listener);
|
|
1511
|
+
}
|
|
1512
|
+
teardown() {
|
|
1513
|
+
if (this.#torn)
|
|
1514
|
+
return;
|
|
1515
|
+
this.#torn = true;
|
|
1516
|
+
if (this.#devServer) {
|
|
1517
|
+
try {
|
|
1518
|
+
this.#devServer.kill();
|
|
1519
|
+
} catch {}
|
|
1520
|
+
this.#devServer = undefined;
|
|
1521
|
+
}
|
|
1522
|
+
if (this.#worker) {
|
|
1523
|
+
try {
|
|
1524
|
+
this.#worker.terminate();
|
|
1525
|
+
} catch {}
|
|
1526
|
+
this.#worker = undefined;
|
|
1527
|
+
}
|
|
1528
|
+
if (this.#pageMessageHandler && typeof removeEventListener === "function") {
|
|
1529
|
+
removeEventListener("message", this.#pageMessageHandler);
|
|
1530
|
+
this.#pageMessageHandler = undefined;
|
|
1531
|
+
}
|
|
1532
|
+
if (this.#serviceWorkerRegistration) {
|
|
1533
|
+
this.#serviceWorkerRegistration.unregister().catch(() => {});
|
|
1534
|
+
this.#serviceWorkerRegistration = undefined;
|
|
1535
|
+
}
|
|
1536
|
+
for (const key of Object.keys(this.routes))
|
|
1537
|
+
delete this.routes[Number(key)];
|
|
1538
|
+
for (const watcher of this.#fsWatchers) {
|
|
1539
|
+
try {
|
|
1540
|
+
watcher.close();
|
|
1541
|
+
} catch {}
|
|
1542
|
+
}
|
|
1543
|
+
this.#fsWatchers.clear();
|
|
1544
|
+
this.#emitter.clear();
|
|
1545
|
+
if (liveContainer === this)
|
|
1546
|
+
liveContainer = undefined;
|
|
1547
|
+
}
|
|
1548
|
+
export() {
|
|
1549
|
+
throw new Error(`${NOT_IMPLEMENTED2}: WebContainer.export() is not yet implemented by @start.dev/container. ` + `MemoryFS.snapshot() exists but the @webcontainer/api export format/options are not yet mapped (WS-future).`);
|
|
1550
|
+
}
|
|
1551
|
+
#assertLive() {
|
|
1552
|
+
if (this.#torn)
|
|
1553
|
+
throw new Error("ERR_WC_TORN_DOWN: this WebContainer has been torn down.");
|
|
1554
|
+
}
|
|
1555
|
+
get #previewUrl() {
|
|
1556
|
+
const base = this.#runtime.previewBase;
|
|
1557
|
+
const origin = typeof location !== "undefined" ? location.origin : "";
|
|
1558
|
+
return `${origin}${base}`;
|
|
1559
|
+
}
|
|
1560
|
+
async#registerServiceWorker() {
|
|
1561
|
+
const sw = navigator?.serviceWorker;
|
|
1562
|
+
if (!sw) {
|
|
1563
|
+
throw new Error("ERR_WC_NO_SERVICE_WORKER: navigator.serviceWorker is unavailable; the preview pipeline requires a Service Worker.");
|
|
1564
|
+
}
|
|
1565
|
+
const base = this.#runtime.previewBase;
|
|
1566
|
+
const reg = await sw.register(this.#runtime.serviceWorkerUrl, {
|
|
1567
|
+
type: "module",
|
|
1568
|
+
scope: base,
|
|
1569
|
+
updateViaCache: "none"
|
|
1570
|
+
});
|
|
1571
|
+
this.#serviceWorkerRegistration = reg;
|
|
1572
|
+
await new Promise((resolve) => {
|
|
1573
|
+
const poll = setInterval(() => {
|
|
1574
|
+
if (reg.active && reg.active.state === "activated") {
|
|
1575
|
+
clearInterval(poll);
|
|
1576
|
+
resolve();
|
|
1577
|
+
}
|
|
1578
|
+
}, 50);
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
async#startDevServer(options) {
|
|
1582
|
+
const runtime = this.#runtime;
|
|
1583
|
+
const worker = makeWorker(runtime.viteWorkerUrl);
|
|
1584
|
+
this.#worker = worker;
|
|
1585
|
+
const proc = new DevServerProcess(worker);
|
|
1586
|
+
this.#devServer = proc;
|
|
1587
|
+
const depFiles = { ...this.#runtime.dependencyClosure };
|
|
1588
|
+
const appFiles = this.#collectProjectFiles(this.workdir);
|
|
1589
|
+
worker.addEventListener("message", (event) => {
|
|
1590
|
+
const msg = event.data;
|
|
1591
|
+
if (!msg || typeof msg !== "object")
|
|
1592
|
+
return;
|
|
1593
|
+
if (msg.type === "httpResponse") {
|
|
1594
|
+
const resolve = this.#pendingHttp.get(msg.id);
|
|
1595
|
+
if (resolve) {
|
|
1596
|
+
this.#pendingHttp.delete(msg.id);
|
|
1597
|
+
resolve(msg);
|
|
1598
|
+
}
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
if (msg.type === "hmr-server-message") {
|
|
1602
|
+
const data = typeof msg.payload === "string" ? msg.payload : JSON.stringify(msg.payload);
|
|
1603
|
+
this.#broadcastToPreviewFrames({ type: "hmr-to-iframe", data });
|
|
1604
|
+
if (typeof postMessage === "function") {
|
|
1605
|
+
try {
|
|
1606
|
+
postMessage({ type: "hmr-to-iframe", data }, "*");
|
|
1607
|
+
} catch {}
|
|
1608
|
+
}
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
if (msg.type === "progress") {
|
|
1612
|
+
proc.pushOutput(`[vite] ${msg.stage}${msg.error ? `: ${msg.error}` : ""}`);
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
worker.onerror = (event) => {
|
|
1617
|
+
const message = event.message ?? "vite worker error";
|
|
1618
|
+
this.#emitter.emit("error", { message });
|
|
1619
|
+
proc.pushOutput(`[vite:error] ${message}`);
|
|
1620
|
+
};
|
|
1621
|
+
this.#installPageMessageHandler(worker, proc);
|
|
1622
|
+
const ready = await new Promise((resolve, reject) => {
|
|
1623
|
+
const timer = setTimeout(() => reject(new Error("vite preview boot timed out (180s)")), 180000);
|
|
1624
|
+
const onReady = (event) => {
|
|
1625
|
+
const m = event.data;
|
|
1626
|
+
if (m && m.type === "ready") {
|
|
1627
|
+
clearTimeout(timer);
|
|
1628
|
+
resolve(m);
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
worker.addEventListener("message", onReady);
|
|
1632
|
+
worker.addEventListener("error", (e) => {
|
|
1633
|
+
clearTimeout(timer);
|
|
1634
|
+
reject(new Error("vite worker error: " + (e.message ?? "unknown")));
|
|
1635
|
+
});
|
|
1636
|
+
worker.postMessage({
|
|
1637
|
+
type: "init",
|
|
1638
|
+
mode: "preview",
|
|
1639
|
+
base: runtime.previewBase,
|
|
1640
|
+
files: depFiles,
|
|
1641
|
+
appFiles,
|
|
1642
|
+
pluginReact: true,
|
|
1643
|
+
esbuildWasmUrl: runtime.esbuildWasmUrl,
|
|
1644
|
+
rollupWasmUrl: runtime.rollupWasmUrl,
|
|
1645
|
+
rollupBindingsSource: runtime.rollupBindingsSource,
|
|
1646
|
+
rollupParseAstSharedSource: runtime.rollupParseAstSharedSource
|
|
1647
|
+
});
|
|
1648
|
+
});
|
|
1649
|
+
if (!ready.ok) {
|
|
1650
|
+
throw new Error("vite preview boot failed: " + (ready.error ?? "unknown"));
|
|
1651
|
+
}
|
|
1652
|
+
proc.pushOutput(`[vite] dev server ready (esbuild ${ready.esbuildVersion ?? "?"})`);
|
|
1653
|
+
const port = DEV_SERVER_PORT;
|
|
1654
|
+
const url = this.#previewUrl;
|
|
1655
|
+
this.routes[port] = url;
|
|
1656
|
+
this.#emitter.emit("port", port, "open", url);
|
|
1657
|
+
this.#emitter.emit("server-ready", port, url);
|
|
1658
|
+
return proc;
|
|
1659
|
+
}
|
|
1660
|
+
async#startNodeProcess(plan, options) {
|
|
1661
|
+
const workerUrl = this.#runtime.processWorkerUrl;
|
|
1662
|
+
if (!workerUrl) {
|
|
1663
|
+
throw new Error(`${NOT_IMPLEMENTED2}: spawn resolved "${plan.label}" to a real Node process, but BootOptions.runtime.processWorkerUrl ` + `was not provided. The host page must serve the bundled streaming-process-host worker and pass its URL so the ` + `facade can run real node/npx/bin commands. (The dev-server path uses viteWorkerUrl and is unaffected.)`);
|
|
1664
|
+
}
|
|
1665
|
+
const env = this.#mergeEnv(options.env);
|
|
1666
|
+
const proc = new StreamingWebContainerProcess;
|
|
1667
|
+
const handle = spawnStreamingNodeProcess(this.#memfs, {
|
|
1668
|
+
workerUrl,
|
|
1669
|
+
projectRoot: this.workdir,
|
|
1670
|
+
entry: plan.entry,
|
|
1671
|
+
argv: plan.args,
|
|
1672
|
+
env,
|
|
1673
|
+
onStdout: (chunk) => proc.push(chunk),
|
|
1674
|
+
onStderr: (chunk) => proc.push(chunk)
|
|
1675
|
+
});
|
|
1676
|
+
proc.attach(handle);
|
|
1677
|
+
return proc;
|
|
1678
|
+
}
|
|
1679
|
+
async#startShellProcess(plan, options) {
|
|
1680
|
+
const workerUrl = this.#runtime.shellWorkerUrl;
|
|
1681
|
+
if (!workerUrl) {
|
|
1682
|
+
throw new Error(`${NOT_IMPLEMENTED2}: spawn resolved "${plan.label}" to the real shell, but BootOptions.runtime.shellWorkerUrl ` + `was not provided. The host page must serve the bundled shell-host worker (real @wc/shell / just-bash) and pass its URL.`);
|
|
1683
|
+
}
|
|
1684
|
+
const env = this.#mergeEnv(options.env);
|
|
1685
|
+
const proc = new StreamingWebContainerProcess;
|
|
1686
|
+
const handle = spawnShellProcess(this.#memfs, {
|
|
1687
|
+
workerUrl,
|
|
1688
|
+
commandLine: plan.commandLine,
|
|
1689
|
+
cwd: plan.cwd,
|
|
1690
|
+
env,
|
|
1691
|
+
onStdout: (chunk) => proc.push(chunk),
|
|
1692
|
+
onStderr: (chunk) => proc.push(chunk)
|
|
1693
|
+
});
|
|
1694
|
+
proc.attach(handle);
|
|
1695
|
+
return proc;
|
|
1696
|
+
}
|
|
1697
|
+
#mergeEnv(env) {
|
|
1698
|
+
const out = {};
|
|
1699
|
+
if (env) {
|
|
1700
|
+
for (const [k, v] of Object.entries(env))
|
|
1701
|
+
out[k] = String(v);
|
|
1702
|
+
}
|
|
1703
|
+
return out;
|
|
1704
|
+
}
|
|
1705
|
+
#collectProjectFiles(dir) {
|
|
1706
|
+
const out = {};
|
|
1707
|
+
const walk = (d) => {
|
|
1708
|
+
for (const entry of this.#memfs.list(d)) {
|
|
1709
|
+
const p = `${d}/${entry.name}`;
|
|
1710
|
+
if (entry.kind === "dir")
|
|
1711
|
+
walk(p);
|
|
1712
|
+
else if (entry.kind === "file")
|
|
1713
|
+
out[p] = this.#memfs.readFile(p);
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
if (this.#memfs.exists(dir))
|
|
1717
|
+
walk(dir);
|
|
1718
|
+
return out;
|
|
1719
|
+
}
|
|
1720
|
+
#viteDispatch(req) {
|
|
1721
|
+
return new Promise((resolve) => {
|
|
1722
|
+
const id = "h" + ++this.#httpSeq;
|
|
1723
|
+
this.#pendingHttp.set(id, resolve);
|
|
1724
|
+
this.#worker.postMessage({ type: "httpRequest", id, method: req.method, url: req.url, headers: req.headers });
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
#broadcastToPreviewFrames(message) {
|
|
1728
|
+
if (typeof document === "undefined")
|
|
1729
|
+
return;
|
|
1730
|
+
const frames = document.querySelectorAll("iframe");
|
|
1731
|
+
const base = this.#runtime.previewBase;
|
|
1732
|
+
frames.forEach((frame) => {
|
|
1733
|
+
const el = frame;
|
|
1734
|
+
if (el.src && (el.src.endsWith(base) || el.src.includes(`${base}index.html`))) {
|
|
1735
|
+
el.contentWindow?.postMessage(message, "*");
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
#frameForSource(source) {
|
|
1740
|
+
if (!source || typeof document === "undefined")
|
|
1741
|
+
return;
|
|
1742
|
+
const frames = document.querySelectorAll("iframe");
|
|
1743
|
+
for (const frame of Array.from(frames)) {
|
|
1744
|
+
const el = frame;
|
|
1745
|
+
if (el.contentWindow === source)
|
|
1746
|
+
return el;
|
|
1747
|
+
}
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
#installPageMessageHandler(worker, proc) {
|
|
1751
|
+
const base = this.#runtime.previewBase;
|
|
1752
|
+
const handler = (event) => {
|
|
1753
|
+
const msg = event.data;
|
|
1754
|
+
if (!msg || typeof msg !== "object")
|
|
1755
|
+
return;
|
|
1756
|
+
const frame = this.#frameForSource(event.source);
|
|
1757
|
+
if (msg.type === "preview-dispatch") {
|
|
1758
|
+
this.#respondToFrame(frame, msg);
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
if (msg.type === "hmr-from-iframe") {
|
|
1762
|
+
worker.postMessage({ type: "hmr-client-message", data: msg.data });
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
if (msg.type === "hmr-iframe-open") {
|
|
1766
|
+
worker.postMessage({ type: "hmr-client-open" });
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
this.#pageMessageHandler = handler;
|
|
1771
|
+
if (typeof addEventListener === "function")
|
|
1772
|
+
addEventListener("message", handler);
|
|
1773
|
+
}
|
|
1774
|
+
async#respondToFrame(frame, msg) {
|
|
1775
|
+
let path = msg.url;
|
|
1776
|
+
try {
|
|
1777
|
+
const u = new URL(msg.url);
|
|
1778
|
+
path = u.pathname + u.search;
|
|
1779
|
+
} catch {}
|
|
1780
|
+
const headers = Object.assign({}, msg.headers, { host: `localhost:${DEV_SERVER_PORT}` });
|
|
1781
|
+
const res = await this.#viteDispatch({ method: msg.method, url: path, headers });
|
|
1782
|
+
const outHeaders = [];
|
|
1783
|
+
const incoming = res.headers ?? {};
|
|
1784
|
+
for (const k of Object.keys(incoming)) {
|
|
1785
|
+
const lk = k.toLowerCase();
|
|
1786
|
+
if (lk === "set-cookie" || lk === "cross-origin-resource-policy")
|
|
1787
|
+
continue;
|
|
1788
|
+
outHeaders.push([k, String(incoming[k])]);
|
|
1789
|
+
}
|
|
1790
|
+
outHeaders.push(["cross-origin-resource-policy", "same-origin"]);
|
|
1791
|
+
const body = res.body instanceof ArrayBuffer ? res.body : null;
|
|
1792
|
+
frame?.contentWindow?.postMessage({ type: "preview-response", rid: msg.rid, status: res.status, statusText: res.statusText, headers: outHeaders, body }, "*", body ? [body] : []);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
export {
|
|
1796
|
+
reloadPreview,
|
|
1797
|
+
isPreviewMessage,
|
|
1798
|
+
createSameOriginRelayTransport,
|
|
1799
|
+
configureAPIKey,
|
|
1800
|
+
auth,
|
|
1801
|
+
WebContainer,
|
|
1802
|
+
RELAY_PROXY_PATH,
|
|
1803
|
+
PreviewMessageType,
|
|
1804
|
+
MAX_FILE_BYTES
|
|
1805
|
+
};
|