@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.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 浏览器模拟运行时的持久化后端(FN-023):异步键值端口,IndexedDB / 内存双实现。
3
+ *
4
+ * 设计与 `persistence.ts`(终端 fs 快照)同一取向:浏览器 IndexedDB、Node 内存,零运行时依赖。
5
+ * 键空间约定(组合层 `runtime.ts` 分配,本文件不管语义):
6
+ * - `db:<projectId>` → SimSqlEngine 的整库表快照(JSON 可结构化克隆值);
7
+ * - `storage:<projectId>` → 项目存储桶 `{ [path]: StoredObject }`。
8
+ *
9
+ * 每次写都是「整值 put」:模拟运行时的写频度与数据量都是开发态规模,不值得做去抖/增量,
10
+ * 简单形状才没有「漏删 / 孤儿」这类错(同 `persistence.ts` 头注释的取舍)。
11
+ */
12
+ /** 浏览器模拟运行时的持久化端口(值必须是可结构化克隆的;内存实现按引用存)。 */
13
+ export interface SimKvBackend {
14
+ get(key: string): Promise<unknown>;
15
+ set(key: string, value: unknown): Promise<void>;
16
+ remove(key: string): Promise<void>;
17
+ }
18
+ /** 内存后端(Node 单测 / SSR / 隐私模式降级)。每次调用产出独立实例,测试之间互不串味。 */
19
+ export declare function createMemorySimKv(): SimKvBackend;
20
+ /** IndexedDB 后端:单库单仓(`key` 为主键),一 KV 一记录。 */
21
+ export declare function createIdbSimKv(dbName?: string): SimKvBackend;
22
+ /** 缺省后端:浏览器 IndexedDB,其余(Node 单测 / SSR)内存。 */
23
+ export declare function pickDefaultSimKv(): SimKvBackend;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 浏览器模拟 `cloud.realtime`(FN-023):离线内存广播总线(@adep/runtime/sim/realtime 的
3
+ * `SimRealtime`,与 CLI sim 同一份语义)+ **回网切真实 WS** 的传输切换。
4
+ *
5
+ * 语义层(广播 / 拉取式缓冲 / 订阅上限 / 错误码)与 CLI sim / 线上提供方共用一份真相源:
6
+ * - 错误码取 `capability-keys` 的 `REALTIME_CAPABILITY_CODES`(跨 realm 常量的家);
7
+ * - 期望表(publish 回执 / 未知订阅 / except 不可用 / 未知方法)在本仓测试里对浏览器 handler
8
+ * 逐条复跑(同 CLI 侧 REALTIME_SIM_CASES 一族)。
9
+ *
10
+ * 回网切换(Gherkin 场景 3):`goOnline()` 为每个活跃订阅开真实 WS(换票 →
11
+ * `Sec-WebSocket-Protocol` 携带票据,与服务端握手契约对齐);WS 下行消息**只**经
12
+ * 「直接推入该订阅缓冲」这一条路径进入,本地总线的监听保持挂载——两条摄入源的
13
+ * 消息集合不相交(本地 publish 只进总线、服务端消息只进 WS 推送),且每条消息
14
+ * 恰好走一条路径一次,因此不会出现双份。`goOffline()` 关 WS 回到纯本地广播。
15
+ *
16
+ * 边界:在线态下浏览器草稿的 publish 仍只进本地总线(浏览器侧没有「以平台身份发布」的
17
+ * HTTP 面;函数发布语义属于服务端执行面)。WS 断开不自动重连——重试发生在下一次
18
+ * `goOnline()`(重连风暴防护与 networkMonitor 的低频探测同口径)。
19
+ */
20
+ import { SimRealtime } from '@adep/runtime/sim/realtime';
21
+ import type { CapabilityBundle } from '@adep/types';
22
+ /** 换票回执(`POST /api/v1/realtime/ticket` 的响应形状)。 */
23
+ export interface TicketResult {
24
+ ticket: string;
25
+ }
26
+ /** 最小 WebSocket 面(鸭子类型:Node 测试注入假 WS 走同一协议)。 */
27
+ export interface MinimalWebSocket {
28
+ close(code?: number): void;
29
+ onmessage: ((event: {
30
+ data: unknown;
31
+ }) => void) | null;
32
+ onclose: ((event: {
33
+ code?: number;
34
+ }) => void) | null;
35
+ }
36
+ export interface BrowserRealtimeOptions {
37
+ /** 项目 id(换票入参)。 */
38
+ projectId: string;
39
+ /** WS 基址(缺省由 `location.origin` 推导为 ws(s)://);Node 测试注入。 */
40
+ wsBaseUrl?: string;
41
+ /** 换票提供者(缺省 POST /api/v1/realtime/ticket,credentials include)。 */
42
+ ticketProvider?: () => Promise<TicketResult>;
43
+ /** WebSocket 工厂(Node 测试注入假 WS)。 */
44
+ wsFactory?: (url: string, protocols: string[]) => MinimalWebSocket;
45
+ /** 一次会话内的订阅上限(与 CLI sim 同参 64)。 */
46
+ maxSubscriptions?: number;
47
+ /** 单订阅缓冲上限(与 CLI sim 同参 64,超出丢最旧)。 */
48
+ maxBufferedMessages?: number;
49
+ }
50
+ /**
51
+ * 构造浏览器侧 `cloud.realtime` 能力 bundle + 回网切换器。
52
+ * 传输切换器暴露给组合层(IDE 侧随 networkMonitor 状态调用 `goOnline` / `goOffline`)。
53
+ */
54
+ export declare function createBrowserRealtime(options: BrowserRealtimeOptions): {
55
+ bundle: CapabilityBundle;
56
+ realtime: SimRealtime;
57
+ goOnline(): Promise<void>;
58
+ goOffline(): void;
59
+ /** 当前处于 WS 摄入态的订阅数(诊断用)。 */
60
+ wsConnectedCount(): number;
61
+ };
@@ -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
  }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * 浏览器内 Vite 兼容 dev server(`vite` 命令本体)。
3
+ *
4
+ * preview.ts 的「虚拟静态 dev server」只内联经典脚本(`<script src>`),明确不跑
5
+ * ESM / TS / npm 依赖的编译管线(其文件头留了「替换为真正的 Vite pipeline」的接口)。
6
+ * 本模块补上这一层——在浏览器内、零第三方运行时约束下实现 Vite dev server 的核心语义:
7
+ *
8
+ * - **入口**:root 下的 `index.html`(或选项指定),`<script type="module">` 是模块入口;
9
+ * - **模块图**:递归解析 ESM import / export…from / 动态 import(),每个模块转换后生成
10
+ * 一个 `blob:` URL,模块里的说明符被改写成目标的 blob URL——整条链是浏览器原生 ESM
11
+ * (真 live binding、真模块隔离),不是 CJS 打平;
12
+ * - **转换**:`.ts` 复用 stripTypes 擦类型;`.css` 变成「注入 `<style>` 的副作用模块」;
13
+ * `.json` 变成默认导出对象;`.vue` 经注入的 SFC 编译器(IDE 侧装 `vue/compiler-sfc`);
14
+ * - **依赖**:裸说明符经 node-resolve 命中 VFS `node_modules/`(终端 `npm install` 的产物),
15
+ * 与 CLI / 线上同一套解析规则;相对说明符支持 Vite 风格省略扩展名(.ts/.js/.vue/…);
16
+ * - **HMR**:订阅 VFS 变更(`vfs.onMutate`,IDE 保存 / shell 写入 / npm 解包皆触发)→
17
+ * 防抖全量重建模块图 → 入口文档变化则经 `BroadcastChannel` 广播新 blob URL,预览 iframe
18
+ * `location.replace` 自刷新(整页刷新式 HMR,组件级热替换留后续);构建 / 解析失败时生成
19
+ * 「红屏错误页」而不是静默白屏。
20
+ *
21
+ * 边界(响亮失败,不静默降级):
22
+ * - **循环依赖直接报错**(与 ide-widget-bundle 同口径):blob 链的改写发生在加载期,
23
+ * 环会让「先有 URL 才能写引用」无解,如实报告环路径链;
24
+ * - `.tsx` / `.jsx` 不支持(stripTypes 只擦类型标注,不转 JSX),明确报错;
25
+ * - Node 内建模块(`node:`)与 CJS-only 包不支持(解析器只认 ESM 入口),明确报错。
26
+ */
27
+ import type { VirtualFileSystem } from './vfs';
28
+ /** SFC 编译器注入契约:`vue/compiler-sfc` 的适配(IDE 侧装配,本包零依赖不引 vue)。 */
29
+ export interface SfcCompiler {
30
+ /**
31
+ * 编译单个 `.vue` 文件。`script` 必须是纯 JS(可含 ESM import,供后续 blob 改写),
32
+ * `styles` 为已编译的 CSS 文本(逐条注入副作用模块)。
33
+ */
34
+ (source: string, filename: string): {
35
+ script: string;
36
+ styles: string[];
37
+ };
38
+ }
39
+ export interface ViteServerOptions {
40
+ /** dev server 根目录(VFS 绝对路径,缺省 '/')。 */
41
+ root?: string;
42
+ /** 入口文件名(相对 root,缺省 'index.html')。 */
43
+ entry?: string;
44
+ /** SFC 编译器(缺省时 `.vue` 模块报「未装配」)。 */
45
+ sfcCompiler?: SfcCompiler;
46
+ /** blob URL 工厂(Node 单测注入假实现)。 */
47
+ createObjectURL?: (blob: Blob) => string;
48
+ revokeObjectURL?: (url: string) => void;
49
+ }
50
+ /** dev server 句柄:url / root 对外只读,`touch` 驱动 HMR,`stop` 释放资源。 */
51
+ export interface ViteServer {
52
+ /** 当前入口文档的 blob URL(首次 `ready` 完成后有效)。 */
53
+ readonly url: string;
54
+ /** 虚拟端口(Vite 默认 5173,仅语义展示)。 */
55
+ readonly port: number;
56
+ /** root 的规范化绝对路径(幂等启动判定用)。 */
57
+ readonly root: string;
58
+ /** 首次构建完成(含错误文档——失败也是一次完成)。 */
59
+ readonly ready: Promise<void>;
60
+ /** 某路径发生变更:防抖全量重建,文档变化则广播 HMR。 */
61
+ touch(path: string): void;
62
+ /** 停止:撤销全部 blob URL、关闭 HMR 通道。 */
63
+ stop(): Promise<void>;
64
+ }
65
+ /** vite 构建期错误(红屏文档的 reason + message)。 */
66
+ export declare class ViteDevError extends Error {
67
+ readonly reason: string;
68
+ constructor(reason: string, message: string);
69
+ }
70
+ /** `vite` 命令端口(shell 的 `case 'vite'` 消费;`bootstrap` 装配实现)。 */
71
+ export interface ViteCommandPort {
72
+ /** `vite [dir]` / `vite dev [dir]`:启动(同根幂等复用,异根重启)。 */
73
+ start(args: readonly string[]): Promise<ViteCommandOutcome>;
74
+ /** `vite stop`:停止并释放。 */
75
+ stop(): Promise<ViteCommandOutcome>;
76
+ }
77
+ export interface ViteCommandOutcome {
78
+ code: number;
79
+ stdout: string;
80
+ }
81
+ /** 一次说明符命中:`start`/`end` 为含引号的原文区间(end 不含),改写按此切片替换。 */
82
+ export interface EsmSpecifierHit {
83
+ specifier: string;
84
+ start: number;
85
+ end: number;
86
+ }
87
+ /**
88
+ * 扫描源码里的 ESM 模块说明符(静态 import / export…from / 动态 import())。
89
+ * 说明符是这些语句里唯一的字符串字面量(`from` 别名 / 对象键等场景的字符串前必有
90
+ * `=` `:` `,` `(` 等标点,天然不命中),因此「字符串 + 前一个 token」即可可靠判定。
91
+ * 模板串整体跳过(`${}` 里的动态 import 不在支持范围,见文件头边界注)。
92
+ */
93
+ export declare function scanEsmSpecifiers(source: string): EsmSpecifierHit[];
94
+ export declare function createViteServer(vfs: VirtualFileSystem, options?: ViteServerOptions): ViteServer;
@@ -26,7 +26,22 @@ export interface WorkerResponse {
26
26
  error?: string;
27
27
  }
28
28
  /** 隔离上下文求值:等价于 vm.ts 的 IFFE 包装,但跑在浏览器任意全局(Node 测试 / Worker 共用)。 */
29
- export declare function evaluateSource(source: string, filename: string, args: readonly string[]): Promise<string>;
29
+ export declare function evaluateSource(source: string, filename: string, args: readonly string[], extraGlobals?: Record<string, unknown>): Promise<string>;
30
+ /** `evaluateModuleSource` 的入参。 */
31
+ export interface ModuleEvalOptions {
32
+ /** 传给脚本的 `__args`(`node <file> a b` 的参数)。 */
33
+ args?: readonly string[];
34
+ /** 追加注入的沙箱全局(测试运行器用它塞 `describe/it/expect`)。 */
35
+ globals?: Record<string, unknown>;
36
+ /** 覆盖源码(测试运行器传入的是擦除类型标注后的版本);省略则从 VFS 读该文件。 */
37
+ source?: string;
38
+ }
39
+ /**
40
+ * 模块形态程序(`require` / 静态 `import`)的求值:读 VFS → import 变换 → 注入 require 全局 →
41
+ * `evaluateSource`。`node <file>` 的模块分支与 FN-022 的 `test` 命令**共用这一条路径**
42
+ * (任务单范围 4),隔离语义与 Worker 路径同款(同一 `fakeConsole` / `with (sandbox)`)。
43
+ */
44
+ export declare function evaluateModuleSource(vfs: VirtualFileSystem, file: string, options?: ModuleEvalOptions): Promise<string>;
30
45
  /** 极简 Worker 形态(鸭子类型:Node 测试注入假 Worker 走同一协议)。 */
31
46
  export interface WorkerLike {
32
47
  postMessage(message: WorkerRequest): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adep/web-container",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -16,7 +16,8 @@
16
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
17
  },
18
18
  "dependencies": {
19
- "@adep/types": "workspace:*"
19
+ "@adep/runtime": "0.2.0",
20
+ "@adep/types": "0.2.0"
20
21
  },
21
22
  "publishConfig": {
22
23
  "main": "./dist/index.js",
@@ -31,5 +32,6 @@
31
32
  "default": "./dist/*.js"
32
33
  }
33
34
  }
34
- }
35
+ },
36
+ "gitHead": "9c29cbb2a942afe4d4c7ae2987bcc229f6b99f9b"
35
37
  }