@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.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 离线函数运行器(FN-023 范围 3):断网在浏览器里执行「用 cloud.* 的草稿」,语义与线上执行面同形。
3
+ *
4
+ * 执行链与 FN-022 测试运行器同族:`stripTypes`(TS 类型擦除)→ `transformModule`
5
+ * (@adep/runtime 的 ESM→CJS 精简变换,`export default` → `module.default`,与线上沙箱
6
+ * 同一份)→ `evaluateModuleSource`(import 变换 + require 注入 + `with (sandbox)` 隔离求值)。
7
+ * `ctx` 按线上同形构造(`sim/ctx.ts` 的 cloud 装配 + method/path/query/headers/body/files/user)。
8
+ *
9
+ * 日志:沙箱 console 落**本地环形缓冲**(超上限丢最旧),条目带 `local: true`——
10
+ * 调试面板离线态渲染时标注「本地」(范围 4)。
11
+ *
12
+ * 边界(fail loud,不静默降级):不支持的 TS 构造 → `UnsupportedTsSyntaxError` 如实上报;
13
+ * 草稿没有 default 导出 → `FN_NO_HANDLER`;执行超时 → `FN_EXEC_TIMEOUT`(浏览器主线程
14
+ * 无法强杀一个卡死的异步句柄,超时只是放弃等待并如实回报)。
15
+ */
16
+ import type { CapabilityBundle } from '@adep/types';
17
+ /** 一条本地执行日志(调试面板离线态的渲染源)。 */
18
+ export interface LocalLogEntry {
19
+ id: number;
20
+ level: 'log' | 'warn' | 'error';
21
+ message: string;
22
+ /** ISO 8601(本地时钟;离线态没有服务端时间戳)。 */
23
+ ts: string;
24
+ }
25
+ /** 离线执行的请求构造(与调试面板在线 payload 同形状)。 */
26
+ export interface OfflineRunRequest {
27
+ method: string;
28
+ path: string;
29
+ query?: Record<string, string | readonly string[]>;
30
+ headers?: Record<string, string>;
31
+ body?: unknown;
32
+ }
33
+ /** 离线执行结果(调试面板 RunResult 的超集:日志带 local 标记)。 */
34
+ export interface OfflineRunResult {
35
+ status: number;
36
+ body: unknown;
37
+ durationMs: number;
38
+ logs: LocalLogEntry[];
39
+ error?: {
40
+ code: string;
41
+ message: string;
42
+ };
43
+ }
44
+ export interface OfflineRunInput {
45
+ /** 草稿整组文件(相对函数根,含入口 index.ts)。 */
46
+ files: Record<string, string>;
47
+ /** 入口文件(相对函数根,如 `index.ts`)。 */
48
+ entry: string;
49
+ request: OfflineRunRequest;
50
+ /** 能力 bundle(`createBrowserSimRuntime` 合并后的那份)。 */
51
+ bundle: CapabilityBundle;
52
+ }
53
+ export interface OfflineRunnerOptions {
54
+ /** 单次执行超时(缺省 10s)。 */
55
+ timeoutMs?: number;
56
+ /** 日志环形缓冲上限(缺省 200,超出丢最旧)。 */
57
+ maxLogEntries?: number;
58
+ now?: () => number;
59
+ }
60
+ /**
61
+ * 创建离线函数运行器。每次 `run` 用全新 VFS / 沙箱(无跨次状态泄漏;
62
+ * 跨次共享的状态只存在于能力 bundle 背后的持久层——IndexedDB,这正是 Gherkin 场景 1 要的)。
63
+ */
64
+ export declare function createOfflineFunctionRunner(options?: OfflineRunnerOptions): {
65
+ run(input: OfflineRunInput): Promise<OfflineRunResult>;
66
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 浏览器模拟运行时组合根(FN-023):db / storage / realtime 三能力合并成一份 bundle,
3
+ * 加上离线函数运行器——与 CLI `createSimRuntime` 同构(能力 + rpcHandlers 合并 + 生命周期),
4
+ * 浏览器侧多暴露「回网切换」与「离线执行」两个入口给 IDE 消费。
5
+ *
6
+ * 一致性红线同 CLI:能力装配**只读复用线上工厂**(createDbCapability / createStorageCapability /
7
+ * SimRealtime 语义层),本文件只做合并与生命周期;契约由 `shared/sim-contract.ts` 钉死。
8
+ */
9
+ import type { CapabilityBundle } from '@adep/types';
10
+ import { createBrowserSimDb } from './db';
11
+ import { createBrowserRealtime } from './realtime';
12
+ import type { BrowserRealtimeOptions } from './realtime';
13
+ import { createBrowserSimStorage } from './storage';
14
+ import { createOfflineFunctionRunner } from './runner';
15
+ import type { OfflineRunnerOptions } from './runner';
16
+ import type { SimKvBackend } from './kv';
17
+ export interface BrowserSimRuntimeOptions {
18
+ /** 项目 id(db / storage 分区 + realtime 换票入参)。 */
19
+ projectId: string;
20
+ /** 持久化后端(缺省浏览器 IndexedDB / 其余内存);测试注入内存后端。 */
21
+ kv?: SimKvBackend;
22
+ /** 签名 URL 基址(浏览器缺省 location.origin)。 */
23
+ baseUrl?: string;
24
+ /** 签名密钥(模拟密钥)。 */
25
+ secret?: string;
26
+ /** 项目存储配额字节(缺省与线上同参 1GiB)。 */
27
+ quotaBytes?: number;
28
+ /** realtime 传输切换的接线(换票 / WS 工厂;Node 测试注入)。 */
29
+ realtime?: Omit<BrowserRealtimeOptions, 'projectId'>;
30
+ /** 离线运行器参数(超时 / 日志上限)。 */
31
+ runner?: OfflineRunnerOptions;
32
+ }
33
+ export interface BrowserSimRuntime {
34
+ /** 合并后的能力 bundle(capabilities + rpcHandlers),喂给离线运行器。 */
35
+ bundle: CapabilityBundle;
36
+ /** cloud.db 背后的引擎(schema 水合 / 测试断言面)。 */
37
+ db: ReturnType<typeof createBrowserSimDb>;
38
+ /** cloud.storage 背后的驱动(测试断言面)。 */
39
+ storage: ReturnType<typeof createBrowserSimStorage>;
40
+ /** cloud.realtime 背后的广播与传输切换器。 */
41
+ realtime: ReturnType<typeof createBrowserRealtime>;
42
+ /** 离线函数运行器(断网在浏览器执行草稿)。 */
43
+ runner: ReturnType<typeof createOfflineFunctionRunner>;
44
+ /** 释放资源(当前无长持句柄;与 CLI createSimRuntime.dispose 对称保留)。 */
45
+ dispose(): Promise<void>;
46
+ }
47
+ /** 装配浏览器模拟运行时(async:db 引擎要先把 IndexedDB 里的快照灌回来)。 */
48
+ export declare function createBrowserSimRuntime(options: BrowserSimRuntimeOptions): Promise<BrowserSimRuntime>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 浏览器模拟 `cloud.storage`(FN-023):IndexedDB/内存 `StorageDriver` → 线上
3
+ * `createStorageCapability` 装配(含同款签名 URL——HMAC 本体已纯化,全 realm 同一份实现)。
4
+ *
5
+ * 一致性红线:只替换「持久化落点」(磁盘目录 → KV 记录),路径校验 / 错误码 / 配额语义 /
6
+ * 列表排序 / 签名 URL 形状全部对齐 `driver/local.ts` 与 `storage/cloud.ts`——
7
+ * 契约表(STORAGE_PROVIDER_CASES)在本仓测试里钉死。
8
+ *
9
+ * 边界(与 CLI sim 同族,启动/文档不再各写一份):签名 URL 的密钥是本地模拟密钥,
10
+ * 离线态该 URL 不可访问;回网后由平台校验的是**平台**密钥签发的 URL,本模拟 URL 不跨系统有效。
11
+ */
12
+ import type { CapabilityBundle } from '@adep/types';
13
+ import type { StorageDriver } from '@adep/runtime/storage/driver';
14
+ import type { SimKvBackend } from './kv';
15
+ export interface CreateBrowserSimStorageOptions {
16
+ /** 项目 id(桶绑定 + 持久化分区键)。 */
17
+ projectId: string;
18
+ /** 持久化后端;缺省按环境自动选。 */
19
+ kv?: SimKvBackend;
20
+ /** 签名 URL 基址;浏览器缺省 `location.origin`,Node 测试注入。 */
21
+ baseUrl?: string;
22
+ /** 签名密钥(模拟密钥,一次运行期一致即可)。 */
23
+ secret?: string;
24
+ /** 项目配额字节;缺省 1GiB(与线上同参,测试可注入小值)。 */
25
+ quotaBytes?: number;
26
+ }
27
+ /** storage 持久化键。 */
28
+ export declare function simStorageKey(projectId: string): string;
29
+ /**
30
+ * 浏览器侧 StorageDriver:语义逐条对齐 `driver/local.ts`(路径校验 / 404 / 配额净增计 /
31
+ * 前缀列举字典序),只把「文件系统 + 旁载 meta」换成「单 KV 记录桶」。
32
+ */
33
+ export declare function createBrowserStorageDriver(options: {
34
+ projectId: string;
35
+ kv: SimKvBackend;
36
+ quotaBytes?: number;
37
+ }): StorageDriver;
38
+ /** 构造浏览器侧 `cloud.storage` 能力 bundle(线上 `createStorageCapability` 原样装配)。 */
39
+ export declare function createBrowserSimStorage(options: CreateBrowserSimStorageOptions): {
40
+ bundle: CapabilityBundle;
41
+ driver: StorageDriver;
42
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * TypeScript 类型擦除器(FN-022):把 `*.test.ts` 的源码降级成浏览器可 `new Function` 求值的 JS。
3
+ *
4
+ * **为什么自己写**:浏览器沙箱求值(`worker-runtime.ts` 的 `evaluateSource`)走 `new Function`,
5
+ * 而浏览器既没有 `Bun.Transpiler`,任务单也要求「自实现、不引第三方运行依赖」。
6
+ * `@adep/runtime` 的 `transpile()` 是宿主能力探测(Bun / `node:module`),浏览器里两条都没有;
7
+ * 更要紧的是同一份测试文件在 Node 单测与浏览器里必须**同语义**,能力探测会带来两套结果——
8
+ * 所以这里做确定性的单遍扫描擦除,两个 realm 同一实现、同一输出。
9
+ *
10
+ * **三条安全纪律**(由 `strip-types.test.ts` 逐条守住):
11
+ * 1. **对纯 JS 恒等**:只有规则明确命中的区间才擦成等长空白(换行照抄,行号不漂),其余字节
12
+ * 原样保留;字符串 / 模板串 / 正则字面量 / 注释里的 `:` `//` `<` 永不被当语法。
13
+ * 2. **判不准就不擦**:漏擦的后果是 `new Function` 抛 `SyntaxError`,由运行器如实上报该文件
14
+ * 「加载失败」——响亮地失败,绝不静默改写语义(例如把三元表达式的中间段当类型标注吃掉)。
15
+ * 3. **需要改写而非擦除的构造直接抛错**(`enum` / `namespace`):与 runtime 沙箱
16
+ * 「超出子集承诺就报错」同口径,不假装能跑。
17
+ *
18
+ * 支持子集:`interface`、`type` 别名、`import type` / `export type`、参数 / 返回值 / 变量 /
19
+ * 类成员的类型标注、解构参数标注 `{ a }: T`、可选参数 `?:`、非空断言 `!`、`as` / `satisfies`、
20
+ * 声明位泛型、`implements` 子句、类成员 TS 修饰符(`private` / `readonly` …;`static` 是真 JS,保留)。
21
+ */
22
+ /** 不受支持的 TypeScript 构造(需要真正改写代码,不是擦除)。 */
23
+ export declare class UnsupportedTsSyntaxError extends Error {
24
+ readonly construct: string;
25
+ readonly line: number;
26
+ constructor(construct: string, line: number);
27
+ }
28
+ /**
29
+ * 擦除 TypeScript 类型标注,返回可被 `new Function` 求值的 JS。
30
+ * 对不含 TS 语法的输入**逐字节恒等**;不受支持的构造抛 `UnsupportedTsSyntaxError`。
31
+ */
32
+ export declare function stripTypes(source: string): string;
package/dist/tar.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * tar.gz 解包(FN-021 浏览器依赖安装闭环):gzip 解压 + ustar 归档读取,零依赖。
3
+ *
4
+ * npm registry 的 `.tgz` = gzip(tar(ustar, "package/..."))。`DecompressionStream('gzip')`
5
+ * 在 Chromium 与 Node ≥18 双端可用(同一代码路径可在 vitest 直接跑),tar 读取按 ustar
6
+ * 头(512 字节块:name/size/typeflag/prefix)手写——手写先例见 SRC-002:
7
+ * `server/domains/src` 下的 `zip.ts` 手写 STORE-only ZIP 写器,因为新增依赖要过审计门,
8
+ * 手写百来行反而便宜。
9
+ *
10
+ * 安全:条目路径拒绝绝对路径与 `..` 穿越(zip-slip 同型攻击);不支持的 typeflag
11
+ * (硬/软链接、字符设备等)直接报错而非静默跳过,避免装出缺件半成品。
12
+ */
13
+ /** 一个 tar 文件条目(内容为原始字节,路径已去掉 `package/` 前缀之前的形态)。 */
14
+ export interface TarEntry {
15
+ path: string;
16
+ contents: Uint8Array;
17
+ }
18
+ /** gzip 解压(`DecompressionStream`:Chromium / Node ≥18 双端内建)。非 gzip 输入报可读错误。 */
19
+ export declare function gunzip(bytes: Uint8Array): Promise<Uint8Array>;
20
+ /**
21
+ * 读 ustar 归档为条目列表(仅 regular file;目录隐式由文件路径承载,跳过)。
22
+ * 支持 prefix 字段拼接与 GNU `L` 长文件名;pax 扩展头(`x`/`g`)按 npm 打包形态跳过;
23
+ * 链接/设备等类型 flag 抛错(本 SDK 只服务 registry tarball 的文件树)。
24
+ */
25
+ export declare function unpackTar(bytes: Uint8Array): TarEntry[];
26
+ /**
27
+ * 解 npm `.tgz` 字节为条目列表;剥掉 registry 归档统一的 `package/` 顶层前缀。
28
+ * 缺 `package.json` 或含两个顶层目录形态一律报错(fail loud,不产出半成品结构)。
29
+ */
30
+ export declare function unpackNpmTarball(bytes: Uint8Array): Promise<TarEntry[]>;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * 浏览器内的轻量测试框架(FN-022 范围 1):`describe` / `it` / `expect` 最小集,自实现零依赖。
3
+ *
4
+ * 为什么不用 vitest:`@adep/web-container` 要在浏览器里跑(`sideEffects: false`、依赖只有
5
+ * `@adep/types`),而 vitest 需要 Node 进程与转换管线。任务单口径是「沙箱注入
6
+ * `describe/it/expect` 最小集,自实现、不引第三方运行依赖」。
7
+ *
8
+ * 分工:
9
+ * - 本文件只管**用例注册与断言语义**(`createTestScope` 产出一组可注入沙箱的全局 + 一个执行器),
10
+ * 不碰文件系统、不碰求值路径;
11
+ * - 谁把源码喂进来(`test-runner.ts` 经 `worker-runtime` 的模块求值路径)、结果怎么打印,都在那边。
12
+ *
13
+ * 明确不支持(超出「最小集」即报错或忽略,不假装兼容 vitest):快照、模块 mock、`it.only` /
14
+ * `it.skip`、`concurrent`、异步 `describe` 体(用例注册以文件求值结束为准)、覆盖率。
15
+ */
16
+ /** 单条用例的执行结果。 */
17
+ export interface TestCaseResult {
18
+ /** 展示名(`describe` 链以 ` › ` 连接)。 */
19
+ name: string;
20
+ status: 'passed' | 'failed';
21
+ durationMs: number;
22
+ /** 失败原因(断言消息或抛错消息)。 */
23
+ message?: string;
24
+ /** 断言失败时的「期望值」文本(终端报告按行打印,Gherkin 的失败定位判据)。 */
25
+ expected?: string;
26
+ /** 断言失败时的「实际值」文本。 */
27
+ received?: string;
28
+ }
29
+ /** 单个测试文件的聚合结果。 */
30
+ export interface TestFileResult {
31
+ /** 展示路径(相对 VFS 根,不带前导 `/`)。 */
32
+ file: string;
33
+ cases: TestCaseResult[];
34
+ /** 文件求值本身失败(语法错误 / import 失败 / 注册期抛错)时非空,此时 `cases` 为空。 */
35
+ loadError?: string;
36
+ /** 用例里 `console.log` 的输出(与 `node <file>` 同一收集路径,报告按行缩进展示)。 */
37
+ output?: string;
38
+ }
39
+ /** 一次 `test` 命令的完整报告(同时供终端打印与 IDE 面板消费)。 */
40
+ export interface TestReport {
41
+ files: TestFileResult[];
42
+ passed: number;
43
+ failed: number;
44
+ /** 求值失败的文件数(与「用例失败」分列:前者是跑不起来,后者是断言不成立)。 */
45
+ errors: number;
46
+ total: number;
47
+ durationMs: number;
48
+ /** `failed === 0 && errors === 0`(退出码语义的唯一判据)。 */
49
+ ok: boolean;
50
+ }
51
+ /** 用例体:可同步可异步,抛错即失败。 */
52
+ export type TestFn = () => void | Promise<void>;
53
+ type HookFn = () => void | Promise<void>;
54
+ export interface TestRegistration {
55
+ /** `it()` 自己的用例名(`chain` 只含所属 `describe` 链)。 */
56
+ name: string;
57
+ chain: readonly string[];
58
+ fn: TestFn;
59
+ /** 注册时所在 `describe` 节点的钩子链(含祖先)。 */
60
+ hooks: {
61
+ beforeEach: readonly HookFn[];
62
+ afterEach: readonly HookFn[];
63
+ };
64
+ }
65
+ /** 断言失败(与用例内抛错区分开:前者带 expected / received,报告里要分行打印)。 */
66
+ export declare class TestAssertionError extends Error {
67
+ readonly expected?: string | undefined;
68
+ readonly received?: string | undefined;
69
+ constructor(message: string, expected?: string | undefined, received?: string | undefined);
70
+ }
71
+ /** 一个测试文件对应一个作用域(注册表 + 注入沙箱的全局对象)。 */
72
+ export interface TestScope {
73
+ /** 注入沙箱的全局:`describe` / `it` / `test` / `expect` / `beforeEach` / `afterEach`。 */
74
+ globals: Record<string, unknown>;
75
+ /** 求值结束后按注册顺序取出用例。 */
76
+ drain(): TestRegistration[];
77
+ }
78
+ /** 创建一次文件求值用的测试作用域(超时由 `runCollectedTests` 在执行阶段把关)。 */
79
+ export declare function createTestScope(): TestScope;
80
+ /** 用例全名(`describe` 链 + 用例名)。 */
81
+ export declare function fullName(test: TestRegistration): string;
82
+ /**
83
+ * 执行一个文件里注册的全部用例:逐条 `beforeEach → 用例 → afterEach`,任何一条抛错只记在该条上
84
+ * (不中断整个文件——一次改坏一个断言不该让后面的用例失去可见性)。
85
+ */
86
+ export declare function runCollectedTests(tests: readonly TestRegistration[], options?: {
87
+ timeoutMs?: number;
88
+ now?: () => number;
89
+ }): Promise<TestCaseResult[]>;
90
+ /** 一条断言链(`.not` 取反,其余匹配器失败抛 `TestAssertionError`)。 */
91
+ export interface Expectation {
92
+ toBe(expected: unknown): void;
93
+ toEqual(expected: unknown): void;
94
+ toBeTruthy(): void;
95
+ toBeFalsy(): void;
96
+ toBeNull(): void;
97
+ toBeUndefined(): void;
98
+ toBeDefined(): void;
99
+ toContain(expected: unknown): void;
100
+ toHaveLength(length: number): void;
101
+ toThrow(expected?: string | RegExp): void;
102
+ readonly not: Expectation;
103
+ }
104
+ /** 沙箱注入的 `expect`:`expect(value).toBe(expected)`。 */
105
+ export declare function expect(actual: unknown): Expectation;
106
+ /** 深相等:数组 / 普通对象 / Date / RegExp 递归比对,其余按引用(`Object.is`)。 */
107
+ export declare function deepEqual(a: unknown, b: unknown): boolean;
108
+ /** 值的展示形式(报告与断言消息共用;不可序列化的一律降级,绝不让打印本身抛错)。 */
109
+ export declare function formatValue(value: unknown): string;
110
+ export {};
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 浏览器内测试运行器(FN-022):`test <file|dir>` 的收集、执行与报告。
3
+ *
4
+ * 与 `node <file>` **共用求值路径**(任务单范围 4):源码经 `strip-types.ts` 擦掉类型标注后,
5
+ * 交给 `worker-runtime.evaluateModuleSource`——同一条「import 变换 + require 注入 + `evaluateSource`」
6
+ * 链,所以测试里的 `console.log` 收集方式与 `with (sandbox)` 隔离语义跟直接跑脚本完全一致。
7
+ * 这里不另起执行器,也不走 Worker:测试全局是同步注入的,Worker 的异步消息协议做不到。
8
+ *
9
+ * 约定(README 与模板注释同源):**文件名以 `.test.ts` / `.spec.ts` 结尾即被收集**,目录递归,
10
+ * `node_modules/` 与隐藏目录不进入。`describe` / `it` / `expect` 是沙箱注入的全局,
11
+ * 测试文件不需要 import 任何测试框架。
12
+ *
13
+ * 退出码语义(与 vitest 对齐):全部通过 0;有失败 / 有加载错误 / 没找到用例 → 非 0。
14
+ */
15
+ import type { VirtualFileSystem } from './vfs';
16
+ import { type TestReport } from './test-framework';
17
+ /** 收集约定:`*.test.ts` / `*.spec.ts`(`.js` / `.mjs` / `.cjs` 同样收,`.d.ts` 不算)。 */
18
+ export declare const TEST_FILE_PATTERN: RegExp;
19
+ /** 求值适配:`(擦除类型后的源码, 文件绝对路径, 注入全局) → 沙箱 console 输出文本`。 */
20
+ export type TestEvaluator = (source: string, file: string, globals: Record<string, unknown>) => Promise<string>;
21
+ export interface TestRunnerOptions {
22
+ vfs: VirtualFileSystem;
23
+ /** 求值实现(缺省走 `evaluateModuleSource`,与 `node <file>` 同一条路径)。 */
24
+ evaluate?: TestEvaluator;
25
+ /** 单条用例超时(默认 5s:浏览器主线程不能把一个卡住的 `await` 放任成挂死的终端)。 */
26
+ timeoutMs?: number;
27
+ now?: () => number;
28
+ }
29
+ /** 一次 `test` 命令的产物:终端打印用 `stdout` / `stderr` / `code`,IDE 面板用 `report`。 */
30
+ export interface TestRunOutcome {
31
+ code: number;
32
+ stdout: string;
33
+ stderr: string;
34
+ report: TestReport;
35
+ }
36
+ /** shell 侧消费的最小接口(`ShellCtx.test`):容器在装配处包一层,把报告回灌给 IDE 面板。 */
37
+ export interface TestCommandPort {
38
+ run(target: string): Promise<{
39
+ code: number;
40
+ stdout: string;
41
+ stderr: string;
42
+ }>;
43
+ }
44
+ /** 创建测试运行器:`run(target)` 的 `target` 是已按 cwd 解析过的绝对路径。 */
45
+ export declare function createTestRunner(options: TestRunnerOptions): {
46
+ run(target: string): Promise<TestRunOutcome>;
47
+ };
48
+ /** 递归收集测试文件(按路径字典序,报告才稳定可比)。 */
49
+ export declare function collectTestFiles(vfs: VirtualFileSystem, root: string): string[];
50
+ /**
51
+ * 报告文本:逐文件列用例(`✓` / `✗`),失败项紧跟期望 / 实际值,最后一行汇总计数。
52
+ * 汇总行以 `N passed` 开头(Gherkin 的通过判据),失败时补 `M failed` / `K error`。
53
+ */
54
+ export declare function formatTestReport(report: TestReport): string;
55
+ /** 汇总行(退出码 0 时只会出现 `N passed`)。 */
56
+ export declare function summaryLine(report: TestReport): string;
57
+ /** VFS 绝对路径 → 展示路径(去掉前导 `/`,终端与面板同一形态)。 */
58
+ export declare function displayPath(absPath: string): string;
59
+ /**
60
+ * `*.test.ts` 示例源码:`adep init` 模板 / 平台模板 / IDE 新建文件脚手架**共用这一份**,
61
+ * 避免「测试怎么写」的约定在多处各写一遍(§9 唯一真相源)。两条断言全过,新建即可跑绿。
62
+ */
63
+ export declare function exampleTestSource(name?: string): string;
package/dist/vfs.d.ts CHANGED
@@ -14,6 +14,14 @@ import type { WebContainerDirectoryTree, WebContainerFsEntry } from '@adep/types
14
14
  export declare const ROOT = "/";
15
15
  export declare class VirtualFileSystem {
16
16
  private readonly nodes;
17
+ /** 变更订阅者(`onMutate` 注册):任意一次写操作后同步回调,供持久化 / HMR 等上层消费。 */
18
+ private readonly mutators;
19
+ /**
20
+ * 订阅文件系统变更(FE-022 持久化的接入点):返回取消订阅函数。
21
+ * 回调在写操作**之后**同步触发,一次 `mount` 可能触发多次(内部逐条目写)——
22
+ * 消费方应自行合并(如 `createFsPersistence` 的去抖落盘),不要在此做重活。
23
+ */
24
+ onMutate(listener: () => void): () => void;
17
25
  /** 挂载一棵目录树(WebContainers `mount` 入参形态)。 */
18
26
  mount(tree: WebContainerDirectoryTree): void;
19
27
  private mountWithin;
@@ -41,6 +49,8 @@ export declare class VirtualFileSystem {
41
49
  getFileSystemTree(): WebContainerDirectoryTree;
42
50
  /** 列出兴趣路径的全部路径(用于模块解析 / glob 预载)。 */
43
51
  keys(): string[];
52
+ /** 通知全部变更订阅者(快照一份再遍历,允许订阅者在回调里退订)。 */
53
+ private notify;
44
54
  private ensureParent;
45
55
  private mkdirEnsureRecursive;
46
56
  }