@adep/web-container 0.1.0 → 0.1.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/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", "test", "node", "js", "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,9 @@
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';
14
16
  export interface BootstrapOptions {
15
17
  /**
16
18
  * 平台 npm 中继基础路径(浏览器端相对当前源,如 `/api/v1/npm-relay`)。
@@ -22,5 +24,22 @@ export interface BootstrapOptions {
22
24
  runtime?: JsRuntime;
23
25
  /** npm 中继网络实现(缺省 globalThis.fetch;测试注入假中继)。 */
24
26
  fetchImpl?: CloudFetch;
27
+ /**
28
+ * 注入既有 VFS 实例(FE-022):容器默认自建内存 VFS,接持久化(`createFsPersistence`)
29
+ * 需要上层先拿到同一个实例去 `hydrate()`,故开此注入点。省略即原行为。
30
+ */
31
+ vfs?: VirtualFileSystem;
32
+ /**
33
+ * cwd 变化回调(FE-022):Tab 补全要把相对路径按当前目录解析,而 `WebContainer` 公开契约
34
+ * 不含 cwd(见 `@adep/types` 的 `web-container.ts`),故以回调而非新增方法暴露,
35
+ * 避免为一个 IDE 需求改动对外 SDK 契约。
36
+ */
37
+ onCwdChange?: (cwd: string) => void;
38
+ /**
39
+ * `test` 命令跑完后的结构化报告回调(FN-022):终端里的文本报告是主口径,
40
+ * IDE 侧还要一份可渲染的数据。与 `onCwdChange` 同一取向——`WebContainer` 公开契约
41
+ * (`@adep/types`)不含这两者,故经装配选项回灌,不为此改动对外 SDK 契约。
42
+ */
43
+ onTestResult?: (report: TestReport) => void;
25
44
  }
26
45
  export declare function bootstrap(options: BootstrapOptions): WebContainer;
package/dist/index.d.ts CHANGED
@@ -16,9 +16,26 @@ 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 { gunzip, unpackTar, unpackNpmTarball } from './tar';
24
+ export type { TarEntry } from './tar';
25
+ export { ResolveError, parsePackageMeta, resolveImport, resolvePackageEntry, splitPackageSpecifier, RESOLVE_EXTENSIONS, } from './node-resolve';
26
+ export type { ResolveFs, PackageMeta, ExportCondition } from './node-resolve';
27
+ export { createRequire, createSandboxModuleGlobals, interopDefault, transformStaticImports, usesModuleSyntax, } from './module-loader';
28
+ export type { ModuleCache, RequireOptions } from './module-loader';
23
29
  export * as posix from './path';
30
+ export { snapshotVfs, restoreVfs, entriesToTree, createMemoryFsBackend, createIndexedDbFsBackend, pickDefaultFsBackend, createFsPersistence, createMemoryKvStorage, createCommandHistory, } from './persistence';
31
+ export type { FsPersistEntry, FsPersistenceBackend, FsPersistence, FsPersistenceOptions, CommandHistory, KvStorage, } from './persistence';
32
+ export { SHELL_COMMANDS, EXTRA_COMMANDS, COMPLETABLE_COMMANDS, parseCompletionContext, completeWithListing, completeShellInput, completeContainerInput, applyCompletion, candidateLabel, } from './completion';
33
+ export type { CompletionContext, CompletionResult, CompletionFsView, AsyncCompletionFsView, } from './completion';
34
+ export { stripTypes, UnsupportedTsSyntaxError } from './strip-types';
35
+ export { createTestRunner, collectTestFiles, formatTestReport, summaryLine, exampleTestSource, TEST_FILE_PATTERN, displayPath, } from './test-runner';
36
+ export type { TestRunnerOptions, TestRunOutcome, TestCommandPort, TestEvaluator, } from './test-runner';
37
+ export { createTestScope, runCollectedTests, expect as expectAssertion, deepEqual, formatValue, fullName, TestAssertionError, } from './test-framework';
38
+ export type { TestReport, TestFileResult, TestCaseResult, TestScope, TestRegistration, Expectation, } from './test-framework';
39
+ export * from './offline';
40
+ export * from './sim';
24
41
  export type { WebContainer, WebContainerDirectoryTree, WebContainerEventMap, WebContainerFsEntry, WebContainerProcess, } from '@adep/types';