فهرست منبع

docs(client-modules,client-hmr): align bootstrap and rebuild semantics

lsdsjy 1 ماه پیش
والد
کامیت
838006d960

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md
-2026-08-15-client-shells-and-dynamic-packages.md: 0bb89a23778c84598964bf408d1a66a16f062687
-2026-08-15-client-shells-and-dynamic-packages.zh.md: 4a0488b678368882a4724733ebea52cc6e9440a9
+2026-08-15-client-shells-and-dynamic-packages.md: 8713234735ddb53db27e17a2ea9087de665d86f7
+2026-08-15-client-shells-and-dynamic-packages.zh.md: ec8a2fb358e86f4fe74db13d555b8be8432ad62e

+ 8 - 8
.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md

@@ -10,7 +10,7 @@ English | [中文](2026-08-15-client-shells-and-dynamic-packages.zh.md)
 
 Client npm dependency sections describe installation and development relationships, but they do not reliably describe bundle contents. Treating `dependencies`, `peerDependencies`, or `devDependencies` as implicit bundler instructions can inline a shared React or workspace identity, or leave a built library carrying unresolved child imports without the host that is meant to assemble them.
 
-The browser application also contains distinct roles: the HTML/Vite compilation entry, the framework-free Cordis startup kernel, static assembly libraries, and Loader-governed plugins. Early execution from HTML is an arrival policy, not a package kind. Runtime and modules need to arrive before the Vite main module while retaining ordinary `lib/client.js` artifacts and dynamic graph rows.
+The browser application also contains distinct roles: the HTML/Vite compilation entry, the framework-free Cordis startup kernel, static assembly libraries, and Loader-governed plugins. Early execution from HTML is an arrival policy, not a package kind. Modules must arrive before the Vite main module while retaining its ordinary `lib/client.js` artifact and dynamic graph row.
 
 Shared UI libraries still expose synchronous TypeScript and React values to many consumers. Until those values move behind services or slots, making the libraries formal dynamic entries would preserve the value coupling while obscuring which module identity the shell must share.
 
@@ -24,7 +24,7 @@ Shared UI libraries still expose synchronous TypeScript and React values to many
 | Startup kernel | `packages/client/web` | Owns the plain-DOM boot page, module-system wiring, Cordis settlement, and renderer handoff | `staticLinked` `lib/index.js`; no `dsh.client` row |
 | Static assembly libraries | Cordis, `ui-primitives`, `ui-slots` | Supply shared module identities and direct value APIs | ESM `lib/index.js`, merged and chunked by Vite; not Loader entries |
 | Module bootstrap | `packages/client/modules` | Supplies the client module table and its Cordis wrapper | Dynamic package with one ordinary `lib/client.js`; the host delivers its factory early |
-| Dynamic client packages | runtime, `ui-renderer`, theme, and feature plugins | Participate through Cordis services, slots, and effects | Declare `dsh.client`, emit self-registering `lib/client.js`, and remain host-graph entries |
+| Dynamic client packages | connection, `ui-renderer`, theme, and feature plugins | Participate through Cordis services, slots, and effects | Declare `dsh.client`, emit self-registering `lib/client.js`, and remain host-graph entries |
 
 `packages/client/web` keeps Cordis as matching peer and development dependencies and uses modules and static UI packages as development compilation inputs. `apps/web` consumes built package exports rather than aliases into workspace source.
 
@@ -32,7 +32,7 @@ The `staticLinked` preset leaves every bare specifier as an external import in `
 
 ### Shared module requests
 
-Dynamic browser bundles implicitly externalize the common baseline: `PLATFORM_MODULES` names shell-seeded React, Cordis, and static UI identities, while `PRELOADED_CLIENT_EXTERNALS` names runtime's parser-preloaded dynamic identity. A package uses `dsh.client.external` only for an exact non-baseline value request. Type-only imports are erased and create no request; permitted third-party implementation libraries remain private bundle contents.
+Dynamic browser bundles implicitly externalize the common baseline: `PLATFORM_MODULES` names shell-seeded React, Cordis, and static UI identities, while `PRELOADED_CLIENT_EXTERNALS` is reserved for a dynamic identity that must arrive before shell boot and is currently empty. A package uses `dsh.client.external` only for an exact non-baseline value request. Type-only imports are erased and create no request; permitted third-party implementation libraries remain private bundle contents.
 
 A request has exactly two suppliers:
 
@@ -46,12 +46,12 @@ There is no general `dsh.client.provide` alias mechanism. Dynamic rows and stati
 The modules Node half injects the startup protocol into the served HTML in this order:
 
 1. Install `window.__ModuleLoader__` in queue mode with `pendingQueue`, `load()`, and `create()`.
-2. Start preloading the content-addressed application batch containing every row except modules and runtime.
-3. Execute one blocking bootstrap batch containing the ordinary modules and runtime factory registrations.
+2. Start preloading the content-addressed application batch containing every row except modules.
+3. Execute one blocking bootstrap batch containing the ordinary modules factory registration.
 4. Assign `window.__DSH_BOOT__`, including both batch descriptors and every row's individual HMR URL.
 5. Execute the Vite main module.
 
-The bootstrap batch only registers factories. The startup kernel passes the raw graph and shell seeds to `__ModuleLoader__.create()`. The facade removes the modules registration, materializes it with a `require` function that rejects every external, and invokes its `createClientModuleSystem` export. The modules bundle parses the graph, constructs `ClientModuleSystem`, caches its own exports as the modules row, and retains the system in a module closure. Construction switches the same facade to live mode before draining runtime's pending factory. The modules client face consequently has a zero-runtime-external bootstrap requirement.
+The bootstrap batch only registers the modules factory. The startup kernel passes the raw graph and shell seeds to `__ModuleLoader__.create()`. The facade removes the modules registration, materializes it with a `require` function that rejects every external, and invokes its `createClientModuleSystem` export. The modules bundle parses the graph, constructs `ClientModuleSystem`, caches its own exports as the modules row, retains the system in a module closure, and switches the same facade to live mode. The modules client face consequently has a zero-external bootstrap requirement.
 
 After the `immediately` tier has registered its factories, the kernel creates all Loader entries, awaits Cordis quiescence, and requires every fiber to be ACTIVE. It then calls `ctx.uiRenderer.mount(container)`. The dynamic `ui-renderer` package owns React, slot rendering, hydration of the existing boot DOM, and the React root lifecycle; the startup kernel and failure page remain React-free.
 
@@ -67,7 +67,7 @@ Ordinary installed libraries remain `dependencies`: a dynamic build may bundle a
 
 **Convert every client package into a dynamic plugin immediately.** `ui-primitives` and `ui-slots` still provide synchronous values without independent service or slot lifecycles; a manifest declaration alone would not remove those imports.
 
-**Generate a separate `client-static.js` for modules or runtime.** Both packages remain dynamic graph rows and Cordis plugins; only their factory arrival is early. A second artifact would encode host policy in a filename and create two runtime products from one source.
+**Generate a separate `client-static.js` for modules.** The package remains a dynamic graph row and Cordis plugin; only its factory arrival is early. A second artifact would encode host policy in a filename and create two runtime products from one source.
 
 **Compile all shared modules into the Vite entry.** This would remove deployment composition and plugin-level replacement from business plugins, including the renderer and theme.
 
@@ -79,7 +79,7 @@ Ordinary installed libraries remain `dependencies`: a dynamic build may bundle a
 
 Bundle contents stay stable when an npm dependency moves between peer and development sections, because each build face declares externality directly. Static libraries remain host-assembled, while dynamic packages retain uniform artifacts and lifecycle governance.
 
-The startup protocol depends on the modules and runtime package ids, and modules must remain self-contained at runtime. Batch generation preserves those two ordinary package artifacts and gives every other row one shared initial transport; HMR still uses each row's revisioned individual artifact. A missing bootstrap registration fails before Cordis starts; later plugin import, apply, and service-wait failures remain visible through the boot page's ACTIVE scan.
+The startup protocol depends on the modules package id, and modules must remain self-contained at runtime. Batch generation preserves its ordinary package artifact and gives every other row one shared initial transport; HMR still uses each row's revisioned individual artifact. A missing bootstrap registration fails before Cordis starts; later plugin import, apply, and service-wait failures remain visible through the boot page's ACTIVE scan.
 
 The shell consumes built `lib/` products, so source and browser artifacts can drift until the relevant build or watcher runs. Typechecking source alone does not prove the served application uses the same code.
 

+ 8 - 8
.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md

@@ -10,7 +10,7 @@ Status: implemented
 
 Client npm 依赖区段描述安装和开发关系,但不能可靠描述 bundle 内容。把 `dependencies`、`peerDependencies` 或 `devDependencies` 当作隐式 bundler 指令,可能内联本应共享的 React 或 workspace 身份,也可能让构建后的库携带未解析子 import,却没有交给预期的宿主组装。
 
-浏览器应用还包含不同角色:HTML/Vite 编译入口、不依赖框架的 Cordis 启动内核、静态装配库,以及由 Loader 治理的插件。HTML 提前执行属于到达策略,不定义包类别。Runtime 和 modules 需要先于 Vite 主模块到达,同时继续使用普通 `lib/client.js` 产物和动态图 row。
+浏览器应用还包含不同角色:HTML/Vite 编译入口、不依赖框架的 Cordis 启动内核、静态装配库,以及由 Loader 治理的插件。HTML 提前执行属于到达策略,不定义包类别。Modules 必须先于 Vite 主模块到达,同时继续使用普通 `lib/client.js` 产物和动态图 row。
 
 共享 UI 库仍向大量消费者暴露同步 TypeScript 与 React 实体。在这些实体进入 service 或 slot 前,形式上把库改为动态 entry 只会保留实体耦合,并模糊外壳必须共享的模块身份。
 
@@ -24,7 +24,7 @@ Client npm 依赖区段描述安装和开发关系,但不能可靠描述 bundl
 | 启动内核 | `packages/client/web` | 拥有纯 DOM 启动页、模块系统接线、Cordis settle 和 renderer handoff | `staticLinked` `lib/index.js`;无 `dsh.client` row |
 | 静态装配库 | Cordis、`ui-primitives`、`ui-slots` | 提供共享模块身份和直接实体 API | ESM `lib/index.js`,由 Vite 合并拆分;不是 Loader entry |
 | 模块自举包 | `packages/client/modules` | 提供 client 模块表及其 Cordis wrapper | 带一个普通 `lib/client.js` 的动态包;host 提前送达其 factory |
-| 动态 client 包 | runtime、`ui-renderer`、主题和功能插件 | 通过 Cordis service、slot 和 effect 参与应用 | 声明 `dsh.client`,产出自注册 `lib/client.js`,并保留 host graph entry |
+| 动态 client 包 | connection、`ui-renderer`、主题和功能插件 | 通过 Cordis service、slot 和 effect 参与应用 | 声明 `dsh.client`,产出自注册 `lib/client.js`,并保留 host graph entry |
 
 `packages/client/web` 把 Cordis 保持为 matching peer 与开发依赖,并把 modules 和静态 UI 包作为开发期编译输入。`apps/web` 消费已构建 package export,不通过 alias 读取 workspace 源码。
 
@@ -32,7 +32,7 @@ Client npm 依赖区段描述安装和开发关系,但不能可靠描述 bundl
 
 ### 共享模块请求
 
-动态浏览器 bundle 会隐式 external 统一基座:`PLATFORM_MODULES` 命名由外壳播种的 React、Cordis 和静态 UI 身份,`PRELOADED_CLIENT_EXTERNALS` 命名由 HTML parser 预载的 runtime 动态身份。包只在精确请求基座外实体时使用 `dsh.client.external`。纯类型 import 会被擦除,不产生请求;允许的第三方实现库保留为 bundle 私有内容。
+动态浏览器 bundle 会隐式 external 统一基座:`PLATFORM_MODULES` 命名由外壳播种的 React、Cordis 和静态 UI 身份,`PRELOADED_CLIENT_EXTERNALS` 则为必须先于 shell 启动到达的动态身份预留,当前为空。包只在精确请求基座外实体时使用 `dsh.client.external`。纯类型 import 会被擦除,不产生请求;允许的第三方实现库保留为 bundle 私有内容。
 
 请求只有两种提供方:
 
@@ -46,12 +46,12 @@ Client npm 依赖区段描述安装和开发关系,但不能可靠描述 bundl
 Modules Node 半按以下顺序向实际返回的 HTML 注入启动协议:
 
 1. 以 queue 模式安装 `window.__ModuleLoader__`,包含 `pendingQueue`、`load()` 与 `create()`。
-2. 开始预加载按内容寻址的 application 批次,其中包含 modules 与 runtime 之外的全部 row。
-3. 执行一个阻塞式 bootstrap 批次,其中包含普通的 modules 与 runtime factory registration。
+2. 开始预加载按内容寻址的 application 批次,其中包含 modules 之外的全部 row。
+3. 执行一个阻塞式 bootstrap 批次,其中包含普通的 modules factory registration。
 4. 赋值 `window.__DSH_BOOT__`,其中包含两个批次描述及每个 row 的独立 HMR URL。
 5. 执行 Vite 主模块。
 
-Bootstrap 批次只登记 factory。启动内核把原始图与外壳 seed 传给 `__ModuleLoader__.create()`。Facade 移除 modules registration,用拒绝全部 external 的 `require` 函数将其物化,再调用其 `createClientModuleSystem` 导出。Modules bundle 解析图、构造 `ClientModuleSystem`、把自身 exports 缓存为 modules row,并在模块闭包中保留该系统。构造过程先把同一 facade 切换到 live 模式,再排空 runtime 的 pending factory。因此 modules client face 必须满足零 runtime external 的自举要求。
+Bootstrap 批次只登记 modules factory。启动内核把原始图与外壳 seed 传给 `__ModuleLoader__.create()`。Facade 移除 modules registration,用拒绝全部 external 的 `require` 函数将其物化,再调用其 `createClientModuleSystem` 导出。Modules bundle 解析图、构造 `ClientModuleSystem`、把自身 exports 缓存为 modules row、在模块闭包中保留该系统,并把同一 facade 切换到 live 模式。因此 modules client face 必须满足零 external 的自举要求。
 
 `immediately` 层级完成 factory 注册后,内核创建全部 Loader entry,等待 Cordis 静止,并要求每个 fiber 都进入 ACTIVE。随后调用 `ctx.uiRenderer.mount(container)`。动态 `ui-renderer` 包拥有 React、slot 渲染、已有启动 DOM 的 hydrate 和 React root 生命周期;启动内核与失败页保持 React-free。
 
@@ -67,7 +67,7 @@ Bootstrap 批次只登记 factory。启动内核把原始图与外壳 seed 传
 
 **立即把所有 client 包改为动态插件。** `ui-primitives` 与 `ui-slots` 仍提供同步实体,且没有独立 service 或 slot 生命周期;只加 manifest 声明不会移除这些 import。
 
-**为 modules 或 runtime 生成单独的 `client-static.js`。** 两个包仍是动态图 row 和 Cordis 插件,只有 factory 提前到达。第二份产物会把宿主策略编码进文件名,并让同一源码产生两个运行期产品。
+**为 modules 生成单独的 `client-static.js`。** 该包仍是动态图 row 和 Cordis 插件,只有 factory 提前到达。第二份产物会把宿主策略编码进文件名,并让同一源码产生两个运行期产品。
 
 **把全部共享模块编进 Vite entry。** 这会让业务插件失去部署组合与插件级替换能力,包括 renderer 和主题。
 
@@ -79,7 +79,7 @@ Bootstrap 批次只登记 factory。启动内核把原始图与外壳 seed 传
 
 Npm 依赖在 peer 与开发区段间移动时,bundle 内容保持稳定,因为每个构建 face 都直接声明 external。静态库继续由宿主装配,动态包则保留统一产物与生命周期治理。
 
-启动协议依赖 modules 和 runtime 的 package id,modules 还必须保持运行期自包含。批次生成保留这两个普通 package 产物,并为其他全部 row 提供一条共享初始传输;HMR 仍使用每个 row 带 revision 的独立产物。缺少 bootstrap registration 会在 Cordis 启动前失败;后续插件 import、apply 与 service 等待失败仍由启动页的 ACTIVE 扫描呈现。
+启动协议依赖 modules 的 package id,modules 还必须保持运行期自包含。批次生成保留其普通 package 产物,并为其他全部 row 提供一条共享初始传输;HMR 仍使用每个 row 带 revision 的独立产物。缺少 bootstrap registration 会在 Cordis 启动前失败;后续插件 import、apply 与 service 等待失败仍由启动页的 ACTIVE 扫描呈现。
 
 外壳消费已构建 `lib/` 产品,因此在相关 build 或 watcher 运行前,源码与浏览器产物可能漂移。仅源码 typecheck 通过不能证明实际服务的应用使用同一份代码。
 

+ 2 - 2
packages/client/hmr/src/index.ts

@@ -82,8 +82,8 @@ export function apply(ctx: Context, config: Config): void {
 
   const rehash = (id: string, watch: WatchedBundle, current: WatchedArtifactStat): void => {
     try {
-      // rebuilt() re-hashes; an unchanged hash stays silent (clientModuleHost
-      // fires onRebuilt only on a real rev change).
+      // rebuilt() replaces the opaque startup rev on its first call; later
+      // calls stay silent when the content hash is unchanged.
       ctx.clientModules.rebuilt(id)
     } catch (error) {
       const code = (error as NodeJS.ErrnoException).code