@crazx/dsh-client-modules 0.1.1-rc.2.zw.1 → 0.1.2-alpha.3.zw.2

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.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/modules/README.md
5
- README.md: eaf64599bc5fc5d9663d00c3b341764a4ccd38c8
6
- README.zh.md: ea051338bb7837cb49f7a5ecd4cdad5b6b3ad71d
5
+ README.md: 9e7dee9fd3162d7bbcec0b06294d36ffc60d0794
6
+ README.zh.md: 88fa3b7780c638bf3c41766844442a01759f861b
package/README.md CHANGED
@@ -1,22 +1,106 @@
1
+ ---
2
+ description: "Client module system for the web GUI: the host composes the boot graph and serves plugin bundles, and the browser loads them lazily, for users and maintainers composing or debugging client plugins."
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-client-modules
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- Client module system: the browser peer of Node's internal ESM loader, built as a lazy CJS table. The web shell mounts the vendored cordis Loader for entry governance (fiber lifecycle, inject waiting, update/refresh) and injects this package's `ClientModuleLoader` through its `internal` contract — the vendored side's only consumption point is `EntryTree.import`, so replacing `internal` replaces exactly "how plugin code arrives" and nothing else.
10
+ ## Summary
11
+
12
+ `dsh-client-modules` turns a plugin package's `dsh.client` declaration into a loadable browser bundle: the host half scans enabled Loader entries, composes the boot graph, and serves each bundle over `/plugins`, and the browser half loads those bundles lazily on demand. Plugin bundles execute lazily — running a bundle only registers a factory, and module side effects run at materialization — so nothing runs until a plugin is first used. Everything here is browser-kernel machinery; the model never sees it.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Use it when you compose or build a browser client plugin: the package turns a package's `dsh.client` declaration into a loadable browser bundle with no per-plugin wiring. It activates with the web composition; the shell boots it before any plugin runs.
29
+
30
+ ### Declaring a client plugin
31
+
32
+ A browser plugin package declares `dsh.client` in its `package.json` with `platform: 'web'`, exports a `./client` bundle, and lists any non-baseline module requests under `dsh.client.external`. The host half turns each declaration into a served bundle under `/plugins`, ordered so dynamic providers load before their consumers.
33
+
34
+ ### What the browser loads
35
+
36
+ The application combo scripts register plugin factories once during boot; module bodies remain lazy and run only at first import or materialization. Rows that share a combo URL share one in-flight script task. HMR switches one changed row to its revisioned one-resource combo URL. `<id>/client` and the bare id resolve to the same exports, because a plugin bundle is its package's client half.
37
+
38
+ ### Sharing modules
39
+
40
+ The shell seeds a frozen module table (`PLATFORM_MODULES`: React, Cordis, and static UI libraries); every dynamic bundle resolves its externals against exactly that baseline. `dsh.client.external` adds only exact non-baseline requests, each answered by the dynamic package row it names or an exact static-table key. Type-only imports are erased and create no request. Composition rejects malformed requests, missing suppliers, self-requests, and synchronous request cycles.
41
+
42
+ ### Build requirements
43
+
44
+ The host serves built client bundles, so `pnpm run build` must have produced each `lib/client.js` before launch; a missing bundle fails activation loudly with one build instruction and a package/path list. Source launch maps host imports to TypeScript source but still consumes the built client export. The package accepts no plugin config of its own.
45
+
46
+ -----
47
+
48
+ <a id="understand-the-implementation"></a>
49
+ ## Understand the implementation
50
+
51
+ <details>
52
+ <summary>Implementation internals — click to expand</summary>
53
+
54
+ This section explains how the module system is built; observable behavior is covered in [Use this package](#use-this-package).
6
55
 
7
- Lazy CJS model (web2): executing a plugin bundle only REGISTERS its factory (`window.__ModuleLoader__.load({id, factory})`); every module body side effect — CSS injection included — lives in the factory closure and runs at materialization (`factory(require)` → exports, memoized in `loadCache`), not at script execution. A factory that requires another registered-but-unmaterialized module materializes it recursively; graph composition places declared dynamic requests before their consumers, and require cycles throw because factory-form CJS cannot deliver partial exports. `<id>/client` and the bare id resolve to the same exports (a plugin bundle IS its package's client half).
56
+ ### Design concept
8
57
 
9
- The Host installs `window.__ModuleLoader__` before parser preloads run. Its queue-mode `load()` retains early registrations; `create()` materializes this package's factory with an external-rejecting bootstrap require and calls its `createClientModuleSystem` export. Construction caches those same exports as the modules row, switches the same facade to live registration, and drains the remaining queue. The bundle retains the resulting system in a module closure, so its later Cordis `apply()` provides the identical instance as `ctx.modules` without another page global.
58
+ The package is dual-face: the node half is the composition and serving side (`ctx.clientModules`, `ClientModuleRegistry`), the browser half is the loading side (`ctx.modules`, `ClientModuleSystem`). The wire between them is the boot graph `WebBootEntry` rows injected as `window.__DSH_BOOT__`, with `<` escaped so plugin-controlled strings cannot break out of the script element. The vendored Loader's only consumption point is `EntryTree.import`, so the module system is the single replacement for "how plugin code arrives".
10
59
 
11
- Resolution branch order (`import(specifier)`): platform seed word → shell instance; memoized record → exports; graph row (`window.__DSH_BOOT__`) → register its classic-script factory; registered factory → materialize; anything else throws — the runtime mirror of the build-time bundle purity gate. The synchronous `require` handed to factories walks the same order minus the asynchronous graph-row load and records observed edges into the module record. `prefetch` is the stage-one arrival hook (script load and factory registration only; concurrent calls share one in-flight task); `invalidate` drops a non-bootstrap factory and materialized record so the next prefetch/import reloads the script (the HMR hook).
60
+ ### Lazy-CJS model
12
61
 
13
- The Node half scans enabled Loader entries for web `dsh.client` packages, resolves each `exports["./client"]`, hashes the built bundle into the boot graph, carries package-specific `dsh.client.external` requests, orders dynamic providers before consumers, and serves each bundle with its source map under `/plugins`. Source launch maps host imports to TypeScript source but still consumes this built client export; missing files share one build instruction followed by a package/path list, while unrelated filesystem errors remain separate failures.
62
+ Executing a plugin bundle only registers its factory; every module-body side effect (CSS injection included) lives in the factory closure and runs at materialization (`factory(require)` → exports, memoized in `loadCache`). A factory that requires another registered-but-unmaterialized module materializes it recursively; require cycles throw because factory-form CJS cannot deliver partial exports. Resolution checks the platform seed table, memoized records, boot-graph rows, and registered factories in that order; anything else throws. The synchronous `require` uses the same order without asynchronous graph-row loading and records observed edges into the module record.
14
63
 
15
- `dsh.client.external` is an optional exact-specifier request list beyond the implicit baseline: shell-seeded React, Cordis, and static UI libraries plus parser-preloaded runtime. A request is answered by the dynamic package row it names or an exact static-table key; only a trailing `/client` aliases a package row, and there is no provider-alias declaration. Type-only imports are erased and create no request. Composition rejects malformed requests, missing suppliers, self-requests, and synchronous request cycles; import and prefetch recursively register dynamic suppliers before their consumers materialize. See [shared modules and the module graph](../AGENTS.md#shared-modules-and-the-module-graph).
64
+ ### Incremental composition
16
65
 
66
+ The node half scans incrementally per package — no full-rescan path. Every `internal/plugin` emission marks the fiber's entry name dirty; a microtask flush reconciles each dirty name against the live loader entries, and the activation pass seeds the same dirty set and flushes synchronously, so first scan and steady state share one implementation. Package metadata is cached per Loader specifier and owning-tree base URL until restart. The Loader specifier is the browser module id; a scoped republish whose unscoped name matches still owns that row. Distinct active Loader sources resolving to one package name are rejected; removing the conflict promotes the remaining source without requiring its fiber to restart. Bundle content changes reach the graph only through `rebuilt()` (the HMR hook).
67
+
68
+ The node half snapshots each client bundle and available source map before publication. It groups resources into `/plugins/??...&rev=...` combo URLs, with one bootstrap combo for the modules row and one or more application combos for the other rows; each phase is partitioned before a URL exceeds 3 KiB. Every combo map is Indexed Source Map v3 and uses an authored section when available or an identity section for the packaged bundle. Initial per-plugin revisions use process nonces, so startup does not hash every plugin; HMR hashes only an artifact reported as changed. Advertised responses are immutable, and an unknown combination or revision returns 404.
69
+
70
+ ### Boot manifest injection
71
+
72
+ The host taps the index render and injects, into `<head>`: the `window.__ModuleLoader__` queue facade, advisory preloads for every application combo, the parser-blocking bootstrap combo scripts, then the boot graph before the shell reads it. The facade's `create()` materializes the modules bundle, delegates construction to its `createClientModuleSystem` export, and leaves the same facade in live-registration mode.
73
+
74
+ ### Source map
75
+
76
+ | File | Role |
77
+ |---|---|
78
+ | [`src/index.ts`](src/index.ts) | Node half: `ClientModuleRegistry`, scan, artifact snapshots, combo routes, index tap |
79
+ | [`src/client/index.ts`](src/client/index.ts) | Browser half: bootstrap export, `ctx.modules` enrollment |
80
+ | [`src/client/system.ts`](src/client/system.ts) | `ClientModuleSystem`: load/materialize/invalidate machinery |
81
+ | [`src/client/manifest.ts`](src/client/manifest.ts) | Wire types and boot-manifest parsing |
82
+
83
+ </details>
84
+
85
+ -----
86
+
87
+ <a id="further-exploration"></a>
88
+ ## Further Exploration
89
+
90
+ Read these when the module contract is not enough: the subsystem reference, the shell that boots the tree, and the client authoring rules behind the graph.
91
+
92
+ - [Client modules subsystem](../../../docs/subsystems/client-modules.md) — the web plugin table, `WebBootGraph` wire, and the bundle route.
93
+ - [Web boot kernel](../web/README.md) — the shell that creates the module system and boots the plugin tree.
94
+ - [Client HMR driver](../hmr/README.md) — the reload chain that drives `invalidate`/`prefetch` on rebuilt bundles.
95
+ - [Client authoring rules](../AGENTS.md#shared-modules-and-the-module-graph) — the shared-module baseline and `dsh.client.external` semantics.
96
+ - [Client group map](../README.md) — the browser half this package belongs to.
97
+
98
+ -----
99
+
100
+ <a id="model-experience"></a>
17
101
  ## Model Experience
18
102
 
19
- None, as the module loader is browser-side kernel machinery; nothing here reaches a model request.
103
+ None, as the module loader is browser-side kernel machinery that registers nothing model-facing.
20
104
 
21
105
  #### KV Cache effect
22
106
 
@@ -24,5 +108,21 @@ None; this package neither assembles nor sends a provider request.
24
108
 
25
109
  ## Known Limitations and Deferred Work
26
110
 
111
+ <a id="known-limitations-and-deferred-work"></a>
112
+
113
+
114
+ These limits define what the module system does not do. They are current package constraints, not a task backlog.
115
+
27
116
  - **Flat module graph by design** — every bundle is one module node whose edges point only at table leaves; the interface (`loadCache`/`edges`/`invalidate`) already supports a general module graph, so the externalization granularity can change without an interface change.
28
117
  - **No unload bookkeeping of its own** — style removal and fiber teardown ordering live with the HMR driver (`@deepseek-ai/dsh-client-hmr`); the loader only inventories owned style tag ids per record.
118
+ - **Snapshot delivery retains artifact bytes** — the Host holds each bundle, optional source map, generated one-resource response, and current startup combo responses in memory; HMR additionally retains one prior startup generation. Memory scales as several copies of the composed client artifacts in exchange for immutable responses and one-generation race tolerance.
119
+
120
+ <a id="dev-note"></a>
121
+ ### Dev Note
122
+
123
+ <details>
124
+ <summary>Working context for maintainers — click to expand</summary>
125
+
126
+ None.
127
+
128
+ </details>
package/README.zh.md CHANGED
@@ -1,28 +1,128 @@
1
+ ---
2
+ description: "面向用户与维护者的 web GUI 客户端模块系统说明:宿主侧组合启动图并提供插件 bundle,浏览器侧按需加载,用于组合或排查客户端插件。"
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-client-modules
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- 客户端模块系统:Node 内部 ESM loader 的浏览器端对等实现,以惰性 CJS 表实现。web 外壳挂载 vendored cordis Loader 来治理配置项(fiber 生命周期、inject 等待、update/refresh),并通过其 `internal` 约定注入该包的 `ClientModuleLoader`;vendored 一侧唯一的消费点是 `EntryTree.import`,因此替换 `internal` 恰好只会替换「插件代码如何到达」,不会改变其他内容。
10
+ ## 概述
11
+
12
+ `dsh-client-modules` 把插件包的 `dsh.client` 声明变成可加载的浏览器 bundle:宿主半侧扫描已启用的 Loader 条目、组合启动图,并通过 `/plugins` 提供每个 bundle;浏览器半侧按需惰性加载这些 bundle。插件 bundle 惰性执行——运行 bundle 只注册 factory,模块副作用在物化时运行——因此插件首次被使用之前什么都不会运行。这里的一切都是浏览器内核机制;模型永远看不到它。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 组合或构建浏览器客户端插件时使用它:本包把包的 `dsh.client` 声明变成可加载的浏览器 bundle,无需任何逐插件接线。它随 web 组合激活;外壳在任何插件运行前启动它。
29
+
30
+ ### 声明客户端插件
31
+
32
+ 浏览器插件包在其 `package.json` 中以 `platform: 'web'` 声明 `dsh.client`,导出 `./client` bundle,并在 `dsh.client.external` 下列出任何基座之外的模块请求。宿主半侧把每份声明变成 `/plugins` 下提供的 bundle,并让动态提供方先于其消费方加载。
33
+
34
+ ### 浏览器加载什么
35
+
36
+ application combo 脚本在启动时注册插件 factory;模块主体仍保持惰性,只在首次 import 或物化时运行。共享 combo URL 的 row 共用一个进行中的脚本任务。HMR 会让一条发生变化的 row 改用带 revision 的单资源 combo URL。`<id>/client` 与裸 id 解析到同一组导出,因为插件 bundle 就是其包的客户端半侧。
37
+
38
+ ### 共享模块
39
+
40
+ 外壳播种一张冻结模块表(`PLATFORM_MODULES`:React、Cordis 与静态 UI 库);每个动态 bundle 都精确针对该基座解析其 external。`dsh.client.external` 只添加基座之外的精确请求,每个请求由其命名的动态包 row 或精确静态表键回答。纯类型 import 会被擦除,不产生请求。组合阶段会拒绝畸形请求、缺失提供方、自请求与同步请求环。
41
+
42
+ ### 构建要求
43
+
44
+ 宿主提供的是已构建的客户端 bundle,因此启动前 `pnpm run build` 必须已产出每个 `lib/client.js`;缺失 bundle 会以一条构建说明加包/路径列表的方式让激活大声失败。源码启动会把宿主侧导入映射到 TypeScript 源码,但仍消费这一构建后的客户端导出。本包自身不接受任何插件配置。
45
+
46
+ -----
47
+
48
+ <a id="understand-the-implementation"></a>
49
+ ## 理解实现
50
+
51
+ <details>
52
+ <summary>实现细节——点击展开</summary>
53
+
54
+ 本节解释模块系统的构建方式;可观察行为已在[使用本包](#use-this-package)中说明。
6
55
 
7
- 惰性 CJS 模型(web2):执行插件 bundle 只会注册其 factory(`window.__ModuleLoader__.load({id, factory})`);每个模块主体的副作用(包括 CSS 注入)都位于 factory 闭包中,在物化时运行(`factory(require)` → 导出表层,并在 `loadCache` 中记忆化),不会在脚本执行时运行。如果 factory 依赖另一个已注册但尚未物化的模块,系统会递归物化它;图组合会把声明的动态请求提供方放在消费者之前,而 require 循环会抛出异常,因为 factory 形式的 CJS 无法提供部分导出。`<id>/client` 与裸 id 指向同一表层(一个插件 bundle 就是其包的客户端侧)。
56
+ ### 设计理念
8
57
 
9
- Host 会在 parser preload 运行前安装 `window.__ModuleLoader__`。其 queue 模式的 `load()` 保存提前到达的 registration;`create()` 使用拒绝 external 的 bootstrap require 物化本包 factory,并调用其 `createClientModuleSystem` 导出。构造过程把同一组导出缓存为 modules row,把同一个 facade 切换到 live registration,再排空余下 queue。Bundle 通过模块闭包保留生成的系统,因此随后 Cordis `apply()` 能把同一实例提供为 `ctx.modules`,无需另一个页面全局变量。
58
+ 本包是双面孔:node 半侧是组合与服务端(`ctx.clientModules`,`ClientModuleRegistry`),浏览器半侧是加载端(`ctx.modules`,`ClientModuleSystem`)。两者之间的协议是启动图——以 `window.__DSH_BOOT__` 注入的 `WebBootEntry` 行,`<` 已转义,插件控制的字符串无法逃出 script 元素。vendored Loader 唯一的消费点是 `EntryTree.import`,因此模块系统就是「插件代码如何到达」的唯一可替换实现。
10
59
 
11
- 解析分支顺序(`import(specifier)`):平台种子词 外壳实例;记忆化记录 → 导出;模块图记录(`window.__DSH_BOOT__`)→ 登记其 classic-script factory;已登记 factory → 物化;其他情况一律抛出异常。这是构建时 bundle 纯度门禁的运行时镜像。交给 factory 的同步 `require` 采用相同顺序,但不含异步 graph-row 加载分支,并把观察到的边记录到模块记录中。`prefetch` 是第一阶段到达钩子(只加载脚本并登记 factory;并发调用共享一个进行中的任务);`invalidate` 会丢弃非 bootstrap factory 与物化记录,使下一次 prefetch/import 重新加载脚本;它是 HMR(热模块替换)钩子。
60
+ ### 惰性 CJS 模型
12
61
 
13
- Node 侧会扫描已启用的 Loader 配置项以发现 web `dsh.client` 包,解析每个 `exports["./client"]`,把构建后的 bundle 哈希和包专属 `dsh.client.external` 请求写入启动图,把动态提供方排在消费者之前,并通过 `/plugins` 提供该文件及其 sourcemap。源码启动会把宿主侧导入映射到 TypeScript 源码,但仍消费这一构建后的客户端导出;缺失文件共享一条构建说明,随后以包/路径列表列出各项,而无关的文件系统错误仍是独立故障。
62
+ 执行插件 bundle 只注册其 factory;每个模块主体副作用(包括 CSS 注入)都位于 factory 闭包中,在物化时运行(`factory(require)` 导出,在 `loadCache` 中记忆化)。factory 依赖另一个已注册但未物化的模块时会递归物化它;require 循环会抛出异常,因为 factory 形式的 CJS 无法提供部分导出。解析会依次检查平台 seed 表、已记忆记录、启动图 row 与已注册 factory;其他情况一律抛错。交给 factory 的同步 `require` 使用相同顺序,但不含异步图 row 加载,并把观察到的边记录到模块记录中。
14
63
 
15
- `dsh.client.external` 是统一基座之外的可选精确 specifier 请求列表。统一基座包括外壳播种的 React、Cordis 和静态 UI 库,以及由 HTML parser 预载的 runtime。请求由其命名的动态 package row 或精确静态表键回答;只有末尾 `/client` 会别名到 package row,并且不存在 provider 别名声明。纯类型 import 会被擦除,不产生请求。组合阶段会拒绝畸形请求、缺失提供方、自请求和同步请求环;import 与 prefetch 会在消费者物化前递归登记动态提供方。参见[共享模块与模块图](../AGENTS.md#shared-modules-and-the-module-graph)。
64
+ ### 增量组合
16
65
 
66
+ node 半侧逐包增量扫描——没有全量重扫路径。每次 `internal/plugin` 发出都会把该 fiber 的 entry 名标脏;一个微任务 flush 会把每个脏名与当前 loader 条目对账,激活 pass 播种同一脏集合并同步 flush,因此首次扫描与稳态共用同一实现。包元数据按 Loader specifier 与所属 tree base URL 缓存至重启。浏览器模块 id 是 Loader specifier;换 scope 再发布且去掉 scope 后的包名仍相同的清单,仍拥有该行。若不同的 active Loader source 解析到同一包名,组合会失败;移除冲突来源后,剩余来源无需重启 fiber 即可接替。bundle 内容变更只能通过 `rebuilt()`(HMR 钩子)进入图。
67
+
68
+ node 半侧会在发布前快照每个客户端 bundle 及其现有 source map。它把资源分组到 `/plugins/??...&rev=...` combo URL:modules row 使用一个 bootstrap combo,其余 row 使用一个或多个 application combo;每个阶段都会在 URL 超过 3 KiB 之前分区。每个 combo map 都是 Indexed Source Map v3,并在可用时使用作者提供的 section,否则为已打包 bundle 生成 identity section。初始逐插件 revision 使用进程 nonce,所以启动时不哈希每个插件;HMR 只哈希被报告为已变化的产物。已公告响应不可变;未知组合或 revision 返回 404。
69
+
70
+ ### 启动清单注入
71
+
72
+ 宿主 tap 索引渲染,并向 `<head>` 注入:`window.__ModuleLoader__` queue facade、每个 application combo 的提示性 preload、阻塞 parser 的 bootstrap combo 脚本,然后才是外壳读取前的启动图。facade 的 `create()` 物化 modules bundle、把构造委托给其 `createClientModuleSystem` 导出,并让同一 facade 进入 live registration 模式。
73
+
74
+ ### 源码地图
75
+
76
+ | 文件 | 职责 |
77
+ |---|---|
78
+ | [`src/index.ts`](src/index.ts) | node 半侧:`ClientModuleRegistry`、扫描、产物快照、combo 路由、索引 tap |
79
+ | [`src/client/index.ts`](src/client/index.ts) | 浏览器半侧:bootstrap 导出、`ctx.modules` 登记 |
80
+ | [`src/client/system.ts`](src/client/system.ts) | `ClientModuleSystem`:加载/物化/失效机制 |
81
+ | [`src/client/manifest.ts`](src/client/manifest.ts) | 协议类型与启动清单解析 |
82
+
83
+ </details>
84
+
85
+ -----
86
+
87
+ <a id="further-exploration"></a>
88
+ ## 进一步探索
89
+
90
+ 当模块约定不够用时阅读以下页面:子系统参考、启动插件树的外壳,以及图背后的客户端编写规则。
91
+
92
+ - [客户端模块子系统](../../../docs/subsystems/client-modules.zh.md)——web 插件表、`WebBootGraph` 协议与 bundle 路由。
93
+ - [Web 启动内核](../web/README.zh.md)——创建模块系统并启动插件树的外壳。
94
+ - [客户端 HMR 驱动器](../hmr/README.zh.md)——在重建 bundle 上驱动 `invalidate`/`prefetch` 的重载链路。
95
+ - [客户端编写规则](../AGENTS.md#shared-modules-and-the-module-graph)——共享模块基座与 `dsh.client.external` 语义。
96
+ - [客户端组地图](../README.zh.md)——本包所属的浏览器半侧。
97
+
98
+ -----
99
+
100
+ <a id="model-experience"></a>
17
101
  ## 模型体验
18
102
 
19
- 无。模块 loader 属于浏览器侧内核机制;这里没有任何内容进入模型请求。
103
+ 无。模块 loader 属于浏览器侧内核机制,不注册任何面向模型的内容。
20
104
 
21
105
  #### KV Cache 影响
22
106
 
23
107
  无;该包既不组装也不发送提供方请求。
24
108
 
25
- ## 已知限制与暂缓事项
109
+ ## 已知限制与延期工作
110
+
111
+ <a id="known-limitations-and-deferred-work"></a>
112
+
113
+
114
+ 这些限制说明模块系统不做什么。它们是当前包约束,不是任务积压。
115
+
116
+ - **有意采用扁平模块图**——每个 bundle 是一个模块节点,其边只指向表中的叶节点;接口(`loadCache`/`edges`/`invalidate`)已经支持通用模块图,因此可以改变 externalization 粒度而不更改接口。
117
+ - **自身不维护卸载记录**——样式移除与 fiber 拆卸顺序属于 HMR 驱动器(`@deepseek-ai/dsh-client-hmr`);loader 只在每条记录中登记其拥有的样式标签 id。
118
+ - **快照式提供会保留产物字节**——Host 在内存中保留每个 bundle、可选 source map、生成的单资源响应和当前启动 combo 响应;HMR 还会保留上一代启动响应。内存会随已组合客户端产物增长为数份副本,以换取不可变响应和一代竞态容忍。
119
+
120
+ <a id="dev-note"></a>
121
+ ### 开发备注
122
+
123
+ <details>
124
+ <summary>维护者的工作上下文——点击展开</summary>
125
+
126
+ 无。
26
127
 
27
- - **有意采用扁平模块图**:每个 bundle 是一个模块节点,其边只指向表中的叶节点;接口(`loadCache`/`edges`/`invalidate`)已经支持通用模块图,因此可以改变 externalization 粒度而不更改接口。
28
- - **自身不维护卸载记录**:样式移除与 fiber 拆卸顺序属于 HMR 驱动器(`@deepseek-ai/dsh-client-hmr`);loader 只在每条记录中登记其拥有的样式标签 id。
128
+ </details>
package/lib/client.js CHANGED
@@ -73,21 +73,26 @@ window.__ModuleLoader__.load({
73
73
  const graph = wire;
74
74
  if (typeof graph.rev !== "string") throw new Error("client-modules: boot manifest rev must be a string");
75
75
  if (!Array.isArray(graph.entries)) throw new Error("client-modules: boot manifest entries must be an array");
76
- const modules = [];
76
+ if (!Array.isArray(graph.batches)) throw new Error("client-modules: boot manifest batches must be an array");
77
+ const moduleFields = [];
77
78
  const plugins = [];
79
+ const seenEntryIds = /* @__PURE__ */ new Set();
78
80
  for (const value of graph.entries) {
79
81
  if (typeof value !== "object" || value === null) throw new Error("client-modules: boot manifest entry is not an object");
80
82
  const row = value;
81
83
  const where = typeof row.id === "string" ? `"${row.id}"` : JSON.stringify(row);
82
84
  if (typeof row.id !== "string" || typeof row.url !== "string" || typeof row.rev !== "string") throw new Error(`client-modules: boot manifest entry ${where} must carry string id/url/rev`);
85
+ if (seenEntryIds.has(row.id)) throw new Error(`client-modules: duplicate graph entry "${row.id}"`);
86
+ seenEntryIds.add(row.id);
83
87
  const subject = `boot manifest entry ${where}`;
84
88
  const inject = optionalStringArray(subject, "inject", row.inject);
85
89
  const external = optionalStringArray(subject, "external", row.external);
86
90
  if (row.immediately !== void 0 && typeof row.immediately !== "boolean") throw new Error(`client-modules: boot manifest entry ${where} immediately must be a boolean`);
87
- modules.push({
91
+ moduleFields.push({
88
92
  id: row.id,
89
93
  url: row.url,
90
94
  rev: row.rev,
95
+ inject: inject === void 0 ? [] : [...inject],
91
96
  external: external === void 0 ? [] : [...external]
92
97
  });
93
98
  plugins.push({
@@ -96,6 +101,33 @@ window.__ModuleLoader__.load({
96
101
  immediately: row.immediately === true
97
102
  });
98
103
  }
104
+ const entryIds = new Set(moduleFields.map((row) => row.id));
105
+ const initialUrls = /* @__PURE__ */ new Map();
106
+ const batchUrls = /* @__PURE__ */ new Set();
107
+ for (const value of graph.batches) {
108
+ if (typeof value !== "object" || value === null) throw new Error("client-modules: boot manifest batch is not an object");
109
+ const batch = value;
110
+ const phase = batch.phase;
111
+ if (phase !== "bootstrap" && phase !== "application") throw new Error(`client-modules: boot manifest batch phase must be "bootstrap" or "application", received ${JSON.stringify(phase)}`);
112
+ if (typeof batch.url !== "string" || typeof batch.rev !== "string") throw new Error(`client-modules: boot manifest ${phase} batch must carry string url/rev`);
113
+ if (batchUrls.has(batch.url)) throw new Error(`client-modules: boot manifest carries duplicate batch URL ${JSON.stringify(batch.url)}`);
114
+ batchUrls.add(batch.url);
115
+ const entries = optionalStringArray(`boot manifest ${phase} batch`, "entries", batch.entries);
116
+ if (entries === void 0 || entries.length === 0) throw new Error(`client-modules: boot manifest ${phase} batch entries must be a non-empty string array`);
117
+ for (const id of entries) {
118
+ if (!entryIds.has(id)) throw new Error(`client-modules: boot manifest ${phase} batch names unknown entry "${id}"`);
119
+ if (initialUrls.has(id)) throw new Error(`client-modules: boot manifest entry "${id}" belongs to more than one batch`);
120
+ initialUrls.set(id, batch.url);
121
+ }
122
+ }
123
+ const modules = moduleFields.map((row) => {
124
+ const initialUrl = initialUrls.get(row.id);
125
+ if (initialUrl === void 0) throw new Error(`client-modules: boot manifest entry "${row.id}" belongs to no initial-load batch`);
126
+ return {
127
+ ...row,
128
+ initialUrl
129
+ };
130
+ });
99
131
  return {
100
132
  rev: graph.rev,
101
133
  modules,
@@ -125,6 +157,11 @@ window.__ModuleLoader__.load({
125
157
  }, { once: true });
126
158
  document.head.append(el);
127
159
  });
160
+ /** Replace the rev query while preserving absolute, protocol-relative, or path-relative form. */
161
+ function atRevision(url, rev) {
162
+ if (!/[?&]rev=[^&#]*/.test(url)) throw new Error(`client-modules: bundle URL ${url} has no revision`);
163
+ return url.replace(/([?&]rev=)[^&#]*/, `$1${encodeURIComponent(rev)}`);
164
+ }
128
165
  /**
129
166
  * Claim and inventory the <style> tags a factory injected during
130
167
  * materialization: preset-emitted tags arrive pre-tagged with data-plugin;
@@ -151,8 +188,10 @@ window.__ModuleLoader__.load({
151
188
  seed;
152
189
  factories = /* @__PURE__ */ new Map();
153
190
  bootstrapIds = /* @__PURE__ */ new Set();
154
- /** In-flight prefetch (script load) per id; concurrent callers share it. */
191
+ /** In-flight script transport per URL; every row in one batch shares it. */
155
192
  pendingArrival = /* @__PURE__ */ new Map();
193
+ /** Single-resource combo URL selected by HMR after invalidating one row. */
194
+ reloadUrls = /* @__PURE__ */ new Map();
156
195
  /** Materialization re-entrancy guard: factory-form CJS cannot deliver partial exports, so a cycle is fatal. */
157
196
  materializing = /* @__PURE__ */ new Set();
158
197
  graphRows = /* @__PURE__ */ new Map();
@@ -194,28 +233,38 @@ window.__ModuleLoader__.load({
194
233
  }
195
234
  /** Load one graph row so its factory is registered (idempotent per in-flight arrival). */
196
235
  arrive(row) {
197
- const { id, url } = row;
198
- const pending = this.pendingArrival.get(id);
199
- if (pending !== void 0) return pending;
236
+ const { id } = row;
200
237
  if (this.loadCache.has(id) || this.factories.has(id)) return Promise.resolve();
201
- const task = this.loadBundle(url).then(() => {
238
+ const reloadUrl = this.reloadUrls.get(id);
239
+ const url = reloadUrl ?? row.initialUrl;
240
+ let transport = this.pendingArrival.get(url);
241
+ if (transport === void 0) {
242
+ transport = this.loadBundle(url).finally(() => {
243
+ this.pendingArrival.delete(url);
244
+ });
245
+ this.pendingArrival.set(url, transport);
246
+ }
247
+ return transport.then(() => {
202
248
  if (!this.factories.has(id)) throw new Error(`client-modules: bundle ${url} loaded without registering "${id}" via __ModuleLoader__.load`);
203
- }).finally(() => {
204
- this.pendingArrival.delete(id);
249
+ if (reloadUrl !== void 0 && this.reloadUrls.get(id) === reloadUrl) this.reloadUrls.delete(id);
205
250
  });
206
- this.pendingArrival.set(id, task);
207
- return task;
208
251
  }
209
- /** Register each unresolved dynamic request before registering its consumer. */
210
- async arriveGraphRow(row, open = []) {
252
+ /** Register each injected package and unresolved dynamic request before its consumer. */
253
+ async arriveGraphRow(row, open = [], visited = /* @__PURE__ */ new Set()) {
211
254
  const cycleStart = open.indexOf(row.id);
212
255
  if (cycleStart !== -1) throw new Error(`client-modules: module arrival cycle ${[...open.slice(cycleStart), row.id].join(" -> ")} (the host must reject this graph before serving it)`);
256
+ if (visited.has(row.id)) return;
257
+ visited.add(row.id);
213
258
  const next = [...open, row.id];
214
259
  for (const request of row.external) {
215
260
  const id = stripClientSuffix(request);
216
261
  if (this.seed.has(request) || this.loadCache.has(id)) continue;
217
262
  const dependency = this.graphRows.get(id);
218
- if (dependency !== void 0) await this.arriveGraphRow(dependency, next);
263
+ if (dependency !== void 0) await this.arriveGraphRow(dependency, next, visited);
264
+ }
265
+ for (const packageName of row.inject) {
266
+ const dependency = this.graphRows.get(packageName);
267
+ if (dependency !== void 0) await this.arriveGraphRow(dependency, [], visited);
219
268
  }
220
269
  await this.arrive(row);
221
270
  }
@@ -276,9 +325,12 @@ window.__ModuleLoader__.load({
276
325
  if (row === void 0) throw new Error(`client-modules: prefetch("${id}") — not a graph entry`);
277
326
  await this.arriveGraphRow(row);
278
327
  }
279
- invalidate(id) {
328
+ invalidate(id, rev) {
280
329
  const normalized = stripClientSuffix(id);
281
330
  if (this.bootstrapIds.has(normalized)) return;
331
+ const row = this.graphRows.get(normalized);
332
+ if (row !== void 0) this.reloadUrls.set(normalized, atRevision(row.url, rev ?? row.rev));
333
+ else this.reloadUrls.delete(normalized);
282
334
  this.factories.delete(normalized);
283
335
  this.loadCache.delete(normalized);
284
336
  }