dshmarket 1.21.1 → 1.21.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/lib/routes.js +24 -6
- package/lib/types/verify.d.ts +36 -12
- package/lib/verify.js +63 -2
- package/package.json +1 -1
- package/src/routes.ts +30 -6
- package/src/verify.ts +67 -2
package/lib/routes.js
CHANGED
|
@@ -31,7 +31,7 @@ import { checkUpdates, fetchNpmLatest, invalidateUpdates, isUpgrade, latestPubli
|
|
|
31
31
|
import { createThemeManager } from './themes.js';
|
|
32
32
|
import { readJsonBody, sameOrigin, sendJson } from './http.js';
|
|
33
33
|
import { detectedSupervisor, restartAllowed, scheduleRestart, servingPort, trustedRestartRequest, trustedDownloadRequest } from './restart.js';
|
|
34
|
-
import { activationAfterReplace, checkClientBundle, hasHostHalf, verifyActivation } from './verify.js';
|
|
34
|
+
import { activationAfterReplace, brokenClientBundles, checkClientBundle, hasHostHalf, newlyBrokenBundles, verifyActivation } from './verify.js';
|
|
35
35
|
import { carrierDisableIds, disableRow, enableRow, findUserPatchPath, isProtectedModule, packagePatchFlags, readUserPatchState, removeRowBlocks, rowIdsForPackage, } from './patch.js';
|
|
36
36
|
import { createProfileBackup, downloadWebdav, MAX_BACKUP_BYTES, mergeRestoreManifest, restoreProfileBackup, unportableDeps, uploadWebdav, } from './backup.js';
|
|
37
37
|
import { createGist, fitsGistLimit, GistError, gistErrorCode, parseGistId, readGist, resolveGistTokenSource, updateGist, verifyGistToken, } from './gist.js';
|
|
@@ -1376,6 +1376,12 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
|
|
|
1376
1376
|
// ghost/bumped entries that break every later pnpm run.
|
|
1377
1377
|
pendingRollbacks.clear();
|
|
1378
1378
|
const compatibilityBefore = assessProfile(config.profile, activeProfileDir);
|
|
1379
|
+
// pnpm re-extracts the whole tree on any operation, so a plugin
|
|
1380
|
+
// nobody touched can come back pristine-and-broken, or lose a
|
|
1381
|
+
// patch that was holding it together (#222). Only what THIS run
|
|
1382
|
+
// broke is attributable to it, so the profile is swept before as
|
|
1383
|
+
// well as after.
|
|
1384
|
+
const bundlesBefore = brokenClientBundles(config.profile, activeProfileDir);
|
|
1379
1385
|
const manifestBefore = readManifestDeps(config.profile, activeProfileDir);
|
|
1380
1386
|
const result = await runPlugin(config.profile, addArgs);
|
|
1381
1387
|
const cancelled = result.cancelled;
|
|
@@ -1458,7 +1464,10 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
|
|
|
1458
1464
|
// See the install route: an update is the operation the #222
|
|
1459
1465
|
// report actually hit.
|
|
1460
1466
|
const bundleCheck = checkClientBundle(config.profile, name, activeProfileDir);
|
|
1461
|
-
const brokenBundles =
|
|
1467
|
+
const brokenBundles = newlyBrokenBundles(bundlesBefore, [
|
|
1468
|
+
...(bundleCheck.ok ? [] : [{ name, reason: bundleCheck.reason ?? 'parse failed' }]),
|
|
1469
|
+
...brokenClientBundles(config.profile, activeProfileDir),
|
|
1470
|
+
].filter((entry, index, all) => all.findIndex(other => other.name === entry.name) === index));
|
|
1462
1471
|
if (risks.length > 0 || shadowed.length > 0 || brokenBundles.length > 0) {
|
|
1463
1472
|
compatibility = {
|
|
1464
1473
|
code: 'soft-incompatible',
|
|
@@ -2240,6 +2249,12 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
|
|
|
2240
2249
|
before.delete(retryAlias);
|
|
2241
2250
|
pendingRollbacks.clear();
|
|
2242
2251
|
const compatibilityBefore = assessProfile(config.profile, activeProfileDir);
|
|
2252
|
+
// pnpm re-extracts the whole tree on any operation, so a plugin
|
|
2253
|
+
// nobody touched can come back pristine-and-broken, or lose a
|
|
2254
|
+
// patch that was holding it together (#222). Only what THIS run
|
|
2255
|
+
// broke is attributable to it, so the profile is swept before as
|
|
2256
|
+
// well as after.
|
|
2257
|
+
const bundlesBefore = brokenClientBundles(config.profile, activeProfileDir);
|
|
2243
2258
|
// RAW manifest snapshot for failure rollback (#65): pnpm writes
|
|
2244
2259
|
// package.json before the build-script check / registry fetches
|
|
2245
2260
|
// run, so a hard-failed add leaves ghost dependencies that break
|
|
@@ -2337,10 +2352,13 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
|
|
|
2337
2352
|
// one half-written or patch-mangled, and today that surfaces
|
|
2338
2353
|
// as a blank settings page long after the install reported
|
|
2339
2354
|
// success, with nothing connecting the two.
|
|
2340
|
-
const brokenBundles =
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2355
|
+
const brokenBundles = newlyBrokenBundles(bundlesBefore, [
|
|
2356
|
+
...addedPackages
|
|
2357
|
+
.map(pkg => ({ name: pkg, check: checkClientBundle(config.profile, pkg, activeProfileDir) }))
|
|
2358
|
+
.filter(entry => !entry.check.ok)
|
|
2359
|
+
.map(entry => ({ name: entry.name, reason: entry.check.reason ?? 'parse failed' })),
|
|
2360
|
+
...brokenClientBundles(config.profile, activeProfileDir),
|
|
2361
|
+
].filter((entry, index, all) => all.findIndex(other => other.name === entry.name) === index));
|
|
2344
2362
|
if (risks.length > 0 || shadowed.length > 0 || brokenBundles.length > 0) {
|
|
2345
2363
|
compatibility = {
|
|
2346
2364
|
code: 'soft-incompatible',
|
package/lib/types/verify.d.ts
CHANGED
|
@@ -102,19 +102,43 @@ export interface BundleCheck {
|
|
|
102
102
|
reason: string | null;
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
|
-
*
|
|
105
|
+
* Every installed plugin whose client bundle will not parse (#222 by
|
|
106
|
+
* @MicroMilo).
|
|
106
107
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
108
|
+
* The per-package check above only ever looked at what an operation added,
|
|
109
|
+
* which misses the failure that was actually reported: pnpm re-extracts the
|
|
110
|
+
* WHOLE tree on any install, so updating one plugin can restore another
|
|
111
|
+
* plugin's pristine — and broken — bundle, or fail to re-apply a patch that
|
|
112
|
+
* was holding it together. The damage then surfaces at the next boot as
|
|
113
|
+
* "failed to load plugins", with nothing connecting it to the install that
|
|
114
|
+
* caused it.
|
|
111
115
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
116
|
+
* Cheap enough to run on every operation: 0.40ms per plugin including the
|
|
117
|
+
* read (measured on a 385KB bundle), so a 30-plugin profile costs ~12ms
|
|
118
|
+
* against an install that takes seconds.
|
|
119
|
+
*
|
|
120
|
+
* Silent on ESM bundles, like the per-package check it calls — see there for
|
|
121
|
+
* why. Widening the sweep is exactly what would have turned that one false
|
|
122
|
+
* "corrupt" into one per ESM plugin in the profile.
|
|
123
|
+
*
|
|
124
|
+
* Callers compare a before-list with an after-list rather than reporting
|
|
125
|
+
* this one directly — a profile can carry a broken bundle indefinitely, and
|
|
126
|
+
* re-reporting a problem the user already had would put them in front of
|
|
127
|
+
* something this operation did not cause and cannot undo.
|
|
119
128
|
*/
|
|
129
|
+
export declare function brokenClientBundles(profile: string, explicitDir?: string): {
|
|
130
|
+
name: string;
|
|
131
|
+
reason: string;
|
|
132
|
+
}[];
|
|
133
|
+
/** Bundles broken after an operation that were intact before it. */
|
|
134
|
+
export declare function newlyBrokenBundles(before: readonly {
|
|
135
|
+
name: string;
|
|
136
|
+
reason: string;
|
|
137
|
+
}[], after: readonly {
|
|
138
|
+
name: string;
|
|
139
|
+
reason: string;
|
|
140
|
+
}[]): {
|
|
141
|
+
name: string;
|
|
142
|
+
reason: string;
|
|
143
|
+
}[];
|
|
120
144
|
export declare function checkClientBundle(profile: string, name: string, explicitDir?: string): BundleCheck;
|
package/lib/verify.js
CHANGED
|
@@ -27,7 +27,7 @@ import { readFileSync } from 'node:fs';
|
|
|
27
27
|
import { Script } from 'node:vm';
|
|
28
28
|
import { join } from 'node:path';
|
|
29
29
|
import { listHotMounts, parseSimplePatch } from './hot.js';
|
|
30
|
-
import { bundlePatchInsertedIds, hasDshManifest, hasLoadableEntry, profileDir } from './profile.js';
|
|
30
|
+
import { bundlePatchInsertedIds, hasDshManifest, hasLoadableEntry, profileDir, readInstalled } from './profile.js';
|
|
31
31
|
/** The profile manifest's `dsh.profile.bundles` — what the CLI reconciled. */
|
|
32
32
|
function readBundles(profile, explicitDir) {
|
|
33
33
|
try {
|
|
@@ -328,6 +328,55 @@ export function clientBundlePath(exportsField, depth = 0) {
|
|
|
328
328
|
* silent, because a false "your plugin is corrupt" is the one outcome worse
|
|
329
329
|
* than not checking.
|
|
330
330
|
*/
|
|
331
|
+
/**
|
|
332
|
+
* V8's wording when a classic-script parse trips over module syntax.
|
|
333
|
+
*
|
|
334
|
+
* Matched on the message because there is no flag-free way to compile a
|
|
335
|
+
* module here (`SourceTextModule` needs --experimental-vm-modules, which the
|
|
336
|
+
* host process does not set). If V8 ever rewords these, the failure mode is
|
|
337
|
+
* the false positive we had before rather than a missed real break — the
|
|
338
|
+
* safer direction of the two.
|
|
339
|
+
*/
|
|
340
|
+
const MODULE_SYNTAX_ERROR = /Unexpected token 'export'|Cannot use import statement outside a module|await is only valid in async functions and the top level bodies of modules/;
|
|
341
|
+
/**
|
|
342
|
+
* Every installed plugin whose client bundle will not parse (#222 by
|
|
343
|
+
* @MicroMilo).
|
|
344
|
+
*
|
|
345
|
+
* The per-package check above only ever looked at what an operation added,
|
|
346
|
+
* which misses the failure that was actually reported: pnpm re-extracts the
|
|
347
|
+
* WHOLE tree on any install, so updating one plugin can restore another
|
|
348
|
+
* plugin's pristine — and broken — bundle, or fail to re-apply a patch that
|
|
349
|
+
* was holding it together. The damage then surfaces at the next boot as
|
|
350
|
+
* "failed to load plugins", with nothing connecting it to the install that
|
|
351
|
+
* caused it.
|
|
352
|
+
*
|
|
353
|
+
* Cheap enough to run on every operation: 0.40ms per plugin including the
|
|
354
|
+
* read (measured on a 385KB bundle), so a 30-plugin profile costs ~12ms
|
|
355
|
+
* against an install that takes seconds.
|
|
356
|
+
*
|
|
357
|
+
* Silent on ESM bundles, like the per-package check it calls — see there for
|
|
358
|
+
* why. Widening the sweep is exactly what would have turned that one false
|
|
359
|
+
* "corrupt" into one per ESM plugin in the profile.
|
|
360
|
+
*
|
|
361
|
+
* Callers compare a before-list with an after-list rather than reporting
|
|
362
|
+
* this one directly — a profile can carry a broken bundle indefinitely, and
|
|
363
|
+
* re-reporting a problem the user already had would put them in front of
|
|
364
|
+
* something this operation did not cause and cannot undo.
|
|
365
|
+
*/
|
|
366
|
+
export function brokenClientBundles(profile, explicitDir) {
|
|
367
|
+
const broken = [];
|
|
368
|
+
for (const name of Object.keys(readInstalled(profile, explicitDir))) {
|
|
369
|
+
const check = checkClientBundle(profile, name, explicitDir);
|
|
370
|
+
if (!check.ok)
|
|
371
|
+
broken.push({ name, reason: check.reason ?? 'parse failed' });
|
|
372
|
+
}
|
|
373
|
+
return broken;
|
|
374
|
+
}
|
|
375
|
+
/** Bundles broken after an operation that were intact before it. */
|
|
376
|
+
export function newlyBrokenBundles(before, after) {
|
|
377
|
+
const seen = new Set(before.map(entry => entry.name));
|
|
378
|
+
return after.filter(entry => !seen.has(entry.name));
|
|
379
|
+
}
|
|
331
380
|
export function checkClientBundle(profile, name, explicitDir) {
|
|
332
381
|
const root = join(profileDir(profile, explicitDir), 'node_modules', name);
|
|
333
382
|
let manifest;
|
|
@@ -360,6 +409,18 @@ export function checkClientBundle(profile, name, explicitDir) {
|
|
|
360
409
|
return { ok: true, reason: null };
|
|
361
410
|
}
|
|
362
411
|
catch (error) {
|
|
363
|
-
|
|
412
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
413
|
+
// `new Script` compiles a CLASSIC script, so perfectly valid module
|
|
414
|
+
// syntax is a SyntaxError to it: `export`, `import`, and top-level
|
|
415
|
+
// `await` all throw. This package ships CJS, which is why nobody noticed
|
|
416
|
+
// — but a plugin whose client bundle is ESM was being told its file was
|
|
417
|
+
// corrupt, and offered a rollback for a file that is fine.
|
|
418
|
+
//
|
|
419
|
+
// That is the exact outcome this check's own contract forbids: a false
|
|
420
|
+
// "your plugin is corrupt" is worse than not checking. Module syntax
|
|
421
|
+
// means "cannot judge with this parser", not "broken".
|
|
422
|
+
if (MODULE_SYNTAX_ERROR.test(message))
|
|
423
|
+
return { ok: true, reason: null };
|
|
424
|
+
return { ok: false, reason: message };
|
|
364
425
|
}
|
|
365
426
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dshmarket",
|
|
3
3
|
"description": "Visual plugin market inside DeepSeek Harness — browse, search, and one-click install community plugins. · DSH 可视化插件市场:逛一逛,点一下,装好。",
|
|
4
|
-
"version": "1.21.
|
|
4
|
+
"version": "1.21.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
package/src/routes.ts
CHANGED
|
@@ -39,7 +39,7 @@ import { checkUpdates, fetchNpmLatest, invalidateUpdates, isUpgrade, latestPubli
|
|
|
39
39
|
import { createThemeManager, type LoaderEntry } from './themes.ts'
|
|
40
40
|
import { readJsonBody, sameOrigin, sendJson } from './http.ts'
|
|
41
41
|
import { detectedSupervisor, restartAllowed, scheduleRestart, servingPort, trustedRestartRequest, trustedDownloadRequest } from './restart.ts'
|
|
42
|
-
import { activationAfterReplace, checkClientBundle, hasHostHalf, verifyActivation } from './verify.ts'
|
|
42
|
+
import { activationAfterReplace, brokenClientBundles, checkClientBundle, hasHostHalf, newlyBrokenBundles, verifyActivation } from './verify.ts'
|
|
43
43
|
import {
|
|
44
44
|
carrierDisableIds, disableRow, enableRow, findUserPatchPath, isProtectedModule, packagePatchFlags,
|
|
45
45
|
readUserPatchState, removeRowBlocks, rowIdsForPackage,
|
|
@@ -1423,6 +1423,12 @@ export function mountMarketRoutes(
|
|
|
1423
1423
|
// ghost/bumped entries that break every later pnpm run.
|
|
1424
1424
|
pendingRollbacks.clear()
|
|
1425
1425
|
const compatibilityBefore = assessProfile(config.profile, activeProfileDir)
|
|
1426
|
+
// pnpm re-extracts the whole tree on any operation, so a plugin
|
|
1427
|
+
// nobody touched can come back pristine-and-broken, or lose a
|
|
1428
|
+
// patch that was holding it together (#222). Only what THIS run
|
|
1429
|
+
// broke is attributable to it, so the profile is swept before as
|
|
1430
|
+
// well as after.
|
|
1431
|
+
const bundlesBefore = brokenClientBundles(config.profile, activeProfileDir)
|
|
1426
1432
|
const manifestBefore = readManifestDeps(config.profile, activeProfileDir)
|
|
1427
1433
|
const result = await runPlugin(config.profile, addArgs)
|
|
1428
1434
|
const cancelled = result.cancelled
|
|
@@ -1508,7 +1514,13 @@ export function mountMarketRoutes(
|
|
|
1508
1514
|
// See the install route: an update is the operation the #222
|
|
1509
1515
|
// report actually hit.
|
|
1510
1516
|
const bundleCheck = checkClientBundle(config.profile, name, activeProfileDir)
|
|
1511
|
-
const brokenBundles =
|
|
1517
|
+
const brokenBundles = newlyBrokenBundles(
|
|
1518
|
+
bundlesBefore,
|
|
1519
|
+
[
|
|
1520
|
+
...(bundleCheck.ok ? [] : [{ name, reason: bundleCheck.reason ?? 'parse failed' }]),
|
|
1521
|
+
...brokenClientBundles(config.profile, activeProfileDir),
|
|
1522
|
+
].filter((entry, index, all) => all.findIndex(other => other.name === entry.name) === index),
|
|
1523
|
+
)
|
|
1512
1524
|
if (risks.length > 0 || shadowed.length > 0 || brokenBundles.length > 0) {
|
|
1513
1525
|
compatibility = {
|
|
1514
1526
|
code: 'soft-incompatible',
|
|
@@ -2294,6 +2306,12 @@ export function mountMarketRoutes(
|
|
|
2294
2306
|
if (retryAlias !== null) before.delete(retryAlias)
|
|
2295
2307
|
pendingRollbacks.clear()
|
|
2296
2308
|
const compatibilityBefore = assessProfile(config.profile, activeProfileDir)
|
|
2309
|
+
// pnpm re-extracts the whole tree on any operation, so a plugin
|
|
2310
|
+
// nobody touched can come back pristine-and-broken, or lose a
|
|
2311
|
+
// patch that was holding it together (#222). Only what THIS run
|
|
2312
|
+
// broke is attributable to it, so the profile is swept before as
|
|
2313
|
+
// well as after.
|
|
2314
|
+
const bundlesBefore = brokenClientBundles(config.profile, activeProfileDir)
|
|
2297
2315
|
// RAW manifest snapshot for failure rollback (#65): pnpm writes
|
|
2298
2316
|
// package.json before the build-script check / registry fetches
|
|
2299
2317
|
// run, so a hard-failed add leaves ghost dependencies that break
|
|
@@ -2386,10 +2404,16 @@ export function mountMarketRoutes(
|
|
|
2386
2404
|
// one half-written or patch-mangled, and today that surfaces
|
|
2387
2405
|
// as a blank settings page long after the install reported
|
|
2388
2406
|
// success, with nothing connecting the two.
|
|
2389
|
-
const brokenBundles =
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2407
|
+
const brokenBundles = newlyBrokenBundles(
|
|
2408
|
+
bundlesBefore,
|
|
2409
|
+
[
|
|
2410
|
+
...addedPackages
|
|
2411
|
+
.map(pkg => ({ name: pkg, check: checkClientBundle(config.profile, pkg, activeProfileDir) }))
|
|
2412
|
+
.filter(entry => !entry.check.ok)
|
|
2413
|
+
.map(entry => ({ name: entry.name, reason: entry.check.reason ?? 'parse failed' })),
|
|
2414
|
+
...brokenClientBundles(config.profile, activeProfileDir),
|
|
2415
|
+
].filter((entry, index, all) => all.findIndex(other => other.name === entry.name) === index),
|
|
2416
|
+
)
|
|
2393
2417
|
if (risks.length > 0 || shadowed.length > 0 || brokenBundles.length > 0) {
|
|
2394
2418
|
compatibility = {
|
|
2395
2419
|
code: 'soft-incompatible',
|
package/src/verify.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { readFileSync } from 'node:fs'
|
|
|
28
28
|
import { Script } from 'node:vm'
|
|
29
29
|
import { join } from 'node:path'
|
|
30
30
|
import { listHotMounts, parseSimplePatch } from './hot.ts'
|
|
31
|
-
import { bundlePatchInsertedIds, hasDshManifest, hasLoadableEntry, profileDir } from './profile.ts'
|
|
31
|
+
import { bundlePatchInsertedIds, hasDshManifest, hasLoadableEntry, profileDir, readInstalled } from './profile.ts'
|
|
32
32
|
|
|
33
33
|
export type ActivationState = 'live' | 'restart' | 'inert' | 'broken' | 'missing' | 'disabled'
|
|
34
34
|
|
|
@@ -367,6 +367,60 @@ export interface BundleCheck {
|
|
|
367
367
|
* silent, because a false "your plugin is corrupt" is the one outcome worse
|
|
368
368
|
* than not checking.
|
|
369
369
|
*/
|
|
370
|
+
/**
|
|
371
|
+
* V8's wording when a classic-script parse trips over module syntax.
|
|
372
|
+
*
|
|
373
|
+
* Matched on the message because there is no flag-free way to compile a
|
|
374
|
+
* module here (`SourceTextModule` needs --experimental-vm-modules, which the
|
|
375
|
+
* host process does not set). If V8 ever rewords these, the failure mode is
|
|
376
|
+
* the false positive we had before rather than a missed real break — the
|
|
377
|
+
* safer direction of the two.
|
|
378
|
+
*/
|
|
379
|
+
const MODULE_SYNTAX_ERROR = /Unexpected token 'export'|Cannot use import statement outside a module|await is only valid in async functions and the top level bodies of modules/
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Every installed plugin whose client bundle will not parse (#222 by
|
|
383
|
+
* @MicroMilo).
|
|
384
|
+
*
|
|
385
|
+
* The per-package check above only ever looked at what an operation added,
|
|
386
|
+
* which misses the failure that was actually reported: pnpm re-extracts the
|
|
387
|
+
* WHOLE tree on any install, so updating one plugin can restore another
|
|
388
|
+
* plugin's pristine — and broken — bundle, or fail to re-apply a patch that
|
|
389
|
+
* was holding it together. The damage then surfaces at the next boot as
|
|
390
|
+
* "failed to load plugins", with nothing connecting it to the install that
|
|
391
|
+
* caused it.
|
|
392
|
+
*
|
|
393
|
+
* Cheap enough to run on every operation: 0.40ms per plugin including the
|
|
394
|
+
* read (measured on a 385KB bundle), so a 30-plugin profile costs ~12ms
|
|
395
|
+
* against an install that takes seconds.
|
|
396
|
+
*
|
|
397
|
+
* Silent on ESM bundles, like the per-package check it calls — see there for
|
|
398
|
+
* why. Widening the sweep is exactly what would have turned that one false
|
|
399
|
+
* "corrupt" into one per ESM plugin in the profile.
|
|
400
|
+
*
|
|
401
|
+
* Callers compare a before-list with an after-list rather than reporting
|
|
402
|
+
* this one directly — a profile can carry a broken bundle indefinitely, and
|
|
403
|
+
* re-reporting a problem the user already had would put them in front of
|
|
404
|
+
* something this operation did not cause and cannot undo.
|
|
405
|
+
*/
|
|
406
|
+
export function brokenClientBundles(profile: string, explicitDir?: string): { name: string; reason: string }[] {
|
|
407
|
+
const broken: { name: string; reason: string }[] = []
|
|
408
|
+
for (const name of Object.keys(readInstalled(profile, explicitDir))) {
|
|
409
|
+
const check = checkClientBundle(profile, name, explicitDir)
|
|
410
|
+
if (!check.ok) broken.push({ name, reason: check.reason ?? 'parse failed' })
|
|
411
|
+
}
|
|
412
|
+
return broken
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Bundles broken after an operation that were intact before it. */
|
|
416
|
+
export function newlyBrokenBundles(
|
|
417
|
+
before: readonly { name: string; reason: string }[],
|
|
418
|
+
after: readonly { name: string; reason: string }[],
|
|
419
|
+
): { name: string; reason: string }[] {
|
|
420
|
+
const seen = new Set(before.map(entry => entry.name))
|
|
421
|
+
return after.filter(entry => !seen.has(entry.name))
|
|
422
|
+
}
|
|
423
|
+
|
|
370
424
|
export function checkClientBundle(profile: string, name: string, explicitDir?: string): BundleCheck {
|
|
371
425
|
const root = join(profileDir(profile, explicitDir), 'node_modules', name)
|
|
372
426
|
let manifest: { dsh?: { client?: unknown }; exports?: unknown }
|
|
@@ -394,6 +448,17 @@ export function checkClientBundle(profile: string, name: string, explicitDir?: s
|
|
|
394
448
|
new Script(source, { filename: relative })
|
|
395
449
|
return { ok: true, reason: null }
|
|
396
450
|
} catch (error) {
|
|
397
|
-
|
|
451
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
452
|
+
// `new Script` compiles a CLASSIC script, so perfectly valid module
|
|
453
|
+
// syntax is a SyntaxError to it: `export`, `import`, and top-level
|
|
454
|
+
// `await` all throw. This package ships CJS, which is why nobody noticed
|
|
455
|
+
// — but a plugin whose client bundle is ESM was being told its file was
|
|
456
|
+
// corrupt, and offered a rollback for a file that is fine.
|
|
457
|
+
//
|
|
458
|
+
// That is the exact outcome this check's own contract forbids: a false
|
|
459
|
+
// "your plugin is corrupt" is worse than not checking. Module syntax
|
|
460
|
+
// means "cannot judge with this parser", not "broken".
|
|
461
|
+
if (MODULE_SYNTAX_ERROR.test(message)) return { ok: true, reason: null }
|
|
462
|
+
return { ok: false, reason: message }
|
|
398
463
|
}
|
|
399
464
|
}
|