@deepseek-ai/dsh-hmr 0.2.0-rc.2 → 0.2.1-alpha.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
@@ -21,8 +21,8 @@
21
21
  en: 45ee194ad781d603
22
22
  zh: b3d7fcd8ed73717c
23
23
  /deepseek-ai-dsh-hmr/understand-the-implementation:
24
- en: 5c7ff789d5d6b653
25
- zh: 46ef96a031ac87bd
24
+ en: 227c3c99c3c4b597
25
+ zh: 853b656340cb8bce
26
26
  /deepseek-ai-dsh-hmr/model-experience:
27
27
  en: 215e7ba838619b7b
28
28
  zh: a311da3843709f90
@@ -39,8 +39,8 @@
39
39
  en: 89e0624fafd701fb
40
40
  zh: 19d36a33c1d9af9d
41
41
  /deepseek-ai-dsh-hmr/known-limitations-and-deferred-work:
42
- en: 379bc2c72fd9e53e
43
- zh: 1fff1e0e676bb161
42
+ en: 0f46d4df1dab7f3b
43
+ zh: d172835159a20cce
44
44
  /deepseek-ai-dsh-hmr/known-limitations-and-deferred-work/dev-note:
45
45
  en: 7972e4b2005ec9a4
46
46
  zh: 754c49ec538c164c
package/README.md CHANGED
@@ -56,7 +56,15 @@ Chokidar options, including polling, retain their existing meaning. Exact config
56
56
 
57
57
  `watchConfig()` registers an awaited configuration handler. `runExclusive()` serializes configuration changes and Loader updates with automatic reloads and rejects nested transactions. Package installation and removal run outside this queue. HMR does not acquire the package writer lock; manifest notifications reload only when the ordered `dsh.profile.bundles` list changes. Profile and home patch changes also trigger recomposition. File events received during a configuration transaction are processed afterward. Include refreshes and profile reconciliation reach plugins through ordinary Loader entry updates; Loader commits volatile-only changes in place.
58
58
 
59
- App-boot owns profile parsing and patch precedence. HMR reads the launcher’s data-only `profileContext`, registers the profile manifest and both user patch watches during initialization, and waits for application readiness before processing changes. Its disposal closes the watchers and cancels reloads waiting for startup. HMR also owns module-cache replacement and reload scheduling. Configuration watchers start outside the active transaction context so later notifications can enter the queue. No invariant companion is published because the queue and watcher registrations have no independent persisted projection.
59
+ App-boot owns profile parsing and patch precedence. HMR reads the launcher’s data-only `profileContext`, registers the profile manifest and both user patch watches during initialization, and waits for application readiness before processing changes. Its disposal closes the watchers and cancels reloads waiting for startup. Configuration watchers start outside the active transaction context so later notifications can enter the queue.
60
+
61
+ A changed `package.json` outside `node_modules` expires that package's cached configuration. A configuration-only manifest triggers no module reload; a manifest imported as a JSON module retains dependency-driven reloads, and one in the host dependency graph retains host reloads. Dedicated configuration watches keep their existing ownership. A new package entry takes effect when its Loader entry restarts.
62
+
63
+ Loader entries retain their raw import results. HMR retains every distinct imported namespace for each entry name and configuration-tree base URL; an uninitialized entry does not overwrite another entry's namespace. HMR matches those objects to cached ModuleJob namespaces instead of resolving entry names again. Names without an imported namespace and `cordis:` builtins keep name-based resolution. Source reloads therefore use the loaded modules after `exports` or `main` changes.
64
+
65
+ Entries sharing a runtime are replaced together, with each entry receiving the implementation for its own module. Duplicate entries for one module retain their separate plugin instances. On replacement failure, HMR removes partially activated replacements and restores the previous modules and plugin implementations. A successful reload updates every matching entry record, including disabled entries; a failed replacement leaves those records unchanged. Entries that have never loaded remain uninitialized.
66
+
67
+ [package-manifest.ts](src/package-manifest.ts) owns the Node internal interfaces for package reads, scope, type, and nearest-manifest lookups. Each HMR instance owns its configuration cache and restores its installed hooks on disposal. Manifest invalidation clears the ESM `ResolveCache` and CommonJS `_pathCache`: neither records every consulted manifest, and entries may point outside their package directory. CommonJS loads resolve requests with Node's default resolver before entering the native loader, bypassing old request aliases while preserving evaluated modules. Synchronous resolve hooks receive the resulting filenames. Ordinary module replacement remains HMR's separate operation.
60
68
 
61
69
  Watched module paths use Node ESM resolution's `realpathSync()` spelling, including Windows short directory names, so file events match the module cache.
62
70
 
@@ -86,7 +94,11 @@ Reloading a contributing plugin can change later request prefixes; HMR does not
86
94
  <a id="known-limitations-and-deferred-work"></a>
87
95
 
88
96
  - Module replacement requires Node loader internals. Framework dependency changes call the host-provided `loader.exit()` hook; HMR itself does not restart the process.
89
- - Replacing installed package versions still requires a restart through Plugin Manager. The browser Client module graph retains its separate browser-side loading mechanism.
97
+ - Replacing installed package versions still requires a restart through Plugin Manager; manifests below `node_modules` keep Node's cached configuration. The browser Client module graph retains its separate browser-side loading mechanism.
98
+ - TSX loaders and ordinary application-created Workers are outside the supported package-cache refresh scope.
99
+ - CommonJS requests must be resolvable by Node's default resolver. Synchronous hooks may postprocess the resulting filenames, as Desktop's Office resolver does; hooks requiring the original request name or introducing virtual requests are unsupported. Builtins retain Node's original loading behavior.
100
+ - Future work: retain package invalidation across HMR disposal and replacement. Restoring the native readers can expose their older cached configuration again.
101
+ - If a shared runtime's replacement modules have different plugin callbacks, an instance without a Loader entry cannot be assigned to one of them. HMR reports an ambiguity error and rolls back the reload.
90
102
  - `watchConfig()` resolves when Chokidar reports readiness. On darwin, libuv starts the FSEvents stream afterwards on its own thread, so a write that lands within milliseconds of registration is not reported until the next event in that directory; edits made after startup are unaffected.
91
103
 
92
104
  ### Dev Note
package/README.zh.md CHANGED
@@ -56,7 +56,15 @@ Chokidar 选项(包括轮询)保持原有含义。精确配置监听同时
56
56
 
57
57
  `watchConfig()` 注册会被等待的配置处理器。`runExclusive()` 将配置变更、Loader 更新与自动重载串行化,并拒绝嵌套事务。包安装和删除在该队列之外执行。HMR 不获取包操作写锁;manifest 通知仅在有序的 `dsh.profile.bundles` 列表变化时触发重载。profile 与 home patch 变化也会触发重新组合。配置事务期间收到的文件事件在事务结束后处理。 Include 刷新和 profile 重载都通过普通的 Loader 条目更新到达插件;仅 volatile 变化由 Loader 就地提交。
58
58
 
59
- App-boot 负责 profile 解析和 patch 优先级规则。HMR 读取启动器提供的纯数据 `profileContext`,在初始化时注册 profile manifest 和两份用户 patch 的监听,并等待应用就绪后处理更改。销毁 HMR 时会关闭监听器并取消等待启动的重载。HMR 也负责模块缓存替换和重载调度。配置监听器在当前事务上下文之外启动,使后续通知可以进入队列。不发布 invariant 伴生入口,因为队列和监听注册没有独立的持久投影。
59
+ App-boot 负责 profile 解析和 patch 优先级规则。HMR 读取启动器提供的纯数据 `profileContext`,在初始化时注册 profile manifest 和两份用户 patch 的监听,并等待应用就绪后处理更改。销毁 HMR 时会关闭监听器并取消等待启动的重载。配置监听器在当前事务上下文之外启动,使后续通知可以进入队列。
60
+
61
+ `node_modules` 之外的 `package.json` 变化使该包的配置缓存失效。仅作为配置的 manifest 不触发模块重载;作为 JSON 模块导入的 manifest 保留依赖驱动重载,位于宿主依赖图中的 manifest 保留宿主重载。专属配置监听保留原有归属。新包入口在 Loader entry 重启时生效。
62
+
63
+ Loader entry 保留原始导入结果。HMR 为每个 entry 名称与配置树 base URL 保留所有不同的已导入命名空间;尚未初始化的 entry 不会覆盖其他 entry 的命名空间。HMR 将这些对象与缓存中 ModuleJob 的模块命名空间按对象身份匹配,不再重新解析 entry 名称。没有已导入命名空间的名称和 `cordis:` 内置入口仍按名称解析。因此 `exports` 或 `main` 改变后,源码重载仍使用已加载模块。
64
+
65
+ 共享同一运行时的 entry 一起替换,每个 entry 使用其自身模块对应的实现。同一模块的重复 entry 仍保有各自的插件实例。替换失败时,HMR 清理已部分激活的替换实例,并恢复原来的模块和插件实现。重载成功后更新所有匹配的 entry 记录,包括已停用的 entry;替换失败时这些记录保持不变。从未加载过的 entry 仍保持未初始化状态。
66
+
67
+ [package-manifest.ts](src/package-manifest.ts) 封装 package 读取、scope、type 和最近 manifest 查询所需的 Node internal 接口。每个 HMR 实例拥有自己的配置缓存,销毁时恢复它安装的 hook。manifest 失效会清空 ESM `ResolveCache` 和 CommonJS `_pathCache`:它们都不记录查询过的全部 manifest,入口也可能指向包目录之外。CommonJS 加载先用 Node 默认 resolver 解析请求,再进入原生 loader,绕过旧请求别名并保留已求值模块。同步 resolve hook 收到的是解析后的文件名。普通模块替换仍由 HMR 独立处理。
60
68
 
61
69
  被监听模块的路径沿用 Node ESM 解析所用的 `realpathSync()` 表示,包括 Windows 短目录名,使文件事件与模块缓存匹配。
62
70
 
@@ -86,7 +94,11 @@ App-boot 负责 profile 解析和 patch 优先级规则。HMR 读取启动器提
86
94
  <a id="known-limitations-and-deferred-work"></a>
87
95
 
88
96
  - 模块替换需要 Node loader 内部接口。框架依赖变化调用宿主提供的 `loader.exit()` 钩子;HMR 本身不重启进程。
89
- - 通过插件管理器替换已安装包版本仍需要重启。浏览器 Client 模块图保留独立的浏览器侧加载机制。
97
+ - 通过插件管理器替换已安装包版本仍需要重启;`node_modules` 之下的 manifest 保留 Node 缓存的配置。浏览器 Client 模块图保留独立的浏览器侧加载机制。
98
+ - TSX loader 和普通应用自行创建的 Worker 不在包缓存刷新支持范围内。
99
+ - CommonJS 请求必须能由 Node 默认 resolver 解析。同步 hook 可以像 Desktop 的 Office resolver 一样处理解析后的文件名;依赖原始请求名或引入虚拟请求的 hook 不受支持。内置模块保留 Node 原有的加载行为。
100
+ - 后续工作:跨 HMR 销毁和替换保留包配置失效状态。恢复原生 reader 后,其旧配置缓存可能重新可见。
101
+ - 如果共享运行时的替换模块具有不同的插件回调,没有 Loader entry 的实例就无法确定应使用哪一个。HMR 会报告歧义错误并回滚此次重载。
90
102
  - `watchConfig()` 在 Chokidar 报告就绪时 resolve。darwin 上 libuv 随后才在自己的线程启动 FSEvents 流,因此注册后数毫秒内落地的写入要等到该目录的下一个事件才会被报告;启动之后的编辑不受影响。
91
103
 
92
104
  ### 开发备注
package/lib/index.js CHANGED
@@ -1,12 +1,14 @@
1
- import { createRequire } from "node:module";
1
+ import { createRequire, isBuiltin } from "node:module";
2
+ import assert from "node:assert/strict";
2
3
  import { AsyncLocalStorage } from "node:async_hooks";
3
- import { basename, dirname, join, relative, resolve } from "node:path";
4
+ import { basename, dirname, join, relative, resolve, sep, toNamespacedPath } from "node:path";
4
5
  import { realpath, stat } from "node:fs/promises";
5
6
  import { watch } from "chokidar";
6
7
  import { Inject, Service } from "@deepseek-ai/cordis";
7
8
  import { readFileSync, realpathSync } from "node:fs";
8
9
  import { PROFILE_PATCH_FILENAME, readProfileManifest, readProfilePatches, reconcileProfilePatches } from "@deepseek-ai/dsh-app-boot";
9
10
  import { codeFrameColumns } from "@babel/code-frame";
11
+ import { isUtf8 } from "node:buffer";
10
12
  import { fileURLToPath, pathToFileURL } from "node:url";
11
13
  import picomatch from "picomatch";
12
14
  import z from "@deepseek-ai/schemastery";
@@ -82,18 +84,24 @@ async function watchConfig(ctx, filename, options, refresh, inTransaction = () =
82
84
  watcher.on("unlink", onChange);
83
85
  const ready = Promise.withResolvers();
84
86
  let pending = true;
87
+ let disposed = false;
85
88
  watcher.once("ready", () => {
86
89
  pending = false;
87
90
  ready.resolve();
88
91
  });
89
- watcher.on("error", (error) => {
92
+ const onError = (error) => {
93
+ if (disposed) return;
90
94
  if (pending) {
91
95
  pending = false;
92
96
  ready.reject(error);
93
97
  } else ctx.logger.warn(error);
94
- });
98
+ };
99
+ watcher.on("error", onError);
95
100
  const dispose = async () => {
96
- await watcher.close();
101
+ disposed = true;
102
+ const closing = watcher.close();
103
+ watcher.on("error", onError);
104
+ await closing;
97
105
  paths.delete(target.filename);
98
106
  if (!inTransaction()) await running;
99
107
  };
@@ -140,6 +148,250 @@ function handleError(ctx, e) {
140
148
  }
141
149
  }
142
150
  //#endregion
151
+ //#region lib/types/package-manifest.js
152
+ /** Expire Node's cached package configuration for package directories whose manifest changed. */
153
+ /** Snapshot own methods; each disposer restores its method only while its replacement is still installed. */
154
+ function createReplaceHelper(target) {
155
+ const rawImpl = { ...target };
156
+ return {
157
+ rawImpl,
158
+ replaceMethod(key, replacement) {
159
+ target[key] = replacement;
160
+ return () => {
161
+ if (target[key] === replacement) target[key] = rawImpl[key];
162
+ };
163
+ }
164
+ };
165
+ }
166
+ /** Same lookup as the vendored Loader's `requireInternal`: `--expose-internals` first, then the builtin addon. */
167
+ function requireInternal(id) {
168
+ const require = createRequire(import.meta.url);
169
+ if (process.execArgv.includes("--expose-internals")) try {
170
+ return require(id);
171
+ } catch (_error) {}
172
+ try {
173
+ return require("node-addon-require-builtin").requireBuiltin(id);
174
+ } catch (_error) {}
175
+ }
176
+ function loadNodeInternals() {
177
+ const realm = requireInternal("internal/bootstrap/realm");
178
+ const esm = requireInternal("internal/modules/esm/loader");
179
+ return {
180
+ binding: realm.internalBinding("modules"),
181
+ reader: requireInternal("internal/modules/package_json_reader"),
182
+ ResolveCache: requireInternal("internal/modules/esm/module_map").ResolveCache,
183
+ cjs: requireInternal("internal/modules/cjs/loader").Module,
184
+ esmLoader: esm.getOrInitializeCascadedLoader()
185
+ };
186
+ }
187
+ /**
188
+ * Read the ESM loader's resolution cache by observing one lookup through its prototype.
189
+ * The prototype method is restored before this returns.
190
+ */
191
+ function captureEsmResolveCache(native, parentURL) {
192
+ const prototype = native.ResolveCache.prototype;
193
+ const original = prototype.get;
194
+ const seen = /* @__PURE__ */ new Set();
195
+ prototype.get = function(key, parent) {
196
+ seen.add(this);
197
+ return Reflect.apply(original, this, [key, parent]);
198
+ };
199
+ try {
200
+ const loader = native.esmLoader;
201
+ /* v8 ignore else -- CI coverage runs the Node 24 loader; the Node 22 matrix exercises the v1 signature */
202
+ if ("getOrCreateModuleJob" in loader) loader.resolveSync(parentURL, {
203
+ specifier: "node:path",
204
+ attributes: {}
205
+ });
206
+ else loader.resolveSync("node:path", parentURL, {});
207
+ } finally {
208
+ prototype.get = original;
209
+ }
210
+ return [...seen][0];
211
+ }
212
+ /** Package directories whose manifests are read from disk instead of Node's native cache. */
213
+ var PackageManifests = class {
214
+ directories = /* @__PURE__ */ new Set();
215
+ configurations = /* @__PURE__ */ new Map();
216
+ /** Real directory of each directory a lookup reached; linked consumers name a package by its link path. */
217
+ realDirectories = /* @__PURE__ */ new Map();
218
+ native;
219
+ rawBinding;
220
+ rawReader;
221
+ rawCommonJs;
222
+ restorers = [];
223
+ /**
224
+ * Expire cached configuration for one package directory. Loaded modules stay evaluated; later resolutions and
225
+ * format checks read the manifest on disk.
226
+ * @param manifest - absolute path of the changed `package.json`.
227
+ */
228
+ invalidate(manifest) {
229
+ const native = this.installPackageHooks();
230
+ const directory = dirname(manifest);
231
+ this.directories.add(directory);
232
+ this.configurations.clear();
233
+ this.realDirectories.clear();
234
+ for (const key of Object.keys(native.cjs._pathCache)) Reflect.deleteProperty(native.cjs._pathCache, key);
235
+ const cache = captureEsmResolveCache(native, pathToFileURL(directory).href + "/");
236
+ /* v8 ignore else -- asynchronous loader hooks own a separate cache; loader-thread invalidation is deferred */
237
+ if (cache !== void 0) Map.prototype.clear.call(cache);
238
+ }
239
+ /** Restore every replaced Node method this instance installed. */
240
+ dispose() {
241
+ for (const restore of this.restorers.splice(0).reverse()) restore();
242
+ this.native = void 0;
243
+ this.realDirectories.clear();
244
+ }
245
+ isPathWithinDirectory(path, directory) {
246
+ return path === directory || path.startsWith(join(directory, sep));
247
+ }
248
+ isPathInInvalidatedDirectory(path) {
249
+ const parent = dirname(path);
250
+ let real = this.realDirectories.get(parent);
251
+ if (real === void 0) {
252
+ try {
253
+ real = realpathSync(parent);
254
+ } catch (_error) {
255
+ real = parent;
256
+ }
257
+ this.realDirectories.set(parent, real);
258
+ }
259
+ const target = join(real, basename(path));
260
+ for (const directory of this.directories) if (this.isPathWithinDirectory(target, directory)) return true;
261
+ return false;
262
+ }
263
+ isUrlInInvalidatedDirectory(url) {
264
+ try {
265
+ return this.isPathInInvalidatedDirectory(fileURLToPath(url));
266
+ } catch (_error) {
267
+ return false;
268
+ }
269
+ }
270
+ installPackageHooks() {
271
+ if (this.native !== void 0) return this.native;
272
+ const native = loadNodeInternals();
273
+ this.native = native;
274
+ const binding = createReplaceHelper(native.binding);
275
+ const reader = createReplaceHelper(native.reader);
276
+ const commonJs = createReplaceHelper(native.cjs);
277
+ this.rawBinding = binding.rawImpl;
278
+ this.rawReader = reader.rawImpl;
279
+ this.rawCommonJs = commonJs.rawImpl;
280
+ this.restorers.push(binding.replaceMethod("readPackageJSON", this.hookBindingReadPackageJSON.bind(this)), binding.replaceMethod("getPackageScopeConfig", this.hookBindingGetPackageScopeConfig.bind(this)), binding.replaceMethod("getPackageType", this.hookBindingGetPackageType.bind(this)), reader.replaceMethod("getNearestParentPackageJSON", this.hookReaderGetNearestParentPackageJSON.bind(this)), commonJs.replaceMethod("_load", this.hookCommonJsLoad.bind(this)));
281
+ return native;
282
+ }
283
+ hookCommonJsLoad(...[request, parent, isMain, ...options]) {
284
+ const native = this.installPackageHooks();
285
+ const filename = request.startsWith("node:") || isBuiltin(request) ? request : native.cjs._resolveFilename(request, parent, isMain);
286
+ return this.rawCommonJs._load.call(native.cjs, filename, parent, isMain, ...options);
287
+ }
288
+ hookBindingReadPackageJSON(path, isEsm, base, specifier) {
289
+ return this.isPathInInvalidatedDirectory(path) ? this.readCachedPackageConfig(path, isEsm && base !== void 0 ? {
290
+ base,
291
+ specifier
292
+ } : void 0)?.serialized : this.rawBinding.readPackageJSON(path, isEsm, base, specifier);
293
+ }
294
+ hookBindingGetPackageScopeConfig(url) {
295
+ if (!this.isUrlInInvalidatedDirectory(url)) return this.rawBinding.getPackageScopeConfig(url);
296
+ const found = this.findNearestPackageConfig(fileURLToPath(url), true);
297
+ if (typeof found === "string") return this.rawBinding.getPackageScopeConfig(pathToFileURL(found).href);
298
+ return found.configuration?.serialized ?? found.path;
299
+ }
300
+ hookBindingGetPackageType(url) {
301
+ if (!this.isUrlInInvalidatedDirectory(url)) return this.rawBinding.getPackageType(url);
302
+ const found = this.findNearestPackageConfig(fileURLToPath(url), true);
303
+ return typeof found === "string" ? this.rawBinding.getPackageType(pathToFileURL(found).href) : found.configuration?.data.type;
304
+ }
305
+ hookReaderGetNearestParentPackageJSON(path) {
306
+ if (!this.isPathInInvalidatedDirectory(path)) return this.rawReader.getNearestParentPackageJSON(path);
307
+ const found = this.findNearestPackageConfig(path, false);
308
+ if (typeof found === "string") return this.rawReader.getNearestParentPackageJSON(found);
309
+ if (found.configuration === void 0) return this.rawReader.getNearestParentPackageJSON(path);
310
+ return {
311
+ data: found.configuration.data,
312
+ exists: true,
313
+ path: found.path
314
+ };
315
+ }
316
+ readCachedPackageConfig(path, context) {
317
+ if (!this.configurations.has(path)) this.configurations.set(path, this.readPackageConfigFromDisk(path, context));
318
+ return this.configurations.get(path);
319
+ }
320
+ /** A string names the first manifest outside the invalidated directories; Node answers for it. */
321
+ findNearestPackageConfig(path, includeRoot) {
322
+ let directory = dirname(path);
323
+ while (true) {
324
+ const manifest = join(directory, "package.json");
325
+ const parent = dirname(directory);
326
+ if (basename(directory) === "node_modules" || !includeRoot && parent === directory) return { path: manifest };
327
+ if (!this.isPathInInvalidatedDirectory(manifest)) return manifest;
328
+ const configuration = this.readCachedPackageConfig(manifest);
329
+ if (configuration !== void 0) return {
330
+ path: toNamespacedPath(manifest),
331
+ configuration
332
+ };
333
+ /* v8 ignore next -- root-scope queries run in an isolated child to contain non-termination */
334
+ if (parent === directory) return { path: manifest };
335
+ directory = parent;
336
+ }
337
+ }
338
+ /** @param context - optional ESM import details used only when reporting invalid package configuration. */
339
+ readPackageConfigFromDisk(path, context) {
340
+ let bytes;
341
+ try {
342
+ bytes = readFileSync(path);
343
+ } catch (_error) {
344
+ return;
345
+ }
346
+ const throwInvalidPackageConfig = () => {
347
+ const base = context?.base;
348
+ const importing = base === void 0 ? "" : ` while importing "${context?.specifier}" from ${base.startsWith("file:") ? fileURLToPath(base) : base}`;
349
+ throw Object.assign(/* @__PURE__ */ new Error(`Invalid package config ${toNamespacedPath(path)}${importing}.`), { code: "ERR_INVALID_PACKAGE_CONFIG" });
350
+ };
351
+ if (!isUtf8(bytes)) return throwInvalidPackageConfig();
352
+ const source = bytes.toString("utf8");
353
+ let parsed;
354
+ try {
355
+ parsed = JSON.parse(source.charCodeAt(0) === 65279 ? source.slice(1) : source);
356
+ } catch (_error) {
357
+ return throwInvalidPackageConfig();
358
+ }
359
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return throwInvalidPackageConfig();
360
+ const fields = parsed;
361
+ if (fields.name !== void 0 && typeof fields.name !== "string") return throwInvalidPackageConfig();
362
+ if (fields.type !== void 0 && typeof fields.type !== "string") return throwInvalidPackageConfig();
363
+ for (const key of [
364
+ "name",
365
+ "type",
366
+ "exports",
367
+ "imports"
368
+ ]) {
369
+ const value = fields[key];
370
+ if (typeof value === "string" && !value.isWellFormed()) return throwInvalidPackageConfig();
371
+ }
372
+ const normalizePackageJsonField = (value) => value !== null && (typeof value === "string" || typeof value === "object") ? value : void 0;
373
+ const data = {
374
+ ...typeof fields.name === "string" ? { name: fields.name } : {},
375
+ ...typeof fields.main === "string" && fields.main.isWellFormed() ? { main: fields.main } : {},
376
+ type: fields.type === "module" || fields.type === "commonjs" ? fields.type : "none",
377
+ exports: normalizePackageJsonField(fields.exports),
378
+ imports: normalizePackageJsonField(fields.imports)
379
+ };
380
+ const serializePackageJsonField = (value) => value === void 0 ? void 0 : typeof value === "string" ? value : JSON.stringify(value);
381
+ return {
382
+ data,
383
+ serialized: [
384
+ data.name,
385
+ data.main,
386
+ data.type,
387
+ serializePackageJsonField(data.imports),
388
+ serializePackageJsonField(data.exports),
389
+ toNamespacedPath(path)
390
+ ]
391
+ };
392
+ }
393
+ };
394
+ //#endregion
143
395
  //#region lib/types/index.js
144
396
  /** Serialized module and profile-configuration reloads. */
145
397
  var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
@@ -206,6 +458,107 @@ async function loadDependencies(job, ignored = /* @__PURE__ */ new Set()) {
206
458
  await traverse(job);
207
459
  return dependencies;
208
460
  }
461
+ /** Entry names with distinct namespaces; iteration falls back to undefined only for names without a loaded namespace. */
462
+ var EntryNamespaces = class {
463
+ internal;
464
+ moduleUrls;
465
+ names = /* @__PURE__ */ new Map();
466
+ constructor(internal, moduleUrls) {
467
+ this.internal = internal;
468
+ this.moduleUrls = moduleUrls;
469
+ }
470
+ static getModuleUrls(internal) {
471
+ const moduleUrls = /* @__PURE__ */ new Map();
472
+ for (const url of internal.loadCache.keys()) {
473
+ const job = internal.loadCache.get(url);
474
+ if (!job?.module) continue;
475
+ try {
476
+ moduleUrls.set(job.module.getNamespace(), url);
477
+ } catch (_error) {}
478
+ }
479
+ return moduleUrls;
480
+ }
481
+ add(name, moduleNamespace) {
482
+ const namespaces = this.names.get(name) ?? /* @__PURE__ */ new Set();
483
+ namespaces.add(moduleNamespace);
484
+ this.names.set(name, namespaces);
485
+ return this;
486
+ }
487
+ *[Symbol.iterator]() {
488
+ for (const [name, namespaces] of this.names) for (const moduleNamespace of namespaces) {
489
+ if (moduleNamespace === void 0 && namespaces.size > 1) continue;
490
+ yield [name, moduleNamespace];
491
+ }
492
+ }
493
+ async resolve(name, baseUrl, moduleNamespace) {
494
+ if (moduleNamespace === void 0 || name.startsWith("cordis:")) switch (this.internal.version) {
495
+ case "v1": return await this.internal.resolve(name, baseUrl, {});
496
+ case "v2": return this.internal.resolveSync(baseUrl, {
497
+ specifier: name,
498
+ attributes: {}
499
+ });
500
+ }
501
+ const url = this.moduleUrls.get(moduleNamespace);
502
+ if (url === void 0) throw new Error(`HMR cannot locate the loaded module for ${name} from ${baseUrl}`);
503
+ return { url };
504
+ }
505
+ };
506
+ /** Entry modules belonging to one runtime, keyed by their original namespaces across replacement. */
507
+ var ReloadModules = class ReloadModules {
508
+ modules = /* @__PURE__ */ new Map();
509
+ static include(reloads, plugin, job, runtime) {
510
+ let info = reloads.get(plugin);
511
+ if (info === void 0 && runtime) info = [...reloads.values()].find((info) => info.runtime === runtime);
512
+ if (info === void 0) return false;
513
+ info.modules.add(job, plugin);
514
+ return true;
515
+ }
516
+ add(job, plugin) {
517
+ assert(job.module, `HMR pending module is missing: ${job.url}`);
518
+ const moduleNamespace = job.module.getNamespace();
519
+ this.modules.set(moduleNamespace, {
520
+ filename: job.url,
521
+ moduleNamespace,
522
+ plugin
523
+ });
524
+ return this;
525
+ }
526
+ async importRemaining(loader, getOuterStack, primary) {
527
+ const replacements = new ReloadModules();
528
+ for (const [originalNamespace, { filename }] of this.modules) {
529
+ if (filename === primary.filename) {
530
+ replacements.modules.set(originalNamespace, primary);
531
+ continue;
532
+ }
533
+ const moduleNamespace = await loader.import(filename, getOuterStack);
534
+ const plugin = loader.unwrapExports(moduleNamespace);
535
+ replacements.modules.set(originalNamespace, {
536
+ filename,
537
+ moduleNamespace,
538
+ plugin
539
+ });
540
+ }
541
+ return replacements;
542
+ }
543
+ *getActiveImplementations(ctx, plugin, fibers) {
544
+ const callbacks = new Set([...this.modules.values()].map((value) => ctx.registry.resolve(value.plugin)));
545
+ const implementations = fibers.map((previousFiber) => {
546
+ if (previousFiber.entry === void 0) return [previousFiber, plugin];
547
+ const replacement = this.modules.get(previousFiber.moduleNamespace);
548
+ const implementation = replacement ? replacement.plugin : ctx.loader.unwrapExports(previousFiber.moduleNamespace);
549
+ callbacks.add(ctx.registry.resolve(implementation));
550
+ return [previousFiber, implementation];
551
+ });
552
+ for (const [previousFiber, implementation] of implementations) {
553
+ if (previousFiber.fiber.parent.fiber.uid === null) continue;
554
+ if (previousFiber.entry === void 0 && callbacks.size > 1) throw new Error("HMR replacement is ambiguous for a plugin instance without a Loader entry");
555
+ yield [previousFiber, implementation];
556
+ }
557
+ }
558
+ updateEntries(loader) {
559
+ for (const [originalNamespace, { moduleNamespace }] of this.modules) for (const entry of loader.entries()) if (entry.moduleNamespace === originalNamespace) entry.moduleNamespace = moduleNamespace;
560
+ }
561
+ };
209
562
  /** Hot reload service with Cordis-compatible module configuration and events. */
210
563
  let Hmr = (() => {
211
564
  let _classDecorators = [Inject("loader"), Inject("timer")];
@@ -272,6 +625,7 @@ let Hmr = (() => {
272
625
  applicationReady = Promise.resolve(true);
273
626
  closing = false;
274
627
  configPaths = /* @__PURE__ */ new Set();
628
+ manifests = new PackageManifests();
275
629
  /** Serialize a caller-owned mutation with all automatic reload paths.
276
630
  * @param operation Work that must not overlap module or configuration replacement.
277
631
  * @returns The operation result after its asynchronous work completes.
@@ -318,23 +672,12 @@ let Hmr = (() => {
318
672
  this.internal = this.ctx.loader.internal;
319
673
  this.baseDir = fileURLToPath(new URL(config.base || ".", ctx.baseUrl));
320
674
  }
321
- /**
322
- * Resolve a module specifier to a URL, compatible with Node 22-24.
323
- */
324
- async _resolve(specifier, parentURL, attrs) {
325
- switch (this.internal.version) {
326
- case "v1": return await this.internal.resolve(specifier, parentURL, attrs);
327
- case "v2": return this.internal.resolveSync(parentURL, {
328
- specifier,
329
- attributes: attrs
330
- });
331
- }
332
- }
333
675
  async *[Service.init]() {
334
676
  yield async () => {
335
677
  this.closing = true;
336
678
  await this.watcher?.close();
337
679
  if (!this.executing.getStore()) await this.operations;
680
+ this.manifests.dispose();
338
681
  };
339
682
  const profile = this.ownerContext.get("profileContext");
340
683
  if (profile !== void 0) {
@@ -402,15 +745,18 @@ let Hmr = (() => {
402
745
  const filename = canonicalPath(resolve(watchBaseDir, path));
403
746
  const configuredFilename = resolve(this.baseDir, path);
404
747
  if (this.configPaths.has(filename) || this.configPaths.has(configuredFilename)) continue;
748
+ const isManifest = basename(filename) === "package.json" && !filename.includes(`${sep}node_modules${sep}`);
749
+ if (isManifest) this.manifests.invalidate(filename);
405
750
  const url = pathToFileURL(filename).href;
406
751
  if (this.externals.has(url)) {
407
752
  fullReload = true;
408
753
  continue;
409
754
  }
410
- if (this.internal.loadCache.has(url)) {
755
+ if (this.internal.loadCache.has(url) || this.internal.loadCache.has(url, "json")) {
411
756
  this.stashed.add(url);
412
757
  continue;
413
758
  }
759
+ if (isManifest) continue;
414
760
  const include = [...loader.entries()].map((entry) => entry.subtree).find((tree) => tree?.filename === filename || tree?.filename === configuredFilename);
415
761
  if (include !== void 0) includes.add(include);
416
762
  else this.ctx.emit("hmr/change", url);
@@ -517,16 +863,17 @@ let Hmr = (() => {
517
863
  await this.analyzeChanges();
518
864
  const pending = /* @__PURE__ */ new Map();
519
865
  const reloads = /* @__PURE__ */ new Map();
866
+ const moduleUrls = EntryNamespaces.getModuleUrls(this.internal);
520
867
  const nameMap = /* @__PURE__ */ new Map();
521
868
  for (const entry of this.ctx.loader.entries()) {
522
869
  const baseUrl = entry.parent.tree.ctx.baseUrl;
523
870
  if (baseUrl === void 0) throw new Error("HMR entry tree has no base URL");
524
- const names = nameMap.get(baseUrl) ?? /* @__PURE__ */ new Set();
525
- names.add(entry.options.name);
871
+ const names = nameMap.get(baseUrl) ?? new EntryNamespaces(this.internal, moduleUrls);
872
+ names.add(entry.options.name, entry.moduleNamespace);
526
873
  nameMap.set(baseUrl, names);
527
874
  }
528
- for (const [baseUrl, names] of nameMap) for (const name of names) try {
529
- const { url } = await this._resolve(name, baseUrl, {});
875
+ for (const [baseUrl, names] of nameMap) for (const [name, moduleNamespace] of names) try {
876
+ const { url } = await names.resolve(name, baseUrl, moduleNamespace);
530
877
  if (this.declined.has(url)) continue;
531
878
  const job = this.internal.loadCache.get(url);
532
879
  const plugin = this.ctx.loader.unwrapExports(job?.module?.getNamespace());
@@ -540,13 +887,21 @@ let Hmr = (() => {
540
887
  this.declined.delete(job.url);
541
888
  const dependencies = [...await loadDependencies(job, this.declined)];
542
889
  this.declined.add(job.url);
890
+ if (dependencies.length === 0) {
891
+ pending.delete(job);
892
+ continue;
893
+ }
543
894
  if (!dependencies.some((dep) => this.accepted.has(dep))) continue;
544
895
  dependencies.forEach((dep) => this.accepted.add(dep));
896
+ const runtime = this.ctx.registry.get(plugin);
897
+ if (ReloadModules.include(reloads, plugin, job, runtime)) continue;
545
898
  reloads.set(plugin, {
546
899
  filename: job.url,
547
- runtime: this.ctx.registry.get(plugin)
900
+ runtime,
901
+ modules: new ReloadModules().add(job, plugin)
548
902
  });
549
903
  }
904
+ for (const [job, plugin] of pending) if (ReloadModules.include(reloads, plugin, job, this.ctx.registry.get(plugin))) this.accepted.add(job.url);
550
905
  /**
551
906
  * Clear module caches for all accepted files before re-importing.
552
907
  *
@@ -590,17 +945,26 @@ let Hmr = (() => {
590
945
  return {
591
946
  fiber,
592
947
  entry,
593
- config: entry === void 0 ? fiber._config : entry.options.config
948
+ config: entry === void 0 ? fiber._config : entry.options.config,
949
+ moduleNamespace: entry?.moduleNamespace
594
950
  };
595
951
  })
596
952
  }));
597
953
  const attempts = [];
598
954
  try {
599
955
  for (const generation of generations) {
600
- const replacement = this.ctx.loader.unwrapExports(await this.ctx.loader.import(generation.filename, this.getOuterStack));
956
+ const moduleNamespace = await this.ctx.loader.import(generation.filename, this.getOuterStack);
957
+ const replacement = this.ctx.loader.unwrapExports(moduleNamespace);
958
+ const replacements = await generation.modules.importRemaining(this.ctx.loader, this.getOuterStack, {
959
+ filename: generation.filename,
960
+ moduleNamespace,
961
+ plugin: replacement
962
+ });
601
963
  attempts.push({
602
964
  ...generation,
603
- replacement
965
+ replacement,
966
+ replacements,
967
+ activated: []
604
968
  });
605
969
  }
606
970
  } catch (e) {
@@ -608,11 +972,9 @@ let Hmr = (() => {
608
972
  rollback();
609
973
  throw e;
610
974
  }
611
- const reload = async (plugin, fibers) => {
612
- const activated = [];
613
- for (const previousFiber of fibers) {
614
- if (previousFiber.fiber.parent.fiber.uid === null) continue;
615
- const fiber = previousFiber.fiber.parent.registry.plugin(plugin, previousFiber.config, this.getOuterStack).ctx.fiber;
975
+ const reload = async (plugin, fibers, replacements = new ReloadModules(), activated = []) => {
976
+ for (const [previousFiber, implementation] of replacements.getActiveImplementations(this.ctx, plugin, fibers)) {
977
+ const fiber = previousFiber.fiber.parent.registry.plugin(implementation, previousFiber.config, this.getOuterStack).ctx.fiber;
616
978
  if (previousFiber.entry !== void 0) {
617
979
  fiber.entry = previousFiber.entry;
618
980
  previousFiber.entry.fiber = fiber;
@@ -623,7 +985,7 @@ let Hmr = (() => {
623
985
  };
624
986
  const removed = /* @__PURE__ */ new Set();
625
987
  try {
626
- for (const { previous: plugin, replacement, filename, runtime, fibers } of attempts) {
988
+ for (const { previous: plugin, replacement, filename, runtime, fibers, replacements, activated } of attempts) {
627
989
  if (!runtime) continue;
628
990
  const path = relative(this.baseDir, fileURLToPath(filename));
629
991
  removed.add(plugin);
@@ -635,7 +997,7 @@ let Hmr = (() => {
635
997
  this.ctx.logger.warn(err);
636
998
  }
637
999
  try {
638
- await reload(replacement, fibers);
1000
+ await reload(replacement, fibers, replacements, activated);
639
1001
  this.ctx.logger.info("reload plugin at %C", path);
640
1002
  } catch (err) {
641
1003
  this.ctx.logger.warn("failed to reload plugin at %C", path);
@@ -645,12 +1007,14 @@ let Hmr = (() => {
645
1007
  }
646
1008
  } catch (error) {
647
1009
  rollback();
648
- for (const { previous: plugin, replacement, fibers } of attempts) {
1010
+ for (const { previous: plugin, fibers, activated } of attempts) {
649
1011
  if (!removed.has(plugin)) continue;
650
1012
  try {
651
- const replacementFibers = [...this.ctx.registry.get(replacement)?.fibers ?? []];
652
- this.ctx.registry.delete(replacement);
653
- await Promise.allSettled(replacementFibers.map((fiber) => fiber.await()));
1013
+ for (const { runtime } of activated) if (runtime && this.ctx.registry.get(runtime.callback) === runtime) {
1014
+ const replacementFibers = [...runtime.fibers];
1015
+ this.ctx.registry.delete(runtime.callback);
1016
+ await Promise.allSettled(replacementFibers.map((fiber) => fiber.await()));
1017
+ }
654
1018
  await reload(plugin, fibers);
655
1019
  } catch (err) {
656
1020
  this.ctx.logger.warn(err);
@@ -659,6 +1023,7 @@ let Hmr = (() => {
659
1023
  throw error;
660
1024
  }
661
1025
  await this.ctx.loader.await();
1026
+ for (const { replacements } of attempts) replacements.updateEntries(this.ctx.loader);
662
1027
  this.ctx.emit("hmr/reload", reloads);
663
1028
  this.stashed = /* @__PURE__ */ new Set();
664
1029
  }
@@ -1,4 +1,5 @@
1
- import { Context, Service, type Plugin } from '@deepseek-ai/cordis';
1
+ import { Context, Service, type Fiber, type Plugin } from '@deepseek-ai/cordis';
2
+ import type { ModuleJob } from '@deepseek-ai/cordis-plugin-loader';
2
3
  import { type ChokidarOptions } from 'chokidar';
3
4
  import z from '@deepseek-ai/schemastery';
4
5
  declare module '@deepseek-ai/cordis' {
@@ -33,8 +34,30 @@ export interface HmrConfig extends ChokidarOptions {
33
34
  /** Module location and runtime retained during a replacement. */
34
35
  export interface Reload {
35
36
  filename: string;
37
+ /** Original namespaces and URLs of all entry modules participating in this runtime replacement. */
38
+ modules: ReloadModules;
36
39
  runtime?: Plugin.Runtime | undefined;
37
40
  }
41
+ type ReloadFiber = {
42
+ fiber: Fiber;
43
+ entry: Fiber['entry'];
44
+ config: unknown;
45
+ moduleNamespace: unknown;
46
+ };
47
+ type ReloadModule = {
48
+ filename: string;
49
+ moduleNamespace: unknown;
50
+ plugin: Plugin;
51
+ };
52
+ /** Entry modules belonging to one runtime, keyed by their original namespaces across replacement. */
53
+ declare class ReloadModules {
54
+ private readonly modules;
55
+ static include(reloads: ReadonlyMap<Plugin, Reload>, plugin: Plugin, job: ModuleJob, runtime: Plugin.Runtime | undefined): boolean;
56
+ add(job: ModuleJob, plugin: Plugin): this;
57
+ importRemaining(loader: Context['loader'], getOuterStack: () => string[], primary: ReloadModule): Promise<ReloadModules>;
58
+ getActiveImplementations(ctx: Context, plugin: Plugin, fibers: readonly ReloadFiber[]): Generator<[ReloadFiber, Plugin]>;
59
+ updateEntries(loader: Context['loader']): void;
60
+ }
38
61
  /** Hot reload service with Cordis-compatible module configuration and events. */
39
62
  declare class Hmr extends Service {
40
63
  config: HmrConfig;
@@ -67,6 +90,7 @@ declare class Hmr extends Service {
67
90
  private applicationReady;
68
91
  private closing;
69
92
  private readonly configPaths;
93
+ private readonly manifests;
70
94
  /** Serialize a caller-owned mutation with all automatic reload paths.
71
95
  * @param operation Work that must not overlap module or configuration replacement.
72
96
  * @returns The operation result after its asynchronous work completes.
@@ -80,10 +104,6 @@ declare class Hmr extends Service {
80
104
  */
81
105
  watchConfig(filename: string, refresh: () => Promise<void>): Promise<() => Promise<void>>;
82
106
  constructor(ctx: Context, config: HmrConfig);
83
- /**
84
- * Resolve a module specifier to a URL, compatible with Node 22-24.
85
- */
86
- private _resolve;
87
107
  [Service.init](): AsyncGenerator<() => Promise<void>, void, unknown>;
88
108
  /** Omit internal HMR frames from module import diagnostics.
89
109
  * @returns The preserved outer stack frames.
@@ -0,0 +1,35 @@
1
+ /** Package directories whose manifests are read from disk instead of Node's native cache. */
2
+ export declare class PackageManifests {
3
+ private readonly directories;
4
+ private readonly configurations;
5
+ /** Real directory of each directory a lookup reached; linked consumers name a package by its link path. */
6
+ private readonly realDirectories;
7
+ private native;
8
+ private rawBinding;
9
+ private rawReader;
10
+ private rawCommonJs;
11
+ private readonly restorers;
12
+ /**
13
+ * Expire cached configuration for one package directory. Loaded modules stay evaluated; later resolutions and
14
+ * format checks read the manifest on disk.
15
+ * @param manifest - absolute path of the changed `package.json`.
16
+ */
17
+ invalidate(manifest: string): void;
18
+ /** Restore every replaced Node method this instance installed. */
19
+ dispose(): void;
20
+ private isPathWithinDirectory;
21
+ private isPathInInvalidatedDirectory;
22
+ private isUrlInInvalidatedDirectory;
23
+ private installPackageHooks;
24
+ private hookCommonJsLoad;
25
+ private hookBindingReadPackageJSON;
26
+ private hookBindingGetPackageScopeConfig;
27
+ private hookBindingGetPackageType;
28
+ private hookReaderGetNearestParentPackageJSON;
29
+ private readCachedPackageConfig;
30
+ /** A string names the first manifest outside the invalidated directories; Node answers for it. */
31
+ private findNearestPackageConfig;
32
+ /** @param context - optional ESM import details used only when reporting invalid package configuration. */
33
+ private readPackageConfigFromDisk;
34
+ }
35
+ //# sourceMappingURL=package-manifest.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-hmr",
3
3
  "description": "Coordinated module and profile configuration hot reload",
4
- "version": "0.2.0-rc.2",
4
+ "version": "0.2.1-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -28,29 +28,30 @@
28
28
  "author": "Shigma <shigma10826@gmail.com>",
29
29
  "license": "MIT",
30
30
  "peerDependencies": {
31
- "@deepseek-ai/cordis-plugin-timer": "~1.1.6",
32
- "@deepseek-ai/cordis": "~4.0.4",
33
- "@deepseek-ai/cordis-plugin-loader": "~1.0.5",
34
- "@deepseek-ai/cordis-plugin-include": "~1.0.9",
35
- "@deepseek-ai/dsh-app-boot": "0.2.0-rc.2",
36
- "@deepseek-ai/dsh-cmdline": "0.2.0-rc.2"
31
+ "@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.1",
32
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1",
33
+ "@deepseek-ai/cordis-plugin-loader": "~1.0.6-alpha.1",
34
+ "@deepseek-ai/cordis-plugin-include": "~1.0.10-alpha.1",
35
+ "@deepseek-ai/dsh-app-boot": "0.2.1-alpha.2",
36
+ "@deepseek-ai/dsh-cmdline": "0.2.1-alpha.2"
37
37
  },
38
38
  "dependencies": {
39
39
  "@babel/code-frame": "^7.29.0",
40
40
  "chokidar": "^4.0.3",
41
- "picomatch": "^4.0.3",
42
- "@deepseek-ai/schemastery": "~3.18.4"
41
+ "node-addon-require-builtin": "^0.1.9",
42
+ "picomatch": "^4.0.7",
43
+ "@deepseek-ai/schemastery": "~3.18.5-alpha.1"
43
44
  },
44
45
  "devDependencies": {
45
46
  "@types/babel__code-frame": "^7.27.0",
46
- "@types/picomatch": "^3.0.2",
47
- "esbuild": "^0.28.1",
48
- "@deepseek-ai/cordis": "~4.0.4",
49
- "@deepseek-ai/cordis-plugin-loader": "~1.0.5",
50
- "@deepseek-ai/cordis-plugin-include": "~1.0.9",
51
- "@deepseek-ai/dsh-app-boot": "0.2.0-rc.2",
52
- "@deepseek-ai/cordis-plugin-timer": "~1.1.6",
53
- "@deepseek-ai/dsh-atomic-write": "0.2.0-rc.2",
54
- "@deepseek-ai/dsh-cmdline": "0.2.0-rc.2"
47
+ "@types/picomatch": "^4.0.3",
48
+ "esbuild": "^0.28.2",
49
+ "@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.1",
50
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1",
51
+ "@deepseek-ai/cordis-plugin-loader": "~1.0.6-alpha.1",
52
+ "@deepseek-ai/cordis-plugin-include": "~1.0.10-alpha.1",
53
+ "@deepseek-ai/dsh-app-boot": "0.2.1-alpha.2",
54
+ "@deepseek-ai/dsh-cmdline": "0.2.1-alpha.2",
55
+ "@deepseek-ai/dsh-atomic-write": "0.2.1-alpha.2"
55
56
  }
56
57
  }