@adep/web-container 0.1.0 → 0.2.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 CHANGED
@@ -10,30 +10,66 @@ pnpm add @adep/web-container
10
10
 
11
11
  ## 能力总览
12
12
 
13
- | 能力 | 说明 |
14
- | ------------- | ------------------------- |
15
- | 虚拟文件系统 (VFS) | 内存文件树:读写 / 查找 / 变更通知 |
16
- | 进程 / shell | 在虚拟环境中执行命令、管道输入输出 |
17
- | 运行时适配接口 | 用适配器接宿主运行时(预览 / 沙箱执行) |
18
- | npm-relay 客户端 | 经平台 npm-relay 安装 / 解析前端依赖 |
13
+ | 能力 | 说明 |
14
+ | ------------------ | --------------------------------------------------- |
15
+ | 虚拟文件系统 (VFS) | 内存文件树:读写 / 查找 / 变更通知 |
16
+ | 进程 / shell | 在虚拟环境中执行命令、管道输入输出 |
17
+ | 运行时适配接口 | 用适配器接宿主运行时(预览 / 沙箱执行) |
18
+ | npm-relay 客户端 | 经平台 npm-relay 安装 / 解析前端依赖 |
19
+ | 浏览器内跑测试 | `test` 命令 + `describe/it/expect` 最小集(FN-022) |
19
20
 
20
21
  ## 用法
21
22
 
22
23
  ```ts
23
- import { createWebContainer } from '@adep/web-container'
24
+ import { bootstrap } from '@adep/web-container'
24
25
 
25
- const container = await createWebContainer({
26
- // 传入文件系统与运行时适配器
26
+ const container = bootstrap({ npmRelayBaseUrl: '/api/v1/npm-relay' })
27
+
28
+ await container.mount({
29
+ 'index.test.ts': "describe('a', () => { it('一', () => expect(1).toBe(1)) })",
27
30
  })
31
+ await container.writeFile('notes.txt', 'hi')
32
+ console.log(await container.readFile('notes.txt'))
33
+ console.log(await container.run('test')) // → ✓ a › 一 … 汇总:1 passed
34
+ ```
35
+
36
+ ## 在浏览器里跑测试(FN-022)
28
37
 
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')
38
+ 终端里的 `test` 是**浏览器内**跑的:不经过服务端,不需要本机 Node。
39
+
40
+ ```bash
41
+ $ test # 当前目录递归收集
42
+ $ test src/utils # 指定目录
43
+ $ test a.test.ts # 指定单文件
32
44
  ```
33
45
 
46
+ | 口径 | 约定 |
47
+ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
48
+ | 收集 | 文件名以 `.test.ts` / `.spec.ts` 结尾(`.js` / `.mjs` / `.cjs` 同样收);递归、字典序;跳过 `node_modules/`、`.git/`、`.adep/` 与 `.d.ts` |
49
+ | 全局 | `describe` / `it`(别名 `test`)/ `expect` / `beforeEach` / `afterEach` 由沙箱注入,**测试文件不 import 测试框架** |
50
+ | 断言 | `toBe`(`Object.is`)/ `toEqual`(深比较)/ `toBeTruthy` / `toBeFalsy` / `toBeNull` / `toBeUndefined` / `toBeDefined` / `toContain` / `toHaveLength` / `toThrow`,均支持 `.not` |
51
+ | 退出码 | 全部通过 `0`;有断言失败、有加载错误、或用例数为 0 → `1`;路径不存在 → `1`(写 `stderr`) |
52
+ | 日志 | 用例里的 `console.log` 随该文件的报告按行展示(`│` 前缀) |
53
+ | 超时 | 单条用例默认 5s(浏览器主线程不能被一个卡住的 `await` 放任成挂死的终端) |
54
+
55
+ **TypeScript**:测试文件可以照常写类型标注。`strip-types.ts` 会把它擦成可 `new Function` 求值的 JS
56
+ ——**对纯 JS 逐字节恒等**、擦除区间保留换行(行号不漂)。擦除器不引第三方依赖,且与
57
+ `@adep/runtime` 的宿主能力探测(`Bun.Transpiler` / `node:module`)无关:同一份文件在浏览器与
58
+ Node 单测里必须同语义。
59
+
60
+ - 擦除子集:`interface`、`type` 别名、`import type` / `export type`、参数 / 返回值 / 变量 /
61
+ 类成员标注、解构参数标注、可选参数 `?:`、非空断言 `!`、`as` / `satisfies`、声明位泛型、
62
+ `implements`、类成员 TS 修饰符。
63
+ - 明确不支持(抛 `UnsupportedTsSyntaxError`,报为该文件「加载失败」):`enum`、`namespace`。
64
+ - 判不准就**不擦**:漏擦的代价是求值期一个 `SyntaxError`(如实上报),而不是静默改变语义。
65
+
66
+ **边界**:测试与 `node <file>` 共用同一条求值路径(`worker-runtime.evaluateModuleSource`),
67
+ 所以沙箱既有的限制都在这里生效——`export` 语句不被支持(入口式函数文件请直接用 `node` 调试),
68
+ `cloud.*` 在测试里可用与否取决于 FN-023 的进度(本包只保证「无 ctx 的纯函数测试」可跑)。
69
+ 完整 vitest 兼容(快照 / 模块 mock / 覆盖率)不在范围内。
70
+
34
71
  ## 说明
35
72
 
36
73
  - 面向浏览器,同时成 Node 可测;依赖 `@adep/types` 提供类型契约。
37
74
 
38
75
  - 需要浏览器内作为 `<script>` 全局加载的 IIFE 变体见 `scripts.build:sdk`(`dist/web-container.iife.js`,`globalName = AdepWebContainer`)。
39
-
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Shell 的 Tab 补全 API(FE-022)。
3
+ *
4
+ * 此前 `BrowserTerminal.vue` 的 `handleData` 没有 `\t` 分支,可打印过滤 `cp >= 0x20` 又把 Tab 静默吞掉
5
+ * ——按 Tab 什么也不发生。本文件把「补全什么」从「按键怎么处理」里拆出来,做成纯函数:
6
+ * - 命令位(行首且未输入 `/`)→ 补内建命令名;
7
+ * - 参数位 → 按 VFS 目录列表补路径,目录候选带尾部 `/`(与 bash 一致,便于继续下钻);
8
+ * - `cd` 的参数只列目录。
9
+ *
10
+ * 解析与匹配全部不依赖具体 fs:调用方给一份目录条目列表(`WebContainerFsEntry[]`)即可,
11
+ * 因此同步(持有 `VirtualFileSystem`,如 `Shell.complete`)与异步(只有 `WebContainer`,如 IDE 终端)
12
+ * 两个入口共用同一套判据,不会各说各话。npm 不是 shell 内建命令(由 IDE 侧拦成 `container.install`),
13
+ * 故只在补全表里出现、不进 `SHELL_COMMANDS`。
14
+ */
15
+ import type { WebContainerFsEntry } from '@adep/types';
16
+ /** shell 内建命令(`shell.ts` 的 switch 分支,`help` 文案由此派生,避免两处清单漂移)。 */
17
+ export declare const SHELL_COMMANDS: readonly ["pwd", "ls", "cat", "echo", "mkdir", "touch", "rm", "rmdir", "cp", "mv", "cd", "head", "tail", "curl", "wget", "test", "node", "js", "vite", "help"];
18
+ /** 只在补全里出现的平台命令(由上层拦截实现,非 shell 内建)。 */
19
+ export declare const EXTRA_COMMANDS: readonly ["npm"];
20
+ /** 命令位可补的全部命令。 */
21
+ export declare const COMPLETABLE_COMMANDS: readonly string[];
22
+ /** 补全上下文(由 `parseCompletionContext` 解析出,纯数据)。 */
23
+ export interface CompletionContext {
24
+ /** 被补全的 token(用户已输入的原文,含目录前缀)。 */
25
+ token: string;
26
+ /** token 在整行中的起始下标(替换区间起点,终点为行尾)。 */
27
+ start: number;
28
+ kind: 'command' | 'path';
29
+ /** 命令位(行首命令名)与参数位所属的命令名(路径补全据此决定要不要只列目录)。 */
30
+ command: string;
31
+ /** 路径补全时要去列出的目录(已按 cwd 解析为绝对路径);命令位为 null。 */
32
+ dir: string | null;
33
+ /** token 里用于过滤的末级名前缀。 */
34
+ prefix: string;
35
+ /** 候选要回写时保留的目录前缀原文(如 `src/` 或 `/tmp/`)。 */
36
+ dirText: string;
37
+ }
38
+ /** 补全结果。 */
39
+ export interface CompletionResult {
40
+ context: CompletionContext;
41
+ /** 候选(完整替换 token:含原目录前缀,目录带尾部 `/`),已按字典序。 */
42
+ candidates: string[];
43
+ /** 候选的公共前缀(用于「多候选先补到分叉点」);无候选时等于 token。 */
44
+ commonPrefix: string;
45
+ }
46
+ /** 同步的目录列表提供者(`VirtualFileSystem` 即符合此形状)。 */
47
+ export interface CompletionFsView {
48
+ listDirectory(path: string): readonly WebContainerFsEntry[];
49
+ }
50
+ /** 异步的目录列表提供者(`WebContainer` 即符合此形状)。 */
51
+ export interface AsyncCompletionFsView {
52
+ listDirectory(path: string): Promise<readonly WebContainerFsEntry[]>;
53
+ }
54
+ /** 解析一行输入,得出「该补命令还是路径、去哪个目录找」。 */
55
+ export declare function parseCompletionContext(line: string, cwd: string): CompletionContext;
56
+ /** 给定目录条目算出候选(纯函数:同步 / 异步入口都收敛到这里)。 */
57
+ export declare function completeWithListing(context: CompletionContext, listing: readonly WebContainerFsEntry[]): CompletionResult;
58
+ /** 同步入口:直接持有 VFS(或任何 `listDirectory` 同步实现)时用它。 */
59
+ export declare function completeShellInput(options: {
60
+ line: string;
61
+ cwd: string;
62
+ fs: CompletionFsView;
63
+ }): CompletionResult;
64
+ /** 异步入口:只持有 `WebContainer`(公开 API 的 `listDirectory` 是异步的)时用它。 */
65
+ export declare function completeContainerInput(options: {
66
+ line: string;
67
+ cwd: string;
68
+ fs: AsyncCompletionFsView;
69
+ }): Promise<CompletionResult>;
70
+ /**
71
+ * 把候选写回输入行:替换 `[start, 行尾)` 区间。
72
+ * `candidate` 省略时取公共前缀(多候选补到分叉点,与 bash 行为一致)。
73
+ */
74
+ export declare function applyCompletion(line: string, result: CompletionResult, candidate?: string): string;
75
+ /** 候选的展示名(去掉目录前缀与目录尾部斜杠,终端里逐列打印时更紧凑)。 */
76
+ export declare function candidateLabel(candidate: string): string;
77
+ /** 供 `help` 文案派生:`内建命令:pwd ls …`。 */
78
+ export declare function helpText(): string;
@@ -10,7 +10,10 @@
10
10
  */
11
11
  import type { WebContainer, WebContainerDirectoryTree } from '@adep/types';
12
12
  import type { CloudFetch } from '@adep/types';
13
+ import { VirtualFileSystem } from './vfs';
13
14
  import { type JsRuntime } from './shell';
15
+ import type { TestReport } from './test-framework';
16
+ import type { SfcCompiler } from './vite-dev';
14
17
  export interface BootstrapOptions {
15
18
  /**
16
19
  * 平台 npm 中继基础路径(浏览器端相对当前源,如 `/api/v1/npm-relay`)。
@@ -22,5 +25,38 @@ export interface BootstrapOptions {
22
25
  runtime?: JsRuntime;
23
26
  /** npm 中继网络实现(缺省 globalThis.fetch;测试注入假中继)。 */
24
27
  fetchImpl?: CloudFetch;
28
+ /**
29
+ * 平台 HTTP 中继基础路径(浏览器端相对当前源,如 `/api/v1/http-relay`)。
30
+ * shell 的 `curl` / `wget` 经此中继访问任意主机(规避浏览器跨域限制;服务端带 SSRF 防护)。
31
+ * 缺省 `/api/v1/http-relay`;显式传 `net` 可整体替换网络实现。
32
+ */
33
+ httpRelayBaseUrl?: string;
34
+ /**
35
+ * `curl` / `wget` 的网络端口(受控 fetch 契约同 `cloud.fetch`)。
36
+ * 缺省按 `httpRelayBaseUrl` 装配成平台 HTTP 中继客户端;测试 / 自定义环境可直接注入。
37
+ */
38
+ net?: CloudFetch;
39
+ /**
40
+ * 注入既有 VFS 实例(FE-022):容器默认自建内存 VFS,接持久化(`createFsPersistence`)
41
+ * 需要上层先拿到同一个实例去 `hydrate()`,故开此注入点。省略即原行为。
42
+ */
43
+ vfs?: VirtualFileSystem;
44
+ /**
45
+ * cwd 变化回调(FE-022):Tab 补全要把相对路径按当前目录解析,而 `WebContainer` 公开契约
46
+ * 不含 cwd(见 `@adep/types` 的 `web-container.ts`),故以回调而非新增方法暴露,
47
+ * 避免为一个 IDE 需求改动对外 SDK 契约。
48
+ */
49
+ onCwdChange?: (cwd: string) => void;
50
+ /**
51
+ * `test` 命令跑完后的结构化报告回调(FN-022):终端里的文本报告是主口径,
52
+ * IDE 侧还要一份可渲染的数据。与 `onCwdChange` 同一取向——`WebContainer` 公开契约
53
+ * (`@adep/types`)不含这两者,故经装配选项回灌,不为此改动对外 SDK 契约。
54
+ */
55
+ onTestResult?: (report: TestReport) => void;
56
+ /**
57
+ * `.vue` SFC 编译器注入点(`vite` 命令):IDE 侧动态 import `vue/compiler-sfc` 后装配
58
+ * (本包零第三方依赖,不直接依赖 vue);未注入时 vite 遇 `.vue` 模块如实报「未装配」。
59
+ */
60
+ viteCompiler?: SfcCompiler;
25
61
  }
26
62
  export declare function bootstrap(options: BootstrapOptions): WebContainer;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Web IDE 预览 ↔ 云函数草稿的 postMessage 桥(CLI-014 的 Web IDE 对应物)。
3
+ *
4
+ * 浏览器内 vite-dev 预览(blob URL)与 Nodebox 真 vite 预览(异源 iframe)都不直接处理
5
+ * HTTP 请求——它们把 `/api/*` 当成「要发到后端的请求」。本模块在预览 iframe 内注入一段
6
+ * fetch 拦截器,把 `/api/*` 请求经 `window.parent.postMessage` 转发到 IDE 主线程;主线程
7
+ * 用 `OfflineRunner` 执行**当前项目的云函数草稿**(改函数即生效,因为每次都实时取草稿),
8
+ * 再把执行结果 postMessage 回 iframe。与 origin 无关:blob URL 与异源 Nodebox 隧道都能用。
9
+ *
10
+ * 消息协议(两侧逐字对齐;改任一侧必须同步另一侧——见 web-project-template.ts 的
11
+ * web/vite.config.ts 模板里那份逐字复制的 FN_PROXY_SCRIPT):
12
+ * - 请求(iframe → parent):`{ __adepFnRequest: true, id, method, url, headers, body }`
13
+ * - `url` 为 pathname + search(不含 origin),如 `/api/hello?a=1`;
14
+ * - `body` 为已读成文本的请求体(GET 为 null)。
15
+ * - 响应(parent → iframe,经 event.source.postMessage 单播,不广播):
16
+ * `{ __adepFnResponse: true, id, status, headers, body }`
17
+ * - `body` 为文本(对象侧已 JSON.stringify)。
18
+ *
19
+ * 函数名解析与 CLI-014 `resolveRouteFnName` 同口径:`/api/{fnName}/...` 剥 `/api/` 后第一段
20
+ * 为函数名,剩余路径作为 `ctx.path`(无剩余则 `/`)。
21
+ */
22
+ import type { CapabilityBundle } from '@adep/types';
23
+ import type { OfflineRunInput, OfflineRunResult } from './sim/runner';
24
+ /** 离线函数运行器的结构面(`createOfflineFunctionRunner()` 的返回值;这里只依赖 `run`)。 */
25
+ export interface OfflineRunner {
26
+ run(input: OfflineRunInput): Promise<OfflineRunResult>;
27
+ }
28
+ /**
29
+ * 浏览器端 fetch 拦截器脚本(IIFE,纯 JS——必须能直接内联进 `<script>`,不能有 TS / ESM)。
30
+ *
31
+ * **同步纪律**:本字符串与 `shared/sdk/web-project-template.ts` 里 web/vite.config.ts 模板的
32
+ * `FN_PROXY_SCRIPT` 必须**逐字一致**(两处复制粘贴,无共享 import——预览 iframe 与浏览器内
33
+ * vite-dev 是两个独立加载上下文,共享一份源码反而会引入打包耦合)。改这里务必同步另一侧。
34
+ */
35
+ export declare const FN_FETCH_INTERCEPTOR_SCRIPT: string;
36
+ /** IDE 主线程侧的消息处理器类型:直接挂到 `window.addEventListener('message', handler)`。 */
37
+ export type FunctionFetchHandler = (event: MessageEvent) => void;
38
+ export interface FunctionFetchHandlerOptions {
39
+ /**
40
+ * 按函数名取草稿文件表(函数名 → 相对路径 → 文件内容)。同步返回当前草稿(含未保存改动);
41
+ * 也允许异步(如对未打开的函数回源拉取)。返回 null / 空表 → 404 FN_NOT_FOUND。
42
+ */
43
+ getFunctionFiles: (name: string) => Record<string, string> | null | Promise<Record<string, string> | null>;
44
+ /** 离线函数运行器(`createBrowserSimRuntime().runner`)。 */
45
+ runner: OfflineRunner;
46
+ /** 能力 bundle(`createBrowserSimRuntime().bundle`)——函数内 cloud.* 的落点。 */
47
+ bundle: CapabilityBundle;
48
+ }
49
+ /**
50
+ * 创建 IDE 主线程侧的 message 处理器:收 iframe 的 `__adepFnRequest` → 解析函数名 → 取草稿
51
+ * → 离线执行 → 经 `event.source.postMessage` 单播回响应。非本协议的消息原样放过(不影响
52
+ * IDE 页面其它 postMessage 用途)。
53
+ */
54
+ export declare function createFunctionFetchHandler(options: FunctionFetchHandlerOptions): FunctionFetchHandler;
package/dist/index.d.ts CHANGED
@@ -16,9 +16,32 @@ export type { BootstrapOptions } from './container';
16
16
  export { VirtualFileSystem } from './vfs';
17
17
  export { Shell } from './shell';
18
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';
19
+ export { createWorkerRuntime, evaluateSource, evaluateModuleSource } from './worker-runtime';
20
+ export type { WorkerRuntimeOptions, WorkerLike, ModuleEvalOptions } from './worker-runtime';
21
+ export { createNpmClient, parseSpec, DEFAULT_MAX_INSTALL_DEPTH } from './npm-client';
22
+ export type { NpmClientOptions, NpmInstallResult, NpmInstalledPackage, NpmInstallStorage, NpmPersistence, } from './npm-client';
23
+ export { createRelayFetch } from './relay-fetch';
24
+ export type { RelayFetchOptions, RelayEnvelope } from './relay-fetch';
25
+ export { gunzip, unpackTar, unpackNpmTarball } from './tar';
26
+ export type { TarEntry } from './tar';
27
+ export { ResolveError, parsePackageMeta, resolveImport, resolvePackageEntry, splitPackageSpecifier, RESOLVE_EXTENSIONS, } from './node-resolve';
28
+ export type { ResolveFs, PackageMeta, ExportCondition } from './node-resolve';
29
+ export { createRequire, createSandboxModuleGlobals, interopDefault, transformStaticImports, usesModuleSyntax, } from './module-loader';
30
+ export type { ModuleCache, RequireOptions } from './module-loader';
31
+ export { createViteServer, scanEsmSpecifiers, ViteDevError } from './vite-dev';
32
+ export type { SfcCompiler, ViteServer, ViteServerOptions, ViteCommandPort, ViteCommandOutcome, EsmSpecifierHit, } from './vite-dev';
23
33
  export * as posix from './path';
34
+ export { snapshotVfs, restoreVfs, entriesToTree, createMemoryFsBackend, createIndexedDbFsBackend, pickDefaultFsBackend, createFsPersistence, createMemoryKvStorage, createCommandHistory, } from './persistence';
35
+ export type { FsPersistEntry, FsPersistenceBackend, FsPersistence, FsPersistenceOptions, CommandHistory, KvStorage, } from './persistence';
36
+ export { SHELL_COMMANDS, EXTRA_COMMANDS, COMPLETABLE_COMMANDS, parseCompletionContext, completeWithListing, completeShellInput, completeContainerInput, applyCompletion, candidateLabel, } from './completion';
37
+ export type { CompletionContext, CompletionResult, CompletionFsView, AsyncCompletionFsView, } from './completion';
38
+ export { stripTypes, UnsupportedTsSyntaxError } from './strip-types';
39
+ export { createTestRunner, collectTestFiles, formatTestReport, summaryLine, exampleTestSource, TEST_FILE_PATTERN, displayPath, } from './test-runner';
40
+ export type { TestRunnerOptions, TestRunOutcome, TestCommandPort, TestEvaluator, } from './test-runner';
41
+ export { createTestScope, runCollectedTests, expect as expectAssertion, deepEqual, formatValue, fullName, TestAssertionError, } from './test-framework';
42
+ export type { TestReport, TestFileResult, TestCaseResult, TestScope, TestRegistration, Expectation, } from './test-framework';
43
+ export * from './offline';
44
+ export * from './sim';
45
+ export { FN_FETCH_INTERCEPTOR_SCRIPT, createFunctionFetchHandler } from './function-fetch-proxy';
46
+ export type { FunctionFetchHandler, FunctionFetchHandlerOptions, OfflineRunner, } from './function-fetch-proxy';
24
47
  export type { WebContainer, WebContainerDirectoryTree, WebContainerEventMap, WebContainerFsEntry, WebContainerProcess, } from '@adep/types';