@deepseek-ai/dsh-client-hmr 0.1.5-rc.2 → 0.1.6-alpha.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.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/hmr/README.md
5
- README.md: de420ef2c809ccc13feea320ae3ffda3720f82a3
6
- README.zh.md: c7066e369b38fa3ffda6888831bba7780c5da8af
5
+ README.md: 26bc640c6cdd213fbc78de920e12444589901990
6
+ README.zh.md: 72d941f678e7e29a56bca678a79c4740667e7e6b
package/README.md CHANGED
@@ -63,11 +63,11 @@ The chain is two halves with one contract: the node half owns bundle detection a
63
63
 
64
64
  ### The browser swap
65
65
 
66
- On a `rebuilt` frame the revision makes `invalidate` select that plugin's immutable one-resource combo URL instead of its initial multi-resource URL. `prefetch` loads and registers the new factory while the old fiber still serves. The remaining order is registry-first teardown (`registry.delete` before the fiber's disposer emits `internal/plugin`, or the vendored Loader flags the entry disabled), drain the old fiber's unload, delete `entry.fiber`, remove owned `<style data-plugin>` tags, then `entry.refresh()` re-imports and remounts, and `fiber.await()` rethrows startup failures loud. The swap is safe because execution is pure registration under the lazy-CJS model: every module side effect lives in the factory closure and runs at materialization.
66
+ On a `rebuilt` frame the revision makes `invalidate` select that plugin's immutable one-resource combo URL instead of its initial multi-resource URL. `prefetch` loads and registers the new factory while the old fiber still serves. The remaining order is registry-first teardown (`registry.delete` before the fiber's disposer emits `internal/plugin`, or the vendored Loader flags the entry disabled), drain the old fiber's unload, delete `entry.fiber`, remove owned `<style data-plugin>` tags, then `entry.refresh()` re-imports and remounts, and `fiber.await()` rethrows startup failures loudly. The swap is safe because execution is pure registration under the lazy-CJS model: every module side effect lives in the factory closure and runs at materialization.
67
67
 
68
68
  ### Cascade and self-reload
69
69
 
70
- A fiber's activation epoch strings its service providers' uids, so replacing a provider's fiber re-cascades every dependent through cordis itself with zero HMR-side bookkeeping. This plugin is itself a graph entry, so a rebuilt frame may name it; the in-flight reload keeps running in the old bundle's closure and the new bundle's apply opens a fresh channel.
70
+ A fiber's activation epoch strings its service providers' uids, so replacing a provider's fiber re-cascades every dependent through Cordis itself with zero HMR-side bookkeeping. This plugin is itself a graph entry, so a rebuilt frame may name it; the in-flight reload keeps running in the old bundle's closure and the new bundle's apply opens a fresh channel.
71
71
 
72
72
  ### Failure policy
73
73
 
package/README.zh.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "面向开发者的浏览器客户端插件热重载说明:重建插件 bundle 后原地替换运行中的插件,用于迭代 web GUI。"
2
+ description: "仅用于开发环境的浏览器客户端插件热重载:重建插件 bundle 后原地替换运行中的插件,供开发者迭代 web GUI。"
3
3
  kind: "package-reference"
4
4
  ---
5
5
 
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- `dsh-client-hmr` 会在浏览器客户端插件的 bundle 重建后原地重载该插件,让编辑插件源码的开发者无需整页刷新即可看到变更。如果没有重建 watcher,整条链路保持空闲:只有 `pnpm run dev:web` 之类的进程重写客户端 bundle 时才会产生它所响应的重建。每次重载只替换一个插件并携带全新组件状态,而数据层(connection、runtime 与 Session 对象)保持不变。这里的一切都是浏览器侧的开发机制;模型永远看不到它。
12
+ `dsh-client-hmr` 会在浏览器客户端插件的 bundle 重建后原地重载该插件,让编辑插件源码的开发者无需整页刷新即可看到变更。如果没有重建 watcher,整条链路保持空闲:只有 `pnpm run dev:web` 之类的进程重写客户端 bundle 时才会产生它所响应的重建。每次重载只替换一个插件并携带全新组件状态,而数据层(连接、运行时与 Session 对象)保持不变。这里的一切都是浏览器侧的开发机制;模型永远看不到它。
13
13
 
14
14
  ## 目录
15
15
 
@@ -33,7 +33,7 @@ kind: "package-reference"
33
33
 
34
34
  ### 一次重载做什么
35
35
 
36
- 每次重载都会重新执行插件 bundle,并用全新状态重新挂载插件。依赖被重载插件的插件会随之自动重载。失败的重载会被明确报告,并在下一次重建时从头重试。
36
+ 每次重载都会重新执行插件 bundle,并用全新状态重新挂载插件。依赖被重载插件的插件会随之自动重载。失败的重载会以可见方式报告,并在下一次重建时从头重试。
37
37
 
38
38
  ### 配置
39
39
 
@@ -41,7 +41,7 @@ kind: "package-reference"
41
41
  |---|---|---|
42
42
  | `pollIntervalMs` | `500` | bundle stat 轮询间隔,单位为毫秒 |
43
43
 
44
- 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-client-hmr)是每个受支持字段及其 JSDoc 的穷尽式真源。
44
+ 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-client-hmr)是所有受支持字段及其 JSDoc 的完整真源。
45
45
 
46
46
  ### 观察成功
47
47
 
@@ -59,19 +59,19 @@ kind: "package-reference"
59
59
 
60
60
  ### 设计理念
61
61
 
62
- 链路分为两半,共用一份约定:node 半侧负责 bundle 检测与通知,浏览器半侧负责替换。node 半侧运行一个 interval,从 module host 读取文件前的基线开始 stat 轮询每个图 bundle。未变化的启动 row 无需读取内容或求 hash 即可开始监视;发生变化的 row,或产物恢复后的 dirty row,会进入 `rebuilt()`,且只广播真实 revision 变更。`rebuilt()` 会把当前 source map 与已变化的 bundle 一起读取;仅写入 map 不会重载可执行代码。node 半侧还提供 `/plugins/events`,一个广播 `graph` 与 `rebuilt` 帧的 SSE 通道。
62
+ 链路分为两半,共用一份约定:node 半侧负责 bundle 检测与通知,浏览器半侧负责替换。node 半侧运行一个 interval,从 module host 读取文件前的基线开始 stat 轮询每个图 bundle。未变化的启动 row 无需读取内容或求 hash 即可开始监视;发生变化的 row,或产物恢复后的 dirty row,会进入 `rebuilt()`,且只广播真实 revision 变更。`rebuilt()` 会把当前 source map 与已变化的 bundle 一起读取;仅写入 map 不会重载可执行代码。node 半侧还提供 `/plugins/events`,一个广播 `graph` 与 `rebuilt` 帧的 SSE(Server-Sent Events)通道。
63
63
 
64
64
  ### 浏览器侧替换
65
65
 
66
- 收到 `rebuilt` 帧后,帧内 revision 会让 `invalidate` 选择该插件不可变的单资源 combo URL,而不是初始多资源 URL。`prefetch` 在旧 fiber 仍在服务时加载并注册新 factory。其余顺序是:先注册表后拆卸(在 fiber 的 disposer 发出 `internal/plugin` 之前执行 `registry.delete`,否则 vendored Loader 会把该 entry 标为禁用)、排空旧 fiber 的卸载、删除 `entry.fiber`、移除自身拥有的 `<style data-plugin>` 标签,然后 `entry.refresh()` 重新导入并挂载,`fiber.await()` 直接把启动失败重新抛出。替换之所以安全,是因为在惰性 CJS 模型下执行只是注册:每个模块副作用都位于 factory 闭包中,在物化时运行。
66
+ 收到 `rebuilt` 帧后,帧内 revision 会让 `invalidate` 选择该插件不可变的单资源 combo URL,而不是初始多资源 URL。`prefetch` 在旧 fiber 仍在服务时加载并注册新 factory。其余顺序是:先从注册表删除,再拆卸(在 fiber 的 disposer 发出 `internal/plugin` 之前执行 `registry.delete`,否则 vendored Loader 会把该 entry 标为禁用)、等待旧 fiber 卸载完成、删除 `entry.fiber`、移除自身拥有的 `<style data-plugin>` 标签,然后 `entry.refresh()` 重新导入并挂载,`fiber.await()` 直接把启动失败重新抛出。替换之所以安全,是因为在惰性 CJS 模型下执行只是注册:每个模块副作用都位于 factory 闭包中,在物化时运行。
67
67
 
68
68
  ### 级联与自重载
69
69
 
70
- fiber 的激活 epoch 会串联其服务提供方的 uid,因此替换提供方 fiber 会通过 cordis 自身零 HMR 簿记地级联所有依赖方。本插件本身也是一个图 entry,因此 `rebuilt` 帧可能点名它;进行中的重载在旧 bundle 的闭包中继续运行,新 bundle 的 apply 会打开全新通道。
70
+ fiber 的激活 epoch 会串联其服务提供方的 uid,因此替换提供方 fiber 会通过 Cordis 自身级联重载所有依赖方,无需 HMR(热模块替换)侧维护任何簿记信息。本插件本身也是一个图 entry,因此 `rebuilt` 帧可能点名它;进行中的重载在旧 bundle 的闭包中继续运行,新 bundle 的 apply 会打开全新通道。
71
71
 
72
72
  ### 失败策略
73
73
 
74
- 不回滚:导入失败会让 entry 失去 fiber(下一个 `rebuilt` 帧从头重试),apply 失败则会在外壳的状态投影中留下 FAILED fiber。两者都会大声记录日志。
74
+ 不回滚:导入失败会让 entry 失去 fiber(下一个 `rebuilt` 帧从头重试),apply 失败则会在外壳的状态投影中留下 FAILED fiber。两者都会输出醒目的错误日志。
75
75
 
76
76
  ### 源码地图
77
77
 
@@ -113,7 +113,7 @@ fiber 的激活 epoch 会串联其服务提供方的 uid,因此替换提供方
113
113
 
114
114
  这些限制说明重载驱动器不会保留或恢复什么。它们是当前包约束,不是任务积压。
115
115
 
116
- - **重载有意保持粗粒度**——全新 fiber 与全新组件;被重载插件内的 React 状态会丢失,而数据层(connection/runtime fiber、Session 对象)不受影响。react-refresh 级状态保留与重新执行 bundle 冲突,因此有意排除。
116
+ - **重载有意保持粗粒度**——全新 fiber 与全新组件;被重载插件内的 React 状态会丢失,而数据层(连接 fiber、运行时 fiber、Session 对象)不受影响。react-refresh 级状态保留与重新执行 bundle 冲突,因此有意排除。
117
117
  - **失败时不回滚**——失败的重载会让该 entry 保持 FAILED 并在 loader 状态投影中可见;系统不会自动恢复先前 bundle。
118
118
  - **重建帧不会替换启动图**——每个帧都携带单资源 combo 重载所需的插件产物 revision;页面重载时才接收重新组合的启动图。
119
119
 
package/lib/client.js CHANGED
@@ -46,6 +46,23 @@ window.__ModuleLoader__.load({
46
46
  const name = "client-hmr";
47
47
  /** Required services: the vendored Loader (entry governance) and the client module system (boot provide, service name `modules`). */
48
48
  const inject = ["loader", "modules"];
49
+ /**
50
+ * Registry-first teardown of an entry's running fiber so `entry.refresh()`
51
+ * rebuilds it (see the module comment): delete the runtime record before the
52
+ * fiber's disposer emits `internal/plugin` (or the Loader flags the entry
53
+ * disabled), drain the unload so effect disposers finish before a new apply
54
+ * re-registers, then clear `entry.fiber` so `refresh()` re-imports instead of
55
+ * no-oping. A fiberless entry is left untouched.
56
+ * @param entry - the Loader entry to tear down.
57
+ */
58
+ async function tearDownEntryFiber(entry) {
59
+ const oldFiber = entry.fiber;
60
+ if (oldFiber === void 0) return;
61
+ const runtime = oldFiber.runtime;
62
+ if (runtime !== null) entry.ctx.registry.delete(runtime.callback);
63
+ while (oldFiber.inertia !== void 0) await oldFiber.inertia;
64
+ delete entry.fiber;
65
+ }
49
66
  /** Find the loader entry whose module specifier is `id` (entry tree ids are random; the package name lives in `options.name`). */
50
67
  function findEntry(loader, id) {
51
68
  for (const entry of loader.entries()) if (entry.options.name === id) return entry;
@@ -70,13 +87,7 @@ window.__ModuleLoader__.load({
70
87
  }
71
88
  modLoader.invalidate(id, rev);
72
89
  await modLoader.prefetch(id);
73
- const oldFiber = entry.fiber;
74
- if (oldFiber !== void 0) {
75
- const runtime = oldFiber.runtime;
76
- if (runtime !== null) entry.ctx.registry.delete(runtime.callback);
77
- while (oldFiber.inertia !== void 0) await oldFiber.inertia;
78
- delete entry.fiber;
79
- }
90
+ await tearDownEntryFiber(entry);
80
91
  removeOwnedStyles(id);
81
92
  await entry.refresh();
82
93
  await entry.fiber?.await();
@@ -118,6 +129,7 @@ window.__ModuleLoader__.load({
118
129
  exports.apply = apply;
119
130
  exports.inject = inject;
120
131
  exports.name = name;
132
+ exports.tearDownEntryFiber = tearDownEntryFiber;
121
133
  return module.exports;
122
134
  }
123
135
  });
package/lib/index.js CHANGED
@@ -9,7 +9,7 @@ const EVENTS_ENDPOINT = "/plugins/events";
9
9
  * HMR plugin, node half: the host end of the dev reload chain. One interval
10
10
  * stat-polls every graph row's client bundle (polling by design: network mounts
11
11
  * deliver no inotify events), reports changes through
12
- * `clientModuleHost.rebuilt(id)`, and serves the `/plugins/events` SSE channel
12
+ * `clientModules.rebuilt(id)`, and serves the `/plugins/events` SSE channel
13
13
  * broadcasting graph/rebuilt frames to the browser half (src/client/).
14
14
  * The web bundle mounts this row unconditionally: without a rebuild
15
15
  * watcher rewriting client bundles, the poll observes no changes and the
@@ -38,7 +38,7 @@ function sameBundleStat(left, right) {
38
38
  }
39
39
  /**
40
40
  * Mount the dev chain: bundle watches, rebuilt reporting, and the SSE channel.
41
- * @param ctx - host plugin context carrying clientModuleHost and webServer.
41
+ * @param ctx - host plugin context carrying clientModules and webServer.
42
42
  * @param config - validated {@link Config}.
43
43
  */
44
44
  function apply(ctx, config) {
@@ -62,12 +62,23 @@
62
62
  * leaves a FAILED fiber for the shell's status projection. Both log loudly.
63
63
  */
64
64
  import type { Context } from '@deepseek-ai/cordis';
65
+ import type { Entry } from '@deepseek-ai/cordis-plugin-loader';
65
66
  export type { PluginsEventFrame } from '../events.ts';
66
67
  export { EVENTS_ENDPOINT } from '../events.ts';
67
68
  /** Cordis plugin name. */
68
69
  export declare const name = "client-hmr";
69
70
  /** Required services: the vendored Loader (entry governance) and the client module system (boot provide, service name `modules`). */
70
71
  export declare const inject: string[];
72
+ /**
73
+ * Registry-first teardown of an entry's running fiber so `entry.refresh()`
74
+ * rebuilds it (see the module comment): delete the runtime record before the
75
+ * fiber's disposer emits `internal/plugin` (or the Loader flags the entry
76
+ * disabled), drain the unload so effect disposers finish before a new apply
77
+ * re-registers, then clear `entry.fiber` so `refresh()` re-imports instead of
78
+ * no-oping. A fiberless entry is left untouched.
79
+ * @param entry - the Loader entry to tear down.
80
+ */
81
+ export declare function tearDownEntryFiber(entry: Entry): Promise<void>;
71
82
  /**
72
83
  * Mount the HMR driver: subscribe to the system SSE channel and hot-swap
73
84
  * rebuilt entries.
@@ -14,7 +14,7 @@ export interface Config {
14
14
  export declare const Config: z<Config>;
15
15
  /**
16
16
  * Mount the dev chain: bundle watches, rebuilt reporting, and the SSE channel.
17
- * @param ctx - host plugin context carrying clientModuleHost and webServer.
17
+ * @param ctx - host plugin context carrying clientModules and webServer.
18
18
  * @param config - validated {@link Config}.
19
19
  */
20
20
  export declare function apply(ctx: Context, config: Config): void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-hmr",
3
3
  "description": "Dev-only hot-reload driver for script-loaded client entries: SSE rebuilt frames → invalidate/prefetch → fiber swap through the vendored Loader entry",
4
- "version": "0.1.5-rc.2",
4
+ "version": "0.1.6-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -31,7 +31,9 @@
31
31
  },
32
32
  "dsh": {
33
33
  "client": {
34
- "inject": [],
34
+ "inject": [
35
+ "@deepseek-ai/dsh-client-modules"
36
+ ],
35
37
  "platform": "web",
36
38
  "immediately": true
37
39
  }
@@ -45,9 +47,9 @@
45
47
  },
46
48
  "devDependencies": {
47
49
  "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
48
- "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
49
- "@deepseek-ai/dsh-client-modules": "^0.1.5-rc.2",
50
- "@deepseek-ai/dsh-invariants": "^0.1.5-rc.2",
50
+ "@deepseek-ai/dsh-client-modules": "^0.1.6-alpha.1",
51
+ "@deepseek-ai/dsh-host-webserver": "^0.1.6-alpha.1",
52
+ "@deepseek-ai/dsh-invariants": "^0.1.6-alpha.1",
51
53
  "@deepseek-ai/cordis": "^4.0.2"
52
54
  },
53
55
  "files": [