@adep/web-container 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # @adep/web-container — AgentDeploy 浏览器原生开发环境 SDK
2
+
3
+ > **浏览器端开发/运行环境 SDK**(任务单 FN-012):虚拟文件系统 + 进程/shell + JS 运行时适配接口 + npm-relay 客户端。对标 [WebContainers](https://webcontainers.io) API——**Node 可测、浏览器可跑**。
4
+
5
+ ```bash
6
+ npm install @adep/web-container
7
+ # 或
8
+ pnpm add @adep/web-container
9
+ ```
10
+
11
+ ## 能力总览
12
+
13
+ | 能力 | 说明 |
14
+ | ------------- | ------------------------- |
15
+ | 虚拟文件系统 (VFS) | 内存文件树:读写 / 查找 / 变更通知 |
16
+ | 进程 / shell | 在虚拟环境中执行命令、管道输入输出 |
17
+ | 运行时适配接口 | 用适配器接宿主运行时(预览 / 沙箱执行) |
18
+ | npm-relay 客户端 | 经平台 npm-relay 安装 / 解析前端依赖 |
19
+
20
+ ## 用法
21
+
22
+ ```ts
23
+ import { createWebContainer } from '@adep/web-container'
24
+
25
+ const container = await createWebContainer({
26
+ // 传入文件系统与运行时适配器
27
+ })
28
+
29
+ await container.fs.mkdir('/hello')
30
+ await container.fs.writeFile('/hello/index.txt', 'hi')
31
+ const text = await container.fs.readFile('/hello/index.txt', 'utf8')
32
+ ```
33
+
34
+ ## 说明
35
+
36
+ - 面向浏览器,同时成 Node 可测;依赖 `@adep/types` 提供类型契约。
37
+
38
+ - 需要浏览器内作为 `<script>` 全局加载的 IIFE 变体见 `scripts.build:sdk`(`dist/web-container.iife.js`,`globalName = AdepWebContainer`)。
39
+
@@ -0,0 +1,26 @@
1
+ /**
2
+ * WebContainer(FN-012 对外 SDK 主体)——对标 WebContainers `bootstrap()` 返回的实例。
3
+ *
4
+ * 组装虚拟文件系统 + shell + JS 运行时适配 + npm 客户端,对外暴露:
5
+ * `mount` / `writeFile` / `readFile` / `listDirectory` / `createDirectory` / `deleteFile` /
6
+ * `exists` / `spawn` / `run` / `on` / `off` / `install` / `registries` / `close`。
7
+ *
8
+ * `spawn` 走内置 shell(重定向/管道/内建命令),`node`/`js` 命令走可替换 JS 运行时适配
9
+ * (QuickJS WASM 浏览器端 / Node vm 测试端)。`bootstrap` 是唯一工厂。
10
+ */
11
+ import type { WebContainer, WebContainerDirectoryTree } from '@adep/types';
12
+ import type { CloudFetch } from '@adep/types';
13
+ import { type JsRuntime } from './shell';
14
+ export interface BootstrapOptions {
15
+ /**
16
+ * 平台 npm 中继基础路径(浏览器端相对当前源,如 `/api/v1/npm-relay`)。
17
+ */
18
+ npmRelayBaseUrl: string;
19
+ /** 初始目录树(同 `mount` 入参)。 */
20
+ initialTree?: WebContainerDirectoryTree;
21
+ /** JS 运行时适配(缺省禁用 `node` 命令;提供 QuickJS WASM 实现后注入)。 */
22
+ runtime?: JsRuntime;
23
+ /** npm 中继网络实现(缺省 globalThis.fetch;测试注入假中继)。 */
24
+ fetchImpl?: CloudFetch;
25
+ }
26
+ export declare function bootstrap(options: BootstrapOptions): WebContainer;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `@adep/web-container` —— adep 平台浏览器原生开发环境对外 SDK(FN-012)。
3
+ *
4
+ * 对标 WebContainers API(`bootstrap` / `mount` / `spawn` / `writeFile` / `on`):
5
+ * - 虚拟文件系统(`VirtualFileSystem`);
6
+ * - shell(内建命令 + 重定向/管道,`Shell` / `JsRuntime` 适配接口);
7
+ * - `bootstrap()` 组装完整容器,浏览器端缺省注入 Web Worker 沙箱运行时(`node` 命令可执行 JS);
8
+ * - npm 经平台 `/npm-relay/*` 中继取真实 registry(`createNpmClient`)。
9
+ *
10
+ * Node 可测、浏览器可跑(零 node 依赖)。Node vm 实现(`vm.ts`)不在此导出,
11
+ * 以免把 `node:vm` 带进浏览器包;浏览器端缺省走 `createWorkerRuntime`(Web Worker),
12
+ * 也可整体替换为 QuickJS WASM 实现 `JsRuntime`。
13
+ */
14
+ export { bootstrap } from './container';
15
+ export type { BootstrapOptions } from './container';
16
+ export { VirtualFileSystem } from './vfs';
17
+ export { Shell } from './shell';
18
+ export type { JsRuntime, ShellResult } from './shell';
19
+ export { createWorkerRuntime, evaluateSource } from './worker-runtime';
20
+ export type { WorkerRuntimeOptions, WorkerLike } from './worker-runtime';
21
+ export { createNpmClient, parseSpec } from './npm-client';
22
+ export type { NpmClientOptions, NpmInstallResult } from './npm-client';
23
+ export * as posix from './path';
24
+ export type { WebContainer, WebContainerDirectoryTree, WebContainerEventMap, WebContainerFsEntry, WebContainerProcess, } from '@adep/types';
package/dist/index.js ADDED
@@ -0,0 +1,899 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // packages/web-container/src/path.ts
12
+ var path_exports = {};
13
+ __export(path_exports, {
14
+ SEP: () => SEP,
15
+ basename: () => basename,
16
+ dirname: () => dirname,
17
+ extname: () => extname,
18
+ isAbsolute: () => isAbsolute,
19
+ isValidSegment: () => isValidSegment,
20
+ join: () => join,
21
+ normalize: () => normalize
22
+ });
23
+ function normalize(path) {
24
+ const isAbs = path.startsWith(SEP);
25
+ const parts = path.split(SEP);
26
+ const out = [];
27
+ for (const part of parts) {
28
+ if (part === "" || part === ".") continue;
29
+ if (part === "..") {
30
+ if (out.length > 0) out.pop();
31
+ else if (!isAbs) out.push("..");
32
+ continue;
33
+ }
34
+ out.push(part);
35
+ }
36
+ const joined = out.join(SEP);
37
+ const rooted = isAbs || joined.startsWith("..");
38
+ if (rooted) return `${SEP}${joined.replace(/^\/+/, "")}`;
39
+ return joined === "" ? "/" : joined;
40
+ }
41
+ function isAbsolute(path) {
42
+ return path.startsWith(SEP);
43
+ }
44
+ function join(base, ...paths) {
45
+ let result = base;
46
+ for (const p of paths) {
47
+ if (isAbsolute(p)) result = p;
48
+ else if (result === "" || result === SEP) result = `${SEP}${p.replace(/^\/+/, "")}`;
49
+ else result = `${result.replace(/\/+$/, "")}${SEP}${p.replace(/^\/+/, "")}`;
50
+ }
51
+ return normalize(result);
52
+ }
53
+ function dirname(path) {
54
+ const normalized = normalize(path);
55
+ if (normalized === SEP) return SEP;
56
+ const index = normalized.lastIndexOf(SEP);
57
+ if (index <= 0) return SEP;
58
+ return normalized.slice(0, index);
59
+ }
60
+ function basename(path) {
61
+ const normalized = normalize(path);
62
+ if (normalized === SEP) return "";
63
+ const index = normalized.lastIndexOf(SEP);
64
+ return index === -1 ? normalized : normalized.slice(index + 1);
65
+ }
66
+ function extname(path) {
67
+ const name = basename(path);
68
+ const index = name.lastIndexOf(".");
69
+ return index <= 0 ? "" : name.slice(index);
70
+ }
71
+ function isValidSegment(name) {
72
+ return name.length > 0 && name !== "." && name !== ".." && !name.includes(SEP) && name !== "\\";
73
+ }
74
+ var SEP;
75
+ var init_path = __esm({
76
+ "packages/web-container/src/path.ts"() {
77
+ "use strict";
78
+ SEP = "/";
79
+ }
80
+ });
81
+
82
+ // packages/web-container/src/preview.ts
83
+ var preview_exports = {};
84
+ __export(preview_exports, {
85
+ buildPreviewDocument: () => buildPreviewDocument,
86
+ createPreviewServer: () => createPreviewServer,
87
+ inlineAssets: () => inlineAssets
88
+ });
89
+ function resolveLocal(vfs, src, baseDir) {
90
+ if (EXTERNAL_SRC.test(src) || /^data:|^blob:|^#/i.test(src)) return null;
91
+ const abs = src.startsWith("/") ? normalize(src) : join(baseDir, src);
92
+ if (!vfs.exists(abs) || vfs.isDirectory(abs)) return null;
93
+ return abs;
94
+ }
95
+ function inlineAssets(html, vfs, baseDir) {
96
+ let out = html.replace(
97
+ /<script\b([^>]*?)\bsrc\s*=\s*(["'])(.*?)\2([^>]*)><\/script>/gi,
98
+ (whole, pre, _q, orig, post) => {
99
+ if (/\btype\s*=\s*["']module["']/i.test(pre + post)) return whole;
100
+ const abs = resolveLocal(vfs, orig, baseDir);
101
+ if (abs === null) return whole;
102
+ const code = vfs.readFile(abs);
103
+ return `<script${pre}${post}>${code}</script>`;
104
+ }
105
+ );
106
+ out = out.replace(
107
+ /<link\b([^>]*?)\brel\s*=\s*(["'])stylesheet\2([^>]*?)\bhref\s*=\s*(["'])(.*?)\4([^>]*?)\/?\s*>/gi,
108
+ (whole, _pre, _q, _post, _r, orig) => {
109
+ const abs = resolveLocal(vfs, orig, baseDir);
110
+ if (abs === null) return whole;
111
+ return `<style>${vfs.readFile(abs)}</style>`;
112
+ }
113
+ );
114
+ return out;
115
+ }
116
+ function directoryListing(cwd, vfs) {
117
+ const rows = vfs.listDirectory(cwd).map((entry) => `<li>${entry.type === "file" ? "\u{1F4C4}" : "\u{1F4C1}"} <code>${entry.path}</code></li>`).join("");
118
+ return `<!doctype html><html><head><meta charset="utf-8"><title>\u9884\u89C8 ${cwd}</title></head>
119
+ <body><h3>\u9884\u89C8\u5DE5\u4F5C\u533A\uFF1A<code>${cwd}</code></h3><p>\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 <code>index.html</code>\uFF0C\u5217\u51FA\u5982\u4E0B\uFF1A</p>
120
+ <ul>${rows}</ul></body></html>`;
121
+ }
122
+ function buildPreviewDocument(vfs, options) {
123
+ const entryAbs = join(options.cwd, options.entry);
124
+ const html = vfs.exists(entryAbs) && !vfs.isDirectory(entryAbs) ? vfs.readFile(entryAbs) ?? "" : directoryListing(options.cwd, vfs);
125
+ let doc = inlineAssets(html, vfs, dirname(entryAbs));
126
+ const hmr = `<script>(function(){if(typeof BroadcastChannel==='undefined')return;var bc=new BroadcastChannel(${JSON.stringify(
127
+ options.channelName
128
+ )});bc.onmessage=function(e){var d=e.data;if(d&&d.type==='update'&&d.url&&d.url!==location.href){location.replace(d.url);}};})();</script>`;
129
+ if (/<\/body>/i.test(doc)) doc = doc.replace(/<\/(body)>/i, `${hmr}</$1>`);
130
+ else doc += hmr;
131
+ return doc;
132
+ }
133
+ function createPreviewServer(vfs, options = {}) {
134
+ const cwd = normalize(options.cwd ?? "/");
135
+ const entry = options.entry ?? "index.html";
136
+ const createObjectURL = options.createObjectURL ?? ((blob) => URL.createObjectURL(blob));
137
+ const revokeObjectURL = options.revokeObjectURL ?? ((url) => URL.revokeObjectURL(url));
138
+ const channelName = `adep:preview:${Math.random().toString(36).slice(2)}`;
139
+ const port = 5173;
140
+ let currentUrl = "";
141
+ let lastDoc = null;
142
+ let bc = null;
143
+ const broadcast = (url) => {
144
+ try {
145
+ bc ??= new BroadcastChannel(channelName);
146
+ bc.postMessage({ type: "update", url });
147
+ } catch {
148
+ }
149
+ };
150
+ const serve = () => {
151
+ const doc = buildPreviewDocument(vfs, { cwd, entry, channelName });
152
+ const changed = doc !== lastDoc;
153
+ lastDoc = doc;
154
+ if (currentUrl !== "") revokeObjectURL(currentUrl);
155
+ currentUrl = createObjectURL(new Blob([doc], { type: "text/html" }));
156
+ return { url: currentUrl, changed };
157
+ };
158
+ serve();
159
+ return {
160
+ get url() {
161
+ return currentUrl;
162
+ },
163
+ get port() {
164
+ return port;
165
+ },
166
+ touch() {
167
+ const { url: next, changed } = serve();
168
+ if (changed) broadcast(next);
169
+ },
170
+ stop() {
171
+ if (currentUrl !== "") revokeObjectURL(currentUrl);
172
+ currentUrl = "";
173
+ bc?.close();
174
+ bc = null;
175
+ return Promise.resolve();
176
+ }
177
+ };
178
+ }
179
+ var EXTERNAL_SRC;
180
+ var init_preview = __esm({
181
+ "packages/web-container/src/preview.ts"() {
182
+ "use strict";
183
+ init_path();
184
+ EXTERNAL_SRC = /(?:^https?:)?\/\//i;
185
+ }
186
+ });
187
+
188
+ // packages/web-container/src/vfs.ts
189
+ init_path();
190
+ var ensureAbs = (path) => path.startsWith("/") ? normalize(path) : `/${normalize(path).replace(/^\/+/, "")}`;
191
+ var ROOT = "/";
192
+ var isRoot = (abs) => abs === ROOT;
193
+ var VirtualFileSystem = class {
194
+ nodes = /* @__PURE__ */ new Map([[ROOT, { kind: "dir" }]]);
195
+ /** 挂载一棵目录树(WebContainers `mount` 入参形态)。 */
196
+ mount(tree) {
197
+ for (const [name, value] of Object.entries(tree)) {
198
+ if (!isValidSegment(name)) continue;
199
+ const dir = `/`;
200
+ if (typeof value === "string") {
201
+ this.writeFile(`/${name}`, value);
202
+ } else if (value === null) {
203
+ this.mkdir(`/${name}`);
204
+ } else {
205
+ this.mkdir(`/${name}`);
206
+ this.mountWithin(`/${name}`, value);
207
+ }
208
+ void dir;
209
+ }
210
+ }
211
+ mountWithin(base, tree) {
212
+ for (const [name, value] of Object.entries(tree)) {
213
+ if (!isValidSegment(name)) continue;
214
+ if (typeof value === "string") this.writeFile(`${base}/${name}`, value);
215
+ else if (value === null) this.mkdir(`${base}/${name}`);
216
+ else {
217
+ this.mkdir(`${base}/${name}`);
218
+ this.mountWithin(`${base}/${name}`, value);
219
+ }
220
+ }
221
+ }
222
+ /** 写文件(自动补父目录;`../` 与绝对越界段交由 normalize 折叠)。 */
223
+ writeFile(path, contents) {
224
+ const abs = ensureAbs(path);
225
+ this.ensureParent(abs);
226
+ this.nodes.set(abs, { kind: "file", contents });
227
+ }
228
+ /** 读文件文本;路径是目录或不存在时抛错。 */
229
+ readFile(path) {
230
+ const abs = ensureAbs(path);
231
+ const node = this.nodes.get(abs);
232
+ if (node === void 0) throw new Error(`ENOENT: \u6587\u4EF6\u4E0D\u5B58\u5728 ${abs}`);
233
+ if (node.kind !== "file") throw new Error(`EISDIR: ${abs} \u662F\u76EE\u5F55\u4E0D\u662F\u6587\u4EF6`);
234
+ return node.contents;
235
+ }
236
+ /** 是否存在(文件或目录)。 */
237
+ exists(path) {
238
+ return this.nodes.has(ensureAbs(path));
239
+ }
240
+ /** 是否为目录。 */
241
+ isDirectory(path) {
242
+ return this.nodes.get(ensureAbs(path))?.kind === "dir";
243
+ }
244
+ /** 创建目录(缺省 recursive,自动补父)。 */
245
+ mkdir(path, recursive = true) {
246
+ const abs = ensureAbs(path);
247
+ if (recursive) this.ensureParent(abs);
248
+ else if (!isRoot(dirname(abs)) && !this.nodes.has(dirname(abs)))
249
+ throw new Error(`ENOENT: \u7236\u76EE\u5F55\u4E0D\u5B58\u5728 ${dirname(abs)}`);
250
+ if (this.nodes.has(abs)) {
251
+ if (this.nodes.get(abs).kind !== "dir") throw new Error(`EEXIST: ${abs} \u5DF2\u5B58\u5728\u4E14\u4E0D\u662F\u76EE\u5F55`);
252
+ return;
253
+ }
254
+ this.nodes.set(abs, { kind: "dir" });
255
+ }
256
+ /** 列目录条目;目录不存在抛错。条目 path 为相对宿主根(POSIX)。 */
257
+ listDirectory(path) {
258
+ const abs = isRoot(ensureAbs(path)) ? ROOT : ensureAbs(path);
259
+ if (!this.exists(abs)) throw new Error(`ENOENT: \u76EE\u5F55\u4E0D\u5B58\u5728 ${abs}`);
260
+ if (!this.isDirectory(abs)) throw new Error(`ENOTDIR: ${abs} \u4E0D\u662F\u76EE\u5F55`);
261
+ const prefix = abs === ROOT ? ROOT : `${abs}/`;
262
+ const names = /* @__PURE__ */ new Set();
263
+ for (const key of this.nodes.keys()) {
264
+ if (key === ROOT) continue;
265
+ if (key.startsWith(prefix)) {
266
+ const rest = key.slice(prefix.length);
267
+ if (rest === "" || rest.includes("/")) continue;
268
+ names.add(rest);
269
+ }
270
+ }
271
+ const parent = abs === ROOT ? "" : abs;
272
+ const namesToSort = [...names];
273
+ namesToSort.sort();
274
+ return namesToSort.map((name) => ({
275
+ path: `${parent}/${name}`,
276
+ type: this.nodes.get(`${parent}/${name}`)?.kind === "file" ? "file" : "directory"
277
+ }));
278
+ }
279
+ /** 删除文件(非递归);不存在抛错。 */
280
+ deleteFile(path) {
281
+ const abs = ensureAbs(path);
282
+ const node = this.nodes.get(abs);
283
+ if (node === void 0) throw new Error(`ENOENT: \u4E0D\u5B58\u5728 ${abs}`);
284
+ if (node.kind === "dir") throw new Error(`EISDIR: ${abs} \u662F\u76EE\u5F55\uFF0C\u8BF7\u7528 rm \u9012\u5F52\u5220\u9664`);
285
+ this.nodes.delete(abs);
286
+ }
287
+ /** 递归删除文件或目录。 */
288
+ rm(path) {
289
+ const abs = ensureAbs(path);
290
+ if (!this.nodes.has(abs)) throw new Error(`ENOENT: \u4E0D\u5B58\u5728 ${abs}`);
291
+ if (this.nodes.get(abs).kind === "file") {
292
+ this.nodes.delete(abs);
293
+ return;
294
+ }
295
+ const prefix = abs === ROOT ? ROOT : `${abs}/`;
296
+ for (const key of this.nodes.keys()) {
297
+ if (key === abs || key.startsWith(prefix)) this.nodes.delete(key);
298
+ }
299
+ }
300
+ /** 移动/重命名(跨目录亦可);目标父目录须存在。 */
301
+ move(from, to) {
302
+ const src = ensureAbs(from);
303
+ const dst = ensureAbs(to);
304
+ const node = this.nodes.get(src);
305
+ if (node === void 0) throw new Error(`ENOENT: \u4E0D\u5B58\u5728 ${src}`);
306
+ this.ensureParent(dst);
307
+ if (this.nodes.has(dst)) throw new Error(`EEXIST: \u76EE\u6807\u5DF2\u5B58\u5728 ${dst}`);
308
+ const renames = [];
309
+ if (node.kind === "dir") {
310
+ const prefix = src === ROOT ? ROOT : `${src}/`;
311
+ for (const key of this.nodes.keys()) {
312
+ if (key === src) renames.push([src, dst]);
313
+ else if (key.startsWith(prefix)) renames.push([key, dst + key.slice(src.length)]);
314
+ }
315
+ } else {
316
+ renames.push([src, dst]);
317
+ }
318
+ for (const [oldKey, newKey] of renames) {
319
+ this.nodes.set(newKey, this.nodes.get(oldKey));
320
+ this.nodes.delete(oldKey);
321
+ }
322
+ }
323
+ /** 复制文件(不复制目录子树;复制目录用 mount。保持一致的最小实现)。 */
324
+ copyFile(from, to) {
325
+ const src = ensureAbs(from);
326
+ const dst = ensureAbs(to);
327
+ const node = this.nodes.get(src);
328
+ if (node === void 0) throw new Error(`ENOENT: \u4E0D\u5B58\u5728 ${src}`);
329
+ if (node.kind !== "file") throw new Error(`EISDIR: ${src} \u662F\u76EE\u5F55\uFF0C\u6682\u4E0D\u652F\u6301\u6574\u76EE\u5F55\u590D\u5236`);
330
+ this.ensureParent(dst);
331
+ this.nodes.set(dst, { kind: "file", contents: node.contents });
332
+ }
333
+ /** 文件系统树(WebContainers `getFileSystemTree` 对等物):便于 IDE 渲染目录树。 */
334
+ getFileSystemTree() {
335
+ const tree = {};
336
+ for (const [key, node] of this.nodes) {
337
+ if (key === ROOT) continue;
338
+ const names = key.replace(/^\/+/, "").split("/");
339
+ let cursor = tree;
340
+ names.forEach((name, index) => {
341
+ if (index === names.length - 1) {
342
+ if (node.kind === "file") cursor[name] = node.contents;
343
+ else cursor[name] = cursor[name] ?? {};
344
+ } else {
345
+ const existing = cursor[name];
346
+ if (existing === null || typeof existing === "string")
347
+ cursor[name] = typeof existing === "object" && existing !== null ? existing : {};
348
+ cursor = cursor[name];
349
+ }
350
+ });
351
+ }
352
+ return tree;
353
+ }
354
+ /** 列出兴趣路径的全部路径(用于模块解析 / glob 预载)。 */
355
+ keys() {
356
+ return [...this.nodes.keys()].filter((key) => key !== ROOT);
357
+ }
358
+ ensureParent(abs) {
359
+ const parent = dirname(abs);
360
+ if (isRoot(parent)) return;
361
+ if (!this.nodes.has(parent)) this.mkdirEnsureRecursive(parent);
362
+ if (this.nodes.get(parent)?.kind !== "dir")
363
+ throw new Error(`ENOTDIR: \u7236\u7EA7 ${parent} \u662F\u6587\u4EF6\uFF0C\u65E0\u6CD5\u4F5C\u4E3A\u76EE\u5F55`);
364
+ }
365
+ mkdirEnsureRecursive(abs) {
366
+ const parent = dirname(abs);
367
+ if (!isRoot(parent)) this.mkdirEnsureRecursive(parent);
368
+ this.nodes.set(abs, { kind: "dir" });
369
+ }
370
+ };
371
+
372
+ // packages/web-container/src/shell.ts
373
+ init_path();
374
+ function tokenize(line) {
375
+ const tokens = [];
376
+ let current = "";
377
+ let inSingle = false;
378
+ let inDouble = false;
379
+ for (let i = 0; i < line.length; i++) {
380
+ const ch = line[i];
381
+ if (ch === "'" && !inDouble) inSingle = !inSingle;
382
+ else if (ch === '"' && !inSingle) inDouble = !inDouble;
383
+ else if ((ch === " " || ch === " ") && !inSingle && !inDouble) {
384
+ if (current !== "") {
385
+ tokens.push(current);
386
+ current = "";
387
+ }
388
+ } else current += ch;
389
+ }
390
+ if (current !== "") tokens.push(current);
391
+ return tokens;
392
+ }
393
+ var newSegment = () => ({
394
+ argv: [],
395
+ stdoutRedirect: null,
396
+ stdoutAppend: false,
397
+ stderrRedirect: null,
398
+ link: "end"
399
+ });
400
+ function splitSegments(tokens) {
401
+ const segments = [];
402
+ let current = newSegment();
403
+ const flush = () => {
404
+ segments.push(current);
405
+ current = newSegment();
406
+ };
407
+ let i = 0;
408
+ while (i < tokens.length) {
409
+ const token = tokens[i];
410
+ if (token === "|") {
411
+ current.link = "pipe";
412
+ flush();
413
+ i++;
414
+ continue;
415
+ }
416
+ if (token === "&&") {
417
+ current.link = "and";
418
+ flush();
419
+ i++;
420
+ continue;
421
+ }
422
+ if (token === ";") {
423
+ current.link = "seq";
424
+ flush();
425
+ i++;
426
+ continue;
427
+ }
428
+ if (token === ">") {
429
+ current.stdoutRedirect = tokens[i + 1] ?? null;
430
+ current.stdoutAppend = false;
431
+ i += 2;
432
+ continue;
433
+ }
434
+ if (token === ">>") {
435
+ current.stdoutRedirect = tokens[i + 1] ?? null;
436
+ current.stdoutAppend = true;
437
+ i += 2;
438
+ continue;
439
+ }
440
+ if (token === "2>") {
441
+ current.stderrRedirect = tokens[i + 1] ?? null;
442
+ i += 2;
443
+ continue;
444
+ }
445
+ current.argv.push(token);
446
+ i++;
447
+ }
448
+ flush();
449
+ return segments;
450
+ }
451
+ function applyRedirect(ctx, output, target, append) {
452
+ if (target === null) return output;
453
+ const existing = ctx.vfs.exists(target) ? ctx.vfs.readFile(target) : "";
454
+ ctx.vfs.writeFile(target, append ? existing + output : output);
455
+ return "";
456
+ }
457
+ var Shell = class {
458
+ constructor(ctx) {
459
+ this.ctx = ctx;
460
+ }
461
+ /** 执行一行命令;返回 stdout/stderr 与退出码。 */
462
+ async execute(line) {
463
+ const segments = splitSegments(tokenize(line));
464
+ let pipelineInput = "";
465
+ let stdout = "";
466
+ let stderr = "";
467
+ for (let idx = 0; idx < segments.length; idx++) {
468
+ const seg = segments[idx];
469
+ const result = await this.runSegment(seg, pipelineInput);
470
+ stdout += result.stdout;
471
+ stderr += result.stderr;
472
+ const isLast = idx === segments.length - 1;
473
+ if (isLast) return { code: result.code, stdout, stderr };
474
+ if (seg.link === "pipe") pipelineInput = result.stdout;
475
+ else pipelineInput = "";
476
+ if (result.code !== 0 && seg.link !== "seq") return { code: result.code, stdout, stderr };
477
+ }
478
+ return { code: 0, stdout, stderr };
479
+ }
480
+ async runSegment(seg, stdin) {
481
+ const argv = seg.argv;
482
+ if (argv.length === 0) return { code: 0, stdout: "", stderr: "" };
483
+ const command = argv[0];
484
+ let stdout = "";
485
+ let stderr = "";
486
+ switch (command) {
487
+ case "pwd":
488
+ stdout = `${this.ctx.getCwd()}
489
+ `;
490
+ break;
491
+ case "ls":
492
+ stdout = `${this.ls(argv.slice(1)).join("\n")}${this.ls(argv.slice(1)).length ? "\n" : ""}`;
493
+ break;
494
+ case "echo":
495
+ stdout = `${argv.slice(1).join(" ")}
496
+ `;
497
+ break;
498
+ case "cat": {
499
+ const files = argv.slice(1);
500
+ if (files.length === 0) stdout = stdin;
501
+ else for (const file of files) stdout += `${this.ctx.vfs.readFile(this.resolve(file))}
502
+ `;
503
+ break;
504
+ }
505
+ case "mkdir":
506
+ for (const dir of argv.slice(1)) this.ctx.vfs.mkdir(this.resolve(dir));
507
+ break;
508
+ case "touch":
509
+ for (const file of argv.slice(1)) {
510
+ const target = this.resolve(file);
511
+ if (!this.ctx.vfs.exists(target)) this.ctx.vfs.writeFile(target, "");
512
+ }
513
+ break;
514
+ case "rm": {
515
+ for (const file of argv.slice(1)) {
516
+ if (file === "-r" || file === "-rf") continue;
517
+ this.ctx.vfs.rm(this.resolve(file));
518
+ }
519
+ break;
520
+ }
521
+ case "rmdir":
522
+ for (const file of argv.slice(1)) this.ctx.vfs.deleteFile(this.resolve(file));
523
+ break;
524
+ case "cp": {
525
+ const [from, to] = argv.slice(1);
526
+ this.ctx.vfs.copyFile(this.resolve(from ?? ""), this.resolve(to ?? ""));
527
+ break;
528
+ }
529
+ case "mv": {
530
+ const [from, to] = argv.slice(1);
531
+ this.ctx.vfs.move(this.resolve(from ?? ""), this.resolve(to ?? ""));
532
+ break;
533
+ }
534
+ case "cd": {
535
+ const target = argv[1] ?? "/";
536
+ if (!this.ctx.vfs.isDirectory(this.resolve(target)))
537
+ return { code: 1, stdout: "", stderr: `cd: \u4E0D\u662F\u76EE\u5F55: ${target}` };
538
+ this.ctx.setCwd(this.resolve(target));
539
+ break;
540
+ }
541
+ case "head": {
542
+ const lines = stdin !== "" ? stdin.split("\n") : this.ctx.vfs.readFile(this.resolve(argv[1] ?? "")).split("\n");
543
+ stdout = `${lines.slice(0, argv.includes("-n") ? Number(argv[argv.indexOf("-n") + 1]) : 10).join("\n")}${"\n"}`;
544
+ break;
545
+ }
546
+ case "tail": {
547
+ const lines = stdin !== "" ? stdin.split("\n") : this.ctx.vfs.readFile(this.resolve(argv[1] ?? "")).split("\n");
548
+ const n = argv.includes("-n") ? Number(argv[argv.indexOf("-n") + 1]) : 10;
549
+ stdout = `${lines.slice(-n).join("\n")}${"\n"}`;
550
+ break;
551
+ }
552
+ case "help":
553
+ stdout = "\u5185\u5EFA\u547D\u4EE4\uFF1Apwd ls cat echo mkdir touch rm rmdir cp mv cd head tail node js\uFF08\u652F\u6301 > >> 2> \u4E0E |\uFF09\n";
554
+ break;
555
+ case "node":
556
+ case "js": {
557
+ if (this.ctx.runtime === void 0)
558
+ return {
559
+ code: 1,
560
+ stdout: "",
561
+ stderr: "node: \u672A\u6CE8\u5165 JS \u8FD0\u884C\u65F6\uFF08\u6D4F\u89C8\u5668\u7AEF\u9700\u63D0\u4F9B QuickJS WASM \u9002\u914D\uFF09"
562
+ };
563
+ try {
564
+ stdout = `${await this.ctx.runtime.run(this.resolve(argv[1] ?? "index.js"), argv.slice(2), this.ctx.getCwd())}${"\n"}`;
565
+ } catch (error) {
566
+ stderr = `${error instanceof Error ? error.message : String(error)}
567
+ `;
568
+ break;
569
+ }
570
+ break;
571
+ }
572
+ default:
573
+ stderr = `${command}: command not found
574
+ `;
575
+ return { code: 127, stdout: "", stderr };
576
+ }
577
+ stdout = applyRedirect(this.ctx, stdout, seg.stdoutRedirect, seg.stdoutAppend);
578
+ if (seg.stderrRedirect !== null) {
579
+ this.ctx.vfs.writeFile(this.resolve(seg.stderrRedirect), stderr);
580
+ stderr = "";
581
+ }
582
+ return { code: 0, stdout, stderr };
583
+ }
584
+ ls(args) {
585
+ const dirs = args.length ? args.filter((a) => !a.startsWith("-")) : ["."];
586
+ const out = [];
587
+ for (const dir of dirs) {
588
+ const entries = this.ctx.vfs.listDirectory(this.resolve(dir));
589
+ for (const entry of entries)
590
+ out.push(`${entry.type === "directory" ? "d " : "- "}${entry.path}`);
591
+ }
592
+ return out;
593
+ }
594
+ /** 相对 cwd 解析为绝对路径(VFS key)。 */
595
+ resolve(target) {
596
+ if (target.startsWith("/")) return target.replace(/\/+/g, "/");
597
+ return join(this.ctx.getCwd(), target);
598
+ }
599
+ };
600
+
601
+ // packages/web-container/src/npm-client.ts
602
+ var defaultFetch = (url, init) => globalThis.fetch(url, init).then((res) => res);
603
+ function parseSpec(spec) {
604
+ if (spec.startsWith("@")) {
605
+ const at2 = spec.indexOf("@", 1);
606
+ if (at2 === -1) return { name: spec };
607
+ return { name: spec.slice(0, at2), spec: spec.slice(at2 + 1) };
608
+ }
609
+ const at = spec.indexOf("@");
610
+ if (at === -1) return { name: spec };
611
+ return { name: spec.slice(0, at), spec: spec.slice(at + 1) };
612
+ }
613
+ var readBinary = async (res) => {
614
+ const buf = await res.arrayBuffer();
615
+ return new Uint8Array(buf);
616
+ };
617
+ function createNpmClient(options) {
618
+ const fetchImpl = options.fetchImpl ?? defaultFetch;
619
+ const base = options.baseUrl.replace(/\/+$/, "");
620
+ const storage = options.storage;
621
+ return {
622
+ async registries() {
623
+ const res = await fetchImpl(`${base}/registry`);
624
+ if (!res.ok) throw new Error(`\u83B7\u53D6 registry \u5217\u8868\u5931\u8D25\uFF1AHTTP ${res.status}`);
625
+ const body = await res.json();
626
+ return body.registries;
627
+ },
628
+ async install(specArg) {
629
+ const { name, spec } = parseSpec(specArg);
630
+ const params = new URLSearchParams({ name });
631
+ if (spec !== void 0) params.set("spec", spec);
632
+ const resolveRes = await fetchImpl(`${base}/package?${params.toString()}`);
633
+ if (!resolveRes.ok)
634
+ throw new Error(`\u89E3\u6790\u5305\u5931\u8D25 ${name}@${spec ?? "latest"}\uFF1AHTTP ${resolveRes.status}`);
635
+ const meta = await resolveRes.json();
636
+ const tarballPath = `.adep-cache/${meta.name.replaceAll("/", "__")}-${meta.version}.tgz`;
637
+ if (storage.exists(tarballPath)) {
638
+ return { ...meta, tarballPath, downloaded: false };
639
+ }
640
+ const tarballRes = await fetchImpl(
641
+ `${base}/tarball?${new URLSearchParams({ name: meta.name, version: meta.version }).toString()}`
642
+ );
643
+ if (!tarballRes.ok)
644
+ throw new Error(`\u4E0B\u8F7D tarball \u5931\u8D25 ${meta.name}@${meta.version}\uFF1AHTTP ${tarballRes.status}`);
645
+ const bytes = await readBinary(tarballRes);
646
+ storage.writeFile(tarballPath, new TextDecoder("utf-8", { fatal: false }).decode(bytes));
647
+ return { ...meta, tarballPath, downloaded: true };
648
+ }
649
+ };
650
+ }
651
+
652
+ // packages/web-container/src/worker-runtime.ts
653
+ async function evaluateSource(source, filename, args) {
654
+ const output = [];
655
+ const fakeConsole = {
656
+ log: (...parts) => output.push(parts.map((p) => String(p)).join(" ")),
657
+ error: (...parts) => output.push(parts.map((p) => String(p)).join(" ")),
658
+ warn: (...parts) => output.push(parts.map((p) => String(p)).join(" "))
659
+ };
660
+ const sandbox = {
661
+ console: fakeConsole,
662
+ Math,
663
+ JSON,
664
+ Date,
665
+ __args: [...args],
666
+ __filename: filename
667
+ };
668
+ const fn = new Function(
669
+ "sandbox",
670
+ "with (sandbox) { return (async function(){ " + source + "\n })(); }"
671
+ );
672
+ const result = await fn(sandbox);
673
+ if (result !== void 0) output.push(String(result));
674
+ return output.join("\n");
675
+ }
676
+ var WORKER_SCRIPT = (
677
+ /* javascript */
678
+ `
679
+ const evaluate = async (source, filename, args) => {
680
+ const output = []
681
+ const fakeConsole = {
682
+ log: (...parts) => output.push(parts.map((p) => String(p)).join(' ')),
683
+ error: (...parts) => output.push(parts.map((p) => String(p)).join(' ')),
684
+ warn: (...parts) => output.push(parts.map((p) => String(p)).join(' ')),
685
+ }
686
+ const sandbox = { console: fakeConsole, Math, JSON, Date, __args: [...args], __filename: filename }
687
+ const fn = new Function('sandbox', 'with (sandbox) { return (async function(){ ' + source + '\\n })(); }')
688
+ const result = await fn(sandbox)
689
+ if (result !== undefined) output.push(String(result))
690
+ return output.join('\\n')
691
+ }
692
+ self.onmessage = async (event) => {
693
+ const { id, source, filename, args } = event.data
694
+ try {
695
+ const output = await evaluate(source, filename, args)
696
+ self.postMessage({ id, output })
697
+ } catch (error) {
698
+ self.postMessage({ id, error: error && error.message ? error.message : String(error) })
699
+ }
700
+ }
701
+ `
702
+ );
703
+ function createWorkerRuntime(vfs, options = {}) {
704
+ const workerFactory = options.workerFactory ?? ((url) => new Worker(url));
705
+ const scriptUrl = options.workerFactory === void 0 ? URL.createObjectURL(new Blob([WORKER_SCRIPT], { type: "application/javascript" })) : "";
706
+ const worker = workerFactory(scriptUrl);
707
+ const pending = /* @__PURE__ */ new Map();
708
+ let seq = 0;
709
+ worker.onmessage = (event) => {
710
+ const msg = event.data;
711
+ const task = pending.get(msg.id);
712
+ if (task === void 0) return;
713
+ pending.delete(msg.id);
714
+ if (msg.error !== void 0) task.reject(new Error(msg.error));
715
+ else task.resolve(msg.output ?? "");
716
+ };
717
+ worker.onerror = (event) => {
718
+ const reason = event.message ?? "worker error";
719
+ for (const [, task] of pending) task.reject(new Error(reason));
720
+ pending.clear();
721
+ };
722
+ return {
723
+ async run(file, args, _cwd) {
724
+ const source = vfs.readFile(file);
725
+ const id = seq++;
726
+ return new Promise((resolve, reject) => {
727
+ pending.set(id, { resolve, reject });
728
+ worker.postMessage({ id, source, filename: file, args: [...args] });
729
+ });
730
+ },
731
+ close() {
732
+ pending.clear();
733
+ worker.terminate();
734
+ if (options.cleanup !== false && scriptUrl !== "") URL.revokeObjectURL(scriptUrl);
735
+ }
736
+ };
737
+ }
738
+
739
+ // packages/web-container/src/container.ts
740
+ function bootstrap(options) {
741
+ const vfs = new VirtualFileSystem();
742
+ if (options.initialTree !== void 0) vfs.mount(options.initialTree);
743
+ let cwd = "/";
744
+ const setCwd = (path) => {
745
+ cwd = path;
746
+ };
747
+ const defaultRuntime = typeof Worker !== "undefined" ? createWorkerRuntime(vfs) : void 0;
748
+ const runtime = options.runtime ?? defaultRuntime;
749
+ const shell = new Shell({
750
+ vfs,
751
+ getCwd: () => cwd,
752
+ setCwd,
753
+ ...runtime === void 0 ? {} : { runtime }
754
+ });
755
+ const npmClient = createNpmClient({
756
+ baseUrl: options.npmRelayBaseUrl,
757
+ storage: vfs,
758
+ ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl }
759
+ });
760
+ const listeners = /* @__PURE__ */ new Map();
761
+ const emit = (event, payload) => {
762
+ const set = listeners.get(event);
763
+ if (set === void 0) return;
764
+ for (const fn of set) fn(payload);
765
+ };
766
+ let preview = null;
767
+ const buildProcess = (cmdLine) => {
768
+ let outputController;
769
+ const output = new ReadableStream({
770
+ start(c) {
771
+ outputController = c;
772
+ }
773
+ });
774
+ const input = new WritableStream({ write() {
775
+ } });
776
+ let resolveExit;
777
+ const exit = new Promise((resolve) => {
778
+ resolveExit = resolve;
779
+ });
780
+ const proc = { exit, input, output, stderr: output };
781
+ const encoder = new TextEncoder();
782
+ const writeAll = (text) => {
783
+ outputController.enqueue(encoder.encode(text));
784
+ };
785
+ emit("process", { pty: proc, event: "start" });
786
+ (async () => {
787
+ try {
788
+ const result = await shell.execute(cmdLine);
789
+ if (result.stdout !== "") writeAll(result.stdout);
790
+ if (result.stderr !== "") writeAll(result.stderr);
791
+ outputController.close();
792
+ emit("process", { pty: proc, event: "exit", code: result.code });
793
+ resolveExit(result.code);
794
+ } catch (error) {
795
+ const message = error instanceof Error ? error.message : String(error);
796
+ writeAll(message);
797
+ outputController.close();
798
+ emit("process", { pty: proc, event: "error", error });
799
+ resolveExit(1);
800
+ }
801
+ })();
802
+ return proc;
803
+ };
804
+ return {
805
+ async mount(tree) {
806
+ vfs.mount(tree);
807
+ preview?.touch("/");
808
+ },
809
+ async writeFile(path, contents) {
810
+ vfs.writeFile(path, contents);
811
+ preview?.touch(path);
812
+ },
813
+ async readFile(path) {
814
+ return vfs.readFile(path);
815
+ },
816
+ async listDirectory(path) {
817
+ return vfs.listDirectory(path);
818
+ },
819
+ async createDirectory(path, recursive = true) {
820
+ vfs.mkdir(path, recursive);
821
+ },
822
+ async deleteFile(path) {
823
+ vfs.rm(path);
824
+ preview?.touch(path);
825
+ },
826
+ async exists(path) {
827
+ return vfs.exists(path);
828
+ },
829
+ spawn(command, args) {
830
+ const cmdLine = [command, ...args ?? []].join(" ");
831
+ return buildProcess(cmdLine);
832
+ },
833
+ async run(command, args, opts) {
834
+ const proc = this.spawn(command, args, opts);
835
+ await proc.exit;
836
+ const reader = proc.output.getReader();
837
+ const chunks = [];
838
+ for (; ; ) {
839
+ const { done, value } = await reader.read();
840
+ if (done) break;
841
+ chunks.push(new TextDecoder().decode(value));
842
+ }
843
+ return chunks.join("");
844
+ },
845
+ on(event, callback) {
846
+ const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
847
+ set.add(callback);
848
+ listeners.set(event, set);
849
+ },
850
+ off(event, callback) {
851
+ const set = listeners.get(event);
852
+ set?.delete(callback);
853
+ },
854
+ async install(spec) {
855
+ emit("installprogress", { kind: "start", spec });
856
+ try {
857
+ await npmClient.install(spec);
858
+ emit("installprogress", { kind: "complete", spec });
859
+ } catch (error) {
860
+ emit("installprogress", {
861
+ kind: "fail",
862
+ spec,
863
+ message: error instanceof Error ? error.message : String(error)
864
+ });
865
+ throw error;
866
+ }
867
+ },
868
+ async registries() {
869
+ return npmClient.registries();
870
+ },
871
+ async preview(previewOptions) {
872
+ if (preview === null) {
873
+ const { createPreviewServer: createPreviewServer2 } = await Promise.resolve().then(() => (init_preview(), preview_exports));
874
+ preview = createPreviewServer2(vfs, previewOptions);
875
+ emit("serverready", { port: preview.port, url: preview.url });
876
+ }
877
+ return { url: preview.url, port: preview.port };
878
+ },
879
+ async close() {
880
+ listeners.clear();
881
+ defaultRuntime?.close?.();
882
+ await preview?.stop();
883
+ preview = null;
884
+ }
885
+ };
886
+ }
887
+
888
+ // packages/web-container/src/index.ts
889
+ init_path();
890
+ export {
891
+ Shell,
892
+ VirtualFileSystem,
893
+ bootstrap,
894
+ createNpmClient,
895
+ createWorkerRuntime,
896
+ evaluateSource,
897
+ parseSpec,
898
+ path_exports as posix
899
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * npm 客户端(FN-012 对外 SDK):经平台 `/npm-relay/*` 中继取真实 registry 依赖。
3
+ *
4
+ * 浏览器虚拟系统无法直连 npm registry(跨域)→ 全部请求走平台中继;
5
+ * tarball 下载后写入虚拟文件系统 `.adep-cache/<name>-<version>.tgz`(本地离线复用,
6
+ * 与中继的「元数据 + tarball 双层缓存」呼应;完整解包到 node_modules 属后续增强)。
7
+ */
8
+ import type { CloudFetch } from '@adep/types';
9
+ export interface NpmPersistence {
10
+ exists(path: string): boolean;
11
+ writeFile(path: string, contents: string): void;
12
+ }
13
+ export interface NpmClientOptions {
14
+ /** 平台 npm 中继基础路径(如 `/api/v1/npm-relay`;相对绝对均可)。 */
15
+ baseUrl: string;
16
+ /** 本地 tarball 落盘(写虚拟文件系统)。 */
17
+ storage: NpmPersistence;
18
+ /** 网络实现(缺省 globalThis.fetch)。 */
19
+ fetchImpl?: CloudFetch;
20
+ }
21
+ export interface NpmInstallResult {
22
+ name: string;
23
+ version: string;
24
+ distTags: string[];
25
+ /** 本地缓存 tarball 的 VFS 路径。 */
26
+ tarballPath: string;
27
+ /** 本次是否发生了真实下载(false = 本地缓存命中)。 */
28
+ downloaded: boolean;
29
+ }
30
+ /** 解析 `name` 或 `name@spec` 到 { name, spec? }(含 scoped 包)。 */
31
+ export declare function parseSpec(spec: string): {
32
+ name: string;
33
+ spec?: string;
34
+ };
35
+ export declare function createNpmClient(options: NpmClientOptions): {
36
+ registries(): Promise<string[]>;
37
+ install(spec: string): Promise<NpmInstallResult>;
38
+ };
package/dist/path.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * POSIX 路径工具(虚拟文件系统的路径语义)。
3
+ *
4
+ * 与 `node:path` 的 posix 行为保持一致,但**零运行时依赖**、浏览器可跑
5
+ * (virtual fs 由浏览器原生开发环境与 Node 单测共用,禁止 import node 内建模块)。
6
+ */
7
+ export declare const SEP = "/";
8
+ /** 规范化路径:折叠 `.`、`..`,拒绝绝对路径(VF 根固定 `/`,入参一律相对根)。 */
9
+ export declare function normalize(path: string): string;
10
+ /** 判断是否绝对路径(以 `/` 开头)。虚拟系统根为 `/`。 */
11
+ export declare function isAbsolute(path: string): boolean;
12
+ /** 把相对路径接在给定基础路径后(基础不必已规范化)。 */
13
+ export declare function join(base: string, ...paths: string[]): string;
14
+ /** 返回父目录(根目录的父是根)。 */
15
+ export declare function dirname(path: string): string;
16
+ /** 返回末级文件名。 */
17
+ export declare function basename(path: string): string;
18
+ /** 返回扩展名(含点;无扩展名返回空串)。 */
19
+ export declare function extname(path: string): string;
20
+ /** 把 `/` 分隔的字符串当作单一段(用于校验段名,禁止空段与 `..` 穿越)。 */
21
+ export declare function isValidSegment(name: string): boolean;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 浏览器原生开发环境的「dev server 内联预览 + HMR」(FN-012 Scenario 1 / 验收 5)。
3
+ *
4
+ * 真实浏览器里没有 Node / Vite 运行时,预习用「虚拟静态 dev server」实现(全程浏览器内、
5
+ * 零依赖、无需本机运行时):
6
+ * - serve 时把入口 HTML 里的**本地**资源(`<script src>` / `<link rel=stylesheet>`)内联进文档,
7
+ * 生成一份自包含的 blob URL 文档——等价于 dev build 的静态输出,可被 `<iframe>` 直接预览;
8
+ * - 内联后注入一段 HMR 接收脚本:订阅稳定名的 `BroadcastChannel`,收到 `{type:'update', url}` 时
9
+ * `location.replace(url)` 自刷新。SDK 侧在文件变更时重取文档、比对 hash,内容变才广播新 URL;
10
+ * - 对外经 `serverready` 事件暴露 `{ port, url }`(`port` 为虚拟端口号)。
11
+ *
12
+ * 边界(与 task 超出范围相称):不跑真实的 Vite/TS 编译管道,只贴原位静态资源;`type=module`
13
+ * 脚本与外部 URL 原样保留不内联。需要全量编译时替换为真正的 WASM/Vite pipeline(接口可替换)。
14
+ */
15
+ import type { VirtualFileSystem } from './vfs';
16
+ import type { PreviewServerOptions as PreviewServerOptionsContract } from '@adep/types';
17
+ export interface PreviewServerOptions extends PreviewServerOptionsContract {
18
+ /** blob URL 工厂(Node 无 `URL.createObjectURL`,单测注入假实现)。 */
19
+ createObjectURL?: (blob: Blob) => string;
20
+ revokeObjectURL?: (url: string) => void;
21
+ }
22
+ export interface PreviewDocumentOptions {
23
+ cwd: string;
24
+ entry: string;
25
+ /** HMR 通道名(同一 preview 实例内稳定,文档与主线程共享)。 */
26
+ channelName: string;
27
+ }
28
+ /** 服务结果:`url` 为当前文档 blob URL,`changed` 标记与上次是否不同。 */
29
+ export interface PreviewServeResult {
30
+ url: string;
31
+ changed: boolean;
32
+ }
33
+ /** dev server 句柄:向容器暴露 `url` / `port`,变更经 `touch` 广播 HMR。 */
34
+ export interface PreviewServer {
35
+ readonly url: string;
36
+ readonly port: number;
37
+ /** 某路径发生变更:重取文档,内容变则广播新 URL 驱动预览自刷新(HMR)。 */
38
+ touch(path: string): void;
39
+ /** 关闭:撤销 URL、关闭 HMR 通道。 */
40
+ stop(): Promise<void>;
41
+ }
42
+ /**
43
+ * 把入口 HTML 里的本地 `<script src>` / `<link rel=stylesheet>` 内联为内嵌内容(纯函数,Node 可测)。
44
+ * `type="module"` 脚本与外部 URL 一律保持原样,避免破坏 import 语义。
45
+ */
46
+ export declare function inlineAssets(html: string, vfs: VirtualFileSystem, baseDir: string): string;
47
+ /** 组装最终预览文档:读入口 → 内联本地资源 → 注入 HMR 接收脚本。纯函数,Node 可测。 */
48
+ export declare function buildPreviewDocument(vfs: VirtualFileSystem, options: PreviewDocumentOptions): string;
49
+ /** 创建虚拟静态 dev server:`serve`+`touch` 驱动 blob 文档与 HMR 广播。 */
50
+ export declare function createPreviewServer(vfs: VirtualFileSystem, options?: PreviewServerOptions): PreviewServer;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shell(FN-012 对外 SDK):UNIX-like 命令解析与执行。
3
+ *
4
+ * 解析器支持:双引号/单引号、空格分隔、`>`/`>>` 输出重定向、`|` 管道(内置命令之间)。
5
+ * 每个内置命令对 `VirtualFileSystem` 操作;`node <file>` / `js <file>` 走注入的 JS 运行时适配。
6
+ * 输出以 `stdout` / `stderr` 分通道收集,供上层包装成 WebContainerProcess 双工流。
7
+ *
8
+ * 浏览器可跑(零 node 依赖),`runtime` 为可替换适配(QuickJS WASM / Node vm)。
9
+ */
10
+ import type { VirtualFileSystem } from './vfs';
11
+ /** JS 运行时适配接口:能加载并执行一段 JS 模块(QuickJS WASM / Web Worker 沙箱 / Node vm 等实现)。 */
12
+ export interface JsRuntime {
13
+ /** 执行脚本文件,返回标准输出文本;实现抛错时以 `stderr` 语义上报。 */
14
+ run(file: string, args: readonly string[], cwd: string): Promise<string>;
15
+ /** 释放运行时常驻的线程/句柄(如浏览器 Worker)。未定义表示无需清理(如 vm 单次求值)。 */
16
+ close?(): void;
17
+ }
18
+ export interface ShellResult {
19
+ code: number;
20
+ stdout: string;
21
+ stderr: string;
22
+ }
23
+ export interface ShellCtx {
24
+ vfs: VirtualFileSystem;
25
+ getCwd(): string;
26
+ setCwd(path: string): void;
27
+ runtime?: JsRuntime;
28
+ }
29
+ export declare class Shell {
30
+ private readonly ctx;
31
+ constructor(ctx: ShellCtx);
32
+ /** 执行一行命令;返回 stdout/stderr 与退出码。 */
33
+ execute(line: string): Promise<ShellResult>;
34
+ private runSegment;
35
+ private ls;
36
+ /** 相对 cwd 解析为绝对路径(VFS key)。 */
37
+ private resolve;
38
+ }
39
+ /** 构造一个在给定 VFS 上、cwd 为根 `/` 的 shell。 */
40
+ export declare function createShell(vfs: VirtualFileSystem, runtime?: JsRuntime): {
41
+ shell: Shell;
42
+ getCwd(): string;
43
+ setCwd(path: string): void;
44
+ };
package/dist/vfs.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 虚拟文件系统(FN-012 浏览器原生开发环境 / 对外 SDK 核心)。
3
+ *
4
+ * 对标 WebContainers `mount` / `writeFile` / `readFile` / `listDirectory` / `deleteFile`。
5
+ * 实现为**内存中的 dict**,key 一律是**以 `/` 开头的规范化绝对路径**(`/src/index.ts`),
6
+ * 目录与文件共享同一 key 空间(`dir` vs `file` 二元自描述),枚举靠前缀扫描。
7
+ *
8
+ * 浏览器可跑(零 node 依赖);`mount(tree)` 递归建目录、`writeFile` 自动补父目录、
9
+ * `move`/`rm` 支持递归。持久化由上层(IndexedDB / 平台 ST-001)负责,本类只管内存态。
10
+ */
11
+ import { basename } from './path';
12
+ import type { WebContainerDirectoryTree, WebContainerFsEntry } from '@adep/types';
13
+ /** 根目录的规范化形态。 */
14
+ export declare const ROOT = "/";
15
+ export declare class VirtualFileSystem {
16
+ private readonly nodes;
17
+ /** 挂载一棵目录树(WebContainers `mount` 入参形态)。 */
18
+ mount(tree: WebContainerDirectoryTree): void;
19
+ private mountWithin;
20
+ /** 写文件(自动补父目录;`../` 与绝对越界段交由 normalize 折叠)。 */
21
+ writeFile(path: string, contents: string): void;
22
+ /** 读文件文本;路径是目录或不存在时抛错。 */
23
+ readFile(path: string): string;
24
+ /** 是否存在(文件或目录)。 */
25
+ exists(path: string): boolean;
26
+ /** 是否为目录。 */
27
+ isDirectory(path: string): boolean;
28
+ /** 创建目录(缺省 recursive,自动补父)。 */
29
+ mkdir(path: string, recursive?: boolean): void;
30
+ /** 列目录条目;目录不存在抛错。条目 path 为相对宿主根(POSIX)。 */
31
+ listDirectory(path: string): WebContainerFsEntry[];
32
+ /** 删除文件(非递归);不存在抛错。 */
33
+ deleteFile(path: string): void;
34
+ /** 递归删除文件或目录。 */
35
+ rm(path: string): void;
36
+ /** 移动/重命名(跨目录亦可);目标父目录须存在。 */
37
+ move(from: string, to: string): void;
38
+ /** 复制文件(不复制目录子树;复制目录用 mount。保持一致的最小实现)。 */
39
+ copyFile(from: string, to: string): void;
40
+ /** 文件系统树(WebContainers `getFileSystemTree` 对等物):便于 IDE 渲染目录树。 */
41
+ getFileSystemTree(): WebContainerDirectoryTree;
42
+ /** 列出兴趣路径的全部路径(用于模块解析 / glob 预载)。 */
43
+ keys(): string[];
44
+ private ensureParent;
45
+ private mkdirEnsureRecursive;
46
+ }
47
+ export type { WebContainerDirectoryTree };
48
+ export { basename };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 浏览器端 JS 运行时适配(FN-012 验收 2):Web Worker 沙箱执行,零 WASM 依赖、可替换。
3
+ *
4
+ * `vm.ts` 是 Node-only(import `node:vm`),浏览器端没有 node:vm。本模块用浏览器自己的
5
+ * V8 引擎,在一个 `blob:` URL 内联 Worker 里执行 JS——Worker 与主线程隔离(无 DOM、无
6
+ * 主线程全局)、独立 global,作为对外 SDK `JsRuntime` 的浏览器实现。执行逻辑与 OW
7
+ * 上游同款契约:读取 VFS 文件 → 隔离上下文求值 → 捕获 console 输出 → 返回 stdout 文本,
8
+ * 可整体替换为 QuickJS WASM 实现(`JsRuntime` 只暴露一个 `run` 方法)。
9
+ *
10
+ * 包形态与 vm.ts 对称:vm.ts 仅在 Node 加载(不进浏览器包),本模块可在浏览器 / Node 双跑
11
+ * (Node 下 `Worker` 全局由 options.workerFactory 注入,供单测驱动消息协议)。
12
+ */
13
+ import type { VirtualFileSystem } from './vfs';
14
+ import type { JsRuntime } from './shell';
15
+ /** 主线程 → Worker 的求值请求。 */
16
+ interface WorkerRequest {
17
+ id: number;
18
+ source: string;
19
+ filename: string;
20
+ args: readonly string[];
21
+ }
22
+ /** Worker → 主线程的求值结果。 */
23
+ export interface WorkerResponse {
24
+ id: number;
25
+ output?: string;
26
+ error?: string;
27
+ }
28
+ /** 隔离上下文求值:等价于 vm.ts 的 IFFE 包装,但跑在浏览器任意全局(Node 测试 / Worker 共用)。 */
29
+ export declare function evaluateSource(source: string, filename: string, args: readonly string[]): Promise<string>;
30
+ /** 极简 Worker 形态(鸭子类型:Node 测试注入假 Worker 走同一协议)。 */
31
+ export interface WorkerLike {
32
+ postMessage(message: WorkerRequest): void;
33
+ onmessage: ((event: MessageEvent<WorkerResponse>) => void) | null;
34
+ onerror: ((event: ErrorEvent) => void) | null;
35
+ terminate(): void;
36
+ }
37
+ export interface WorkerRuntimeOptions {
38
+ /** Worker 构造函数(缺省 `new Worker(blobUrl)`;Node 单测注入假 Worker)。 */
39
+ workerFactory?: (scriptUrl: string) => WorkerLike;
40
+ /** 是否在 close 时 terminate Worker。缺省 true。 */
41
+ cleanup?: boolean;
42
+ }
43
+ /**
44
+ * 在浏览器内执行 JS 的 `JsRuntime` 实现:读 VFS 文件 → 交给 Worker 沙箱求值 → 返回 stdout。
45
+ * `node <file>` / `js <file>` 内建命令经此在**浏览器**内(而非服务端沙箱)跑用户代码。
46
+ */
47
+ export declare function createWorkerRuntime(vfs: VirtualFileSystem, options?: WorkerRuntimeOptions): JsRuntime;
48
+ export {};
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@adep/web-container",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "description": "adep 平台浏览器原生开发环境对外 SDK(FN-012):虚拟文件系统 + 进程/shell + JS 运行时适配接口 + npm-relay 客户端,对标 WebContainers API,Node 可测、浏览器可跑。",
8
+ "main": "./src/index.ts",
9
+ "types": "./src/index.ts",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "test": "vitest run",
15
+ "build": "bun run ../../scripts/build-package.ts web-container",
16
+ "build:sdk": "pnpm exec esbuild src/index.ts --bundle --format=esm --platform=browser --outfile=dist/web-container.esm.js && pnpm exec esbuild src/index.ts --bundle --format=iife --global-name=AdepWebContainer --platform=browser --outfile=dist/web-container.iife.js"
17
+ },
18
+ "dependencies": {
19
+ "@adep/types": "workspace:*"
20
+ },
21
+ "publishConfig": {
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "./*": {
30
+ "types": "./dist/*.d.ts",
31
+ "default": "./dist/*.js"
32
+ }
33
+ }
34
+ }
35
+ }