dsh-hot-reload 0.1.2 → 0.1.3
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/CHANGELOG.md +19 -0
- package/lib/index.js +65 -23
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
All notable changes to `dsh-hot-reload` are documented here. This project
|
|
4
4
|
follows [semantic versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## 0.1.3
|
|
7
|
+
|
|
8
|
+
Code-review fixes (engine + CI):
|
|
9
|
+
|
|
10
|
+
- Reload each changed package **once per module**, not once per plugin row —
|
|
11
|
+
rows sharing a specifier share a runtime, so per-row reloading double-applied
|
|
12
|
+
and leaked a module instance.
|
|
13
|
+
- **Serialize reload cycles** so a change arriving mid-reload can't run a second
|
|
14
|
+
cycle concurrently against the registry.
|
|
15
|
+
- Commit the tracked version **only after a successful reload/skip**, so a failed
|
|
16
|
+
reload can be retried by re-installing the same version.
|
|
17
|
+
- **Validate the profile dir** (warn if no `pnpm-lock.yaml`), and prefer a
|
|
18
|
+
candidate dir that actually contains the lockfile.
|
|
19
|
+
- Disposer now awaits `watcher.close()` and guards against in-flight reloads via
|
|
20
|
+
a `disposed` flag.
|
|
21
|
+
- CI: add a `concurrency` group so a commit+tag push can't race to publish
|
|
22
|
+
(E403); publish only on a confirmed `E404` (not on transient `npm view`
|
|
23
|
+
failures).
|
|
24
|
+
|
|
6
25
|
## 0.1.2
|
|
7
26
|
|
|
8
27
|
- Docs: add a **Compatibility** section (built/tested against dsh `0.1.0-rc.6`;
|
package/lib/index.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// crash dsh.
|
|
21
21
|
|
|
22
22
|
import { watch } from "chokidar";
|
|
23
|
-
import { readFileSync } from "node:fs";
|
|
23
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
24
24
|
import { createRequire } from "node:module";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { dirname, join } from "node:path";
|
|
@@ -55,6 +55,14 @@ export function apply(ctx, config = {}) {
|
|
|
55
55
|
}
|
|
56
56
|
const lockfile = join(profileDir, "pnpm-lock.yaml");
|
|
57
57
|
const nodeModules = join(profileDir, "node_modules");
|
|
58
|
+
// Validate the auto-detected dir loudly: watching a wrong/nonexistent lockfile
|
|
59
|
+
// would silently track 0 packages and never fire.
|
|
60
|
+
if (!existsSync(lockfile)) {
|
|
61
|
+
log.warn?.(
|
|
62
|
+
`dsh-hot-reload: no pnpm-lock.yaml at ${lockfile} — is this the profile dir? ` +
|
|
63
|
+
"set config.profileDir to fix; the plugin will watch but detect nothing until it appears."
|
|
64
|
+
);
|
|
65
|
+
}
|
|
58
66
|
|
|
59
67
|
// ---- package <-> loader-entry helpers ----
|
|
60
68
|
|
|
@@ -196,26 +204,44 @@ export function apply(ctx, config = {}) {
|
|
|
196
204
|
|
|
197
205
|
// ---- change handling ----
|
|
198
206
|
|
|
207
|
+
/** Reload every plugin row of a changed package. Returns true if the new
|
|
208
|
+
* version should be committed to the tracked snapshot (success, or a
|
|
209
|
+
* terminal skip); false only when a reload was attempted and FAILED, so an
|
|
210
|
+
* identical re-install can retry. */
|
|
199
211
|
async function handlePackage(pkg) {
|
|
200
212
|
const affected = entriesForPkg(pkg);
|
|
201
|
-
if (!affected.length) return; // not a loaded plugin (
|
|
213
|
+
if (!affected.length) return true; // not a loaded plugin (fresh install) — out of scope
|
|
202
214
|
const version = versionOf(pkg) ?? "?";
|
|
203
215
|
|
|
204
216
|
if (optedOut(pkg)) {
|
|
205
217
|
log.info?.(`dsh-hot-reload: ${pkg}@${version} sets dsh.hotReload:false — restart dsh to load the new version`);
|
|
206
|
-
return;
|
|
218
|
+
return true;
|
|
207
219
|
}
|
|
208
220
|
if (!internal) {
|
|
209
221
|
log.info?.(`dsh-hot-reload: ${pkg}@${version} changed — restart dsh to load the new version`);
|
|
210
|
-
return;
|
|
222
|
+
return true;
|
|
211
223
|
}
|
|
212
224
|
|
|
225
|
+
// De-duplicate by module specifier: rows sharing one specifier share one
|
|
226
|
+
// runtime, and reloadEntry swaps ALL of that runtime's fibers at once —
|
|
227
|
+
// reloading per-entry would re-import and double-apply. Distinct specifiers
|
|
228
|
+
// (e.g. a package with several host files) each still get reloaded.
|
|
229
|
+
const seen = new Set();
|
|
230
|
+
const modules = affected.filter((e) => {
|
|
231
|
+
const key = e?.options?.name;
|
|
232
|
+
if (!key || seen.has(key)) return false;
|
|
233
|
+
seen.add(key);
|
|
234
|
+
return true;
|
|
235
|
+
});
|
|
236
|
+
|
|
213
237
|
try {
|
|
214
|
-
for (const entry of
|
|
215
|
-
log.info?.(`dsh-hot-reload: hot-reloaded ${pkg}@${version} (${
|
|
238
|
+
for (const entry of modules) await reloadEntry(entry);
|
|
239
|
+
log.info?.(`dsh-hot-reload: hot-reloaded ${pkg}@${version} (${modules.length} module(s))`);
|
|
240
|
+
return true;
|
|
216
241
|
} catch (err) {
|
|
217
242
|
log.warn?.(`dsh-hot-reload: could not hot-reload ${pkg}@${version} — restart dsh to load the new version`);
|
|
218
243
|
log.warn?.(err);
|
|
244
|
+
return false;
|
|
219
245
|
}
|
|
220
246
|
}
|
|
221
247
|
|
|
@@ -223,17 +249,27 @@ export function apply(ctx, config = {}) {
|
|
|
223
249
|
|
|
224
250
|
let versions = snapshotVersions();
|
|
225
251
|
let timer = null;
|
|
252
|
+
let disposed = false;
|
|
253
|
+
let running = Promise.resolve(); // serializes reload cycles across debounce batches
|
|
254
|
+
|
|
255
|
+
async function runCycle() {
|
|
256
|
+
if (disposed) return;
|
|
257
|
+
const next = snapshotVersions();
|
|
258
|
+
for (const pkg in next) {
|
|
259
|
+
if (disposed) return;
|
|
260
|
+
if (versions[pkg] === next[pkg]) continue;
|
|
261
|
+
const commit = await handlePackage(pkg);
|
|
262
|
+
if (commit) versions[pkg] = next[pkg]; // commit only on success/skip (failed reload retries)
|
|
263
|
+
}
|
|
264
|
+
for (const pkg of Object.keys(versions)) if (!(pkg in next)) delete versions[pkg]; // drop uninstalled
|
|
265
|
+
}
|
|
266
|
+
|
|
226
267
|
const trigger = () => {
|
|
268
|
+
if (disposed) return;
|
|
227
269
|
if (timer) clearTimeout(timer);
|
|
228
|
-
timer = setTimeout(
|
|
270
|
+
timer = setTimeout(() => {
|
|
229
271
|
timer = null;
|
|
230
|
-
|
|
231
|
-
const changed = [];
|
|
232
|
-
for (const pkg in next) {
|
|
233
|
-
if (versions[pkg] !== next[pkg]) changed.push(pkg);
|
|
234
|
-
}
|
|
235
|
-
versions = next;
|
|
236
|
-
for (const pkg of changed) await handlePackage(pkg);
|
|
272
|
+
running = running.then(runCycle).catch((e) => log.warn?.("dsh-hot-reload: reload cycle error", e));
|
|
237
273
|
}, debounceMs);
|
|
238
274
|
};
|
|
239
275
|
|
|
@@ -242,9 +278,13 @@ export function apply(ctx, config = {}) {
|
|
|
242
278
|
watcher.on("add", trigger);
|
|
243
279
|
watcher.on("error", (e) => log.warn?.("dsh-hot-reload: watcher error", e));
|
|
244
280
|
|
|
245
|
-
ctx.effect(() => () => {
|
|
281
|
+
ctx.effect(() => async () => {
|
|
282
|
+
disposed = true;
|
|
246
283
|
if (timer) clearTimeout(timer);
|
|
247
|
-
|
|
284
|
+
try {
|
|
285
|
+
await watcher.close();
|
|
286
|
+
} catch {}
|
|
287
|
+
await running.catch(() => {}); // let any in-flight cycle settle
|
|
248
288
|
});
|
|
249
289
|
|
|
250
290
|
log.info?.(
|
|
@@ -252,14 +292,16 @@ export function apply(ctx, config = {}) {
|
|
|
252
292
|
);
|
|
253
293
|
}
|
|
254
294
|
|
|
255
|
-
/** Best-effort profile-dir resolution: explicit config, then the loader base URL.
|
|
295
|
+
/** Best-effort profile-dir resolution: explicit config, then the loader base URL.
|
|
296
|
+
* Prefer a candidate that actually contains a pnpm-lock.yaml. */
|
|
256
297
|
function resolveProfileDir(ctx, config) {
|
|
257
|
-
|
|
298
|
+
const candidates = [];
|
|
299
|
+
if (config.profileDir) candidates.push(config.profileDir);
|
|
258
300
|
try {
|
|
259
|
-
if (ctx.baseUrl)
|
|
260
|
-
const p = fileURLToPath(new URL(".", ctx.baseUrl));
|
|
261
|
-
return p.replace(/\/$/, "");
|
|
262
|
-
}
|
|
301
|
+
if (ctx.baseUrl) candidates.push(fileURLToPath(new URL(".", ctx.baseUrl)).replace(/\/$/, ""));
|
|
263
302
|
} catch {}
|
|
264
|
-
|
|
303
|
+
for (const dir of candidates) {
|
|
304
|
+
if (existsSync(join(dir, "pnpm-lock.yaml"))) return dir;
|
|
305
|
+
}
|
|
306
|
+
return candidates[0] ?? null; // fall back (apply() warns if the lockfile is missing)
|
|
265
307
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-hot-reload",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Live-reload upgraded DeepSeek Harness (dsh) plugins without restarting dsh
|
|
5
|
+
"description": "Live-reload upgraded DeepSeek Harness (dsh) plugins without restarting dsh \u2014 safe plugins are hot-reloaded in place; unsafe ones are flagged for a manual restart.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
|
8
8
|
"dsh-plugin",
|