@max-null/dsh-plugin-center 0.2.10 → 0.2.11
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/client.js +23 -1
- package/dist/rpc.js +1 -0
- package/dist/update.d.ts +20 -0
- package/dist/update.js +131 -2
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -408,9 +408,11 @@ var STRINGS = {
|
|
|
408
408
|
installFailed: "\u5B89\u88C5\u5931\u8D25\uFF1A{e}",
|
|
409
409
|
installNotApplied: "\u5B89\u88C5\u672A\u751F\u6548",
|
|
410
410
|
updatedOne: "\u5DF2\u66F4\u65B0 {n}\uFF0C\u91CD\u542F\u540E\u751F\u6548",
|
|
411
|
+
updatedHot: "\u5DF2\u66F4\u65B0 {n}\uFF0C\u524D\u7AEF\u5DF2\u70ED\u751F\u6548\uFF0C\u65E0\u9700\u91CD\u542F",
|
|
411
412
|
updateFailed: "\u66F4\u65B0\u5931\u8D25\uFF1A{e}",
|
|
412
413
|
updateNotApplied: "\u66F4\u65B0\u672A\u751F\u6548",
|
|
413
414
|
updatedMany: "\u5DF2\u66F4\u65B0 {n} \u4E2A\u63D2\u4EF6\uFF0C\u91CD\u542F\u540E\u751F\u6548",
|
|
415
|
+
updatedManyHot: "\u5DF2\u66F4\u65B0 {n} \u4E2A\u63D2\u4EF6\uFF0C\u524D\u7AEF\u5DF2\u70ED\u751F\u6548",
|
|
414
416
|
commandTitle: "\u5728\u7EC8\u7AEF\u6267\u884C\u4EE5\u4E0B\u547D\u4EE4",
|
|
415
417
|
commandHint: "\u5F53\u524D\u5E94\u7528\u6B63\u5728\u8FD0\u884C\uFF0C\u88AB\u9501\u5B9A\u7684\u6587\u4EF6\u65E0\u6CD5\u5728\u5E94\u7528\u5185\u66FF\u6362\u3002\u8BF7\u5148\u5173\u95ED\u5E94\u7528\uFF0C\u518D\u5728\u7EC8\u7AEF\u6267\u884C\uFF1A",
|
|
416
418
|
commandCopy: "\u590D\u5236\u547D\u4EE4",
|
|
@@ -493,9 +495,11 @@ var STRINGS = {
|
|
|
493
495
|
installFailed: "Install failed: {e}",
|
|
494
496
|
installNotApplied: "Install did not take effect",
|
|
495
497
|
updatedOne: "Updated {n}; restart to take effect",
|
|
498
|
+
updatedHot: "Updated {n}; hot-applied in the browser, no restart needed",
|
|
496
499
|
updateFailed: "Update failed: {e}",
|
|
497
500
|
updateNotApplied: "Update did not take effect",
|
|
498
501
|
updatedMany: "Updated {n} plugin(s); restart to take effect",
|
|
502
|
+
updatedManyHot: "Updated {n} plugin(s); hot-applied, no restart needed",
|
|
499
503
|
commandTitle: "Run this command in a terminal",
|
|
500
504
|
commandHint: "The app is running and locked files cannot be replaced in-place. Close the app first, then run:",
|
|
501
505
|
commandCopy: "Copy command",
|
|
@@ -1143,6 +1147,11 @@ function CenterPanel({ variant = "section" }) {
|
|
|
1143
1147
|
setCopied(false);
|
|
1144
1148
|
return;
|
|
1145
1149
|
}
|
|
1150
|
+
if (value.hot === true) {
|
|
1151
|
+
showToast(t("updatedHot", { n: name }) + (value.durationMs !== void 0 ? `\uFF08${(value.durationMs / 1e3).toFixed(1)}s\uFF09` : ""), "ok", 5e3);
|
|
1152
|
+
refreshUpdates(true);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1146
1155
|
if (value.pending === true) {
|
|
1147
1156
|
pendingInstall.add(name);
|
|
1148
1157
|
readyPending.current.push(name);
|
|
@@ -1173,6 +1182,7 @@ function CenterPanel({ variant = "section" }) {
|
|
|
1173
1182
|
readyPending.current = [];
|
|
1174
1183
|
const commands = [];
|
|
1175
1184
|
let okCount = 0;
|
|
1185
|
+
let hotCount = 0;
|
|
1176
1186
|
const okNames = [];
|
|
1177
1187
|
const failures = [];
|
|
1178
1188
|
for (const u of updates) {
|
|
@@ -1186,6 +1196,11 @@ function CenterPanel({ variant = "section" }) {
|
|
|
1186
1196
|
commands.push(value.command);
|
|
1187
1197
|
continue;
|
|
1188
1198
|
}
|
|
1199
|
+
if (value.hot === true) {
|
|
1200
|
+
hotCount++;
|
|
1201
|
+
okNames.push(value.durationMs !== void 0 ? `${u.name}\uFF08${(value.durationMs / 1e3).toFixed(1)}s\uFF09` : u.name);
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1189
1204
|
if (value.pending === true) {
|
|
1190
1205
|
readyPending.current.push(u.name);
|
|
1191
1206
|
okNames.push(`${u.name}\uFF08\u5F85\u91CD\u542F\uFF09`);
|
|
@@ -1214,6 +1229,8 @@ function CenterPanel({ variant = "section" }) {
|
|
|
1214
1229
|
}
|
|
1215
1230
|
if (failures.length > 0) {
|
|
1216
1231
|
showToast(t("updateSummary", { a: okCount, b: failures.length, c: failures.join("\uFF1B") }), "error", 15e3);
|
|
1232
|
+
} else if (commands.length === 0 && readyPending.current.length === 0 && hotCount === okCount) {
|
|
1233
|
+
showToast(t("updatedManyHot", { n: okCount }) + `\uFF08${okNames.join("\u3001")}\uFF09`, "ok", 6e3);
|
|
1217
1234
|
} else if (commands.length === 0 && readyPending.current.length === 0) {
|
|
1218
1235
|
showToast(t("updatedMany", { n: okCount }) + `\uFF08${okNames.join("\u3001")}\uFF09`, "ok", 6e3);
|
|
1219
1236
|
}
|
|
@@ -1400,6 +1417,7 @@ function WhatsNewDialog() {
|
|
|
1400
1417
|
const updateNow = async () => {
|
|
1401
1418
|
setBusy(true);
|
|
1402
1419
|
let okCount = 0;
|
|
1420
|
+
let hotCount = 0;
|
|
1403
1421
|
const failures = [];
|
|
1404
1422
|
for (const u of whatsNewDigests) {
|
|
1405
1423
|
try {
|
|
@@ -1411,6 +1429,10 @@ function WhatsNewDialog() {
|
|
|
1411
1429
|
continue;
|
|
1412
1430
|
}
|
|
1413
1431
|
okCount++;
|
|
1432
|
+
if (value.hot === true) {
|
|
1433
|
+
hotCount++;
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1414
1436
|
if (value.pending === true || value.direct === true) {
|
|
1415
1437
|
pendingInstall.add(u.name);
|
|
1416
1438
|
markDoneUpdate({ name: u.name, fromVersion: u.fromVersion, toVersion: u.toVersion });
|
|
@@ -1421,7 +1443,7 @@ function WhatsNewDialog() {
|
|
|
1421
1443
|
}
|
|
1422
1444
|
setBusy(false);
|
|
1423
1445
|
if (failures.length === 0) {
|
|
1424
|
-
showToast(t("updatedMany", { n: okCount }), "ok", 6e3);
|
|
1446
|
+
showToast(hotCount === okCount ? t("updatedManyHot", { n: okCount }) : t("updatedMany", { n: okCount }), "ok", 6e3);
|
|
1425
1447
|
closeWhatsNew();
|
|
1426
1448
|
} else {
|
|
1427
1449
|
showToast(t("updateSummary", { a: okCount, b: failures.length, c: failures.join("\uFF1B") }), "error", 8e3);
|
package/dist/rpc.js
CHANGED
package/dist/update.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ export interface PnpmResult {
|
|
|
26
26
|
/** True when the update was installed DIRECTLY (files on disk now; a
|
|
27
27
|
* restart makes the running DSH pick them up), no locks were hit. */
|
|
28
28
|
direct?: boolean;
|
|
29
|
+
/** True when the update changed no host-side files (only the browser
|
|
30
|
+
* bundle, which client-hmr hot-swaps): no restart is needed. Absent when
|
|
31
|
+
* the package dir is missing or the snapshot could not be taken. */
|
|
32
|
+
hot?: boolean;
|
|
29
33
|
/** True when the update was only PREPARED (downloaded to the pending dir)
|
|
30
34
|
* and the real install is queued for the next SSiD/DSH startup. */
|
|
31
35
|
pending?: boolean;
|
|
@@ -84,6 +88,22 @@ export declare function pnpmCommandCandidates(profileDir: string): string[];
|
|
|
84
88
|
export declare function runPnpm(args: readonly string[], cwd: string): Promise<PnpmResult>;
|
|
85
89
|
/** Install a package into the web profile, mirroring `dsh plugin add` semantics. */
|
|
86
90
|
export declare function installPlugin(packageSpec: string, profileDir: string): Promise<PnpmResult>;
|
|
91
|
+
/** One host-side file identity: relative path + content hash. */
|
|
92
|
+
export interface FileIdentity {
|
|
93
|
+
path: string;
|
|
94
|
+
hash: string;
|
|
95
|
+
}
|
|
96
|
+
/** Hash every host-runtime file under a package dir, excluding the browser
|
|
97
|
+
* bundle (hot-swappable via client-hmr) and doc/type artifacts. The installed
|
|
98
|
+
* package lives inside the running profile's node_modules, so the diff tells
|
|
99
|
+
* whether the update touched only bundle files or also host code. The
|
|
100
|
+
* package.json version bump is normalized away — installed-version metadata
|
|
101
|
+
* changes on every update and has no runtime effect. Returns null when the
|
|
102
|
+
* dir is absent or unreadable.
|
|
103
|
+
*/
|
|
104
|
+
export declare function snapshotHostFiles(pkgDir: string): FileIdentity[] | null;
|
|
105
|
+
/** True when the two snapshots disagree (host-side code changed). */
|
|
106
|
+
export declare function hostFilesChanged(before: FileIdentity[], after: FileIdentity[]): boolean;
|
|
87
107
|
/**
|
|
88
108
|
* Update a package to the given version, mirroring `dsh plugin add <pkg>`.
|
|
89
109
|
* The exact version is required: with a bare package name, pnpm 11's
|
package/dist/update.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* first (many community repos ship no release/tag/CHANGELOG — verified §7.2).
|
|
5
5
|
*/
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
|
-
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
8
9
|
import { homedir } from 'node:os';
|
|
9
10
|
import { join } from 'node:path';
|
|
10
11
|
import { compareVersions, satisfies } from "./semver.js";
|
|
@@ -292,6 +293,125 @@ export async function installPlugin(packageSpec, profileDir) {
|
|
|
292
293
|
// `-w` is required: every profile ships a pnpm-workspace.yaml.
|
|
293
294
|
return runPnpm(['add', '-w', packageSpec], profileDir);
|
|
294
295
|
}
|
|
296
|
+
/** Resolve `exports["./client"]` exactly like client-modules does (string or
|
|
297
|
+
* { default }): the browser bundle path, relative to the package dir. */
|
|
298
|
+
function clientBundleRel(pkgDir) {
|
|
299
|
+
try {
|
|
300
|
+
const pkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
|
|
301
|
+
const exp = pkg.exports;
|
|
302
|
+
if (exp !== null && typeof exp === 'object') {
|
|
303
|
+
const client = exp['./client'];
|
|
304
|
+
if (typeof client === 'string')
|
|
305
|
+
return client.replace(/^\.\//, '');
|
|
306
|
+
if (client !== null && typeof client === 'object') {
|
|
307
|
+
const d = client.default;
|
|
308
|
+
if (typeof d === 'string')
|
|
309
|
+
return d.replace(/^\.\//, '');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch { /* unreadable package.json: fall through to name-based rules */ }
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
/** True when the file never needs a host restart: the browser bundle (exact
|
|
317
|
+
* exports path or legacy name), source maps, type declarations, docs/assets,
|
|
318
|
+
* images. Conservative direction: a missed exclusion only adds a restart
|
|
319
|
+
* (false negative), never removes one (false positive). */
|
|
320
|
+
function snapshotExcluded(rel, isDir, clientRels) {
|
|
321
|
+
// join() produces backslashes on Windows; normalize for set membership.
|
|
322
|
+
const norm = rel.replace(/\\/g, '/');
|
|
323
|
+
const name = norm.split('/').pop();
|
|
324
|
+
if (name === 'node_modules' || name === 'docs' || name === 'assets')
|
|
325
|
+
return true;
|
|
326
|
+
if (isDir)
|
|
327
|
+
return false;
|
|
328
|
+
if (clientRels.has(norm))
|
|
329
|
+
return true;
|
|
330
|
+
if (name === 'client.js' || name === 'client.mjs')
|
|
331
|
+
return true;
|
|
332
|
+
if (name.endsWith('.map') || name.endsWith('.d.ts'))
|
|
333
|
+
return true;
|
|
334
|
+
if (/[.](png|jpe?g|gif|webp|svg|ico)$/i.test(name))
|
|
335
|
+
return true;
|
|
336
|
+
if (/^(README|LICENSE|CHANGELOG|UNLICENSE)(\..*)?$/i.test(name))
|
|
337
|
+
return true;
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
/** Hash every host-runtime file under a package dir, excluding the browser
|
|
341
|
+
* bundle (hot-swappable via client-hmr) and doc/type artifacts. The installed
|
|
342
|
+
* package lives inside the running profile's node_modules, so the diff tells
|
|
343
|
+
* whether the update touched only bundle files or also host code. The
|
|
344
|
+
* package.json version bump is normalized away — installed-version metadata
|
|
345
|
+
* changes on every update and has no runtime effect. Returns null when the
|
|
346
|
+
* dir is absent or unreadable.
|
|
347
|
+
*/
|
|
348
|
+
export function snapshotHostFiles(pkgDir) {
|
|
349
|
+
try {
|
|
350
|
+
if (!existsSync(pkgDir) || !statSync(pkgDir).isDirectory())
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
const clientRels = new Set();
|
|
357
|
+
const clientRel = clientBundleRel(pkgDir);
|
|
358
|
+
if (clientRel !== null)
|
|
359
|
+
clientRels.add(clientRel);
|
|
360
|
+
const out = [];
|
|
361
|
+
const walk = (dir, prefix) => {
|
|
362
|
+
let entries = [];
|
|
363
|
+
try {
|
|
364
|
+
entries = readdirSync(dir);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
for (const name of entries) {
|
|
370
|
+
const full = join(dir, name);
|
|
371
|
+
const rel = prefix === '' ? name : join(prefix, name);
|
|
372
|
+
let st;
|
|
373
|
+
try {
|
|
374
|
+
st = statSync(full);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (snapshotExcluded(rel, st.isDirectory(), clientRels))
|
|
380
|
+
continue;
|
|
381
|
+
if (st.isDirectory()) {
|
|
382
|
+
walk(full, rel);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
let data = readFileSync(full);
|
|
387
|
+
if (name === 'package.json') {
|
|
388
|
+
// Installed-version metadata changes on every update; strip it so a
|
|
389
|
+
// pure bundle update still compares equal.
|
|
390
|
+
const pkg = JSON.parse(data.toString('utf8'));
|
|
391
|
+
delete pkg.version;
|
|
392
|
+
data = Buffer.from(JSON.stringify(pkg), 'utf8');
|
|
393
|
+
}
|
|
394
|
+
const hash = createHash('sha256').update(data).digest('hex');
|
|
395
|
+
out.push({ path: rel, hash });
|
|
396
|
+
}
|
|
397
|
+
catch { /* unreadable file: ignore */ }
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
walk(pkgDir, '');
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
/** True when the two snapshots disagree (host-side code changed). */
|
|
404
|
+
export function hostFilesChanged(before, after) {
|
|
405
|
+
const a = new Map(before.map(i => [i.path, i.hash]));
|
|
406
|
+
const b = new Map(after.map(i => [i.path, i.hash]));
|
|
407
|
+
if (a.size !== b.size)
|
|
408
|
+
return true;
|
|
409
|
+
for (const [path, hash] of a) {
|
|
410
|
+
if (b.get(path) !== hash)
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
295
415
|
/**
|
|
296
416
|
* Update a package to the given version, mirroring `dsh plugin add <pkg>`.
|
|
297
417
|
* The exact version is required: with a bare package name, pnpm 11's
|
|
@@ -301,5 +421,14 @@ export async function installPlugin(packageSpec, profileDir) {
|
|
|
301
421
|
* `minimumReleaseAgeExclude` itself (2026-08-18, reproduced in-process).
|
|
302
422
|
*/
|
|
303
423
|
export async function updatePlugin(packageName, version, profileDir) {
|
|
304
|
-
|
|
424
|
+
const pkgDir = join(profileDir, 'node_modules', packageName);
|
|
425
|
+
const before = snapshotHostFiles(pkgDir);
|
|
426
|
+
const result = await runPnpm(['add', '-w', `${packageName}@${version}`], profileDir);
|
|
427
|
+
if (result.ok && before !== null && before.length > 0) {
|
|
428
|
+
const after = snapshotHostFiles(pkgDir);
|
|
429
|
+
if (after !== null && !hostFilesChanged(before, after)) {
|
|
430
|
+
result.hot = true;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return result;
|
|
305
434
|
}
|
package/package.json
CHANGED