* test(#3902): packListToPathSet must resolve npm 12's object-keyed pack --json shape (failing first) * fix(#3902): resolve both npm pack --json shapes — the packaging guard stays armed on Node 26 npm 12 (bundled with Node 26) emits an object keyed by package name where npm <=11 emitted an array; parsed[0] was undefined, the before() hook threw, and all 6 tests in the packaging guard — including the two 'does NOT ship' guards that stop repo-only CI tooling leaking into the published package — went dark for contributors on Node 26. CONTRIBUTING pins Node 24 as the floor but requires Node 26 compatibility for code AND tests; this was the test's parse, not the code. packListToPathSet now resolves either shape and fails loud on anything else — a silently-empty set would be the exact dark-guard failure mode this guard exists to prevent. * fix(#3902): review fold-in — a multi-key object fails loud Single-package assumption documented and enforced: npm 12 emits exactly one key for this repo (no workspaces — verified); if workspaces were ever adopted, first-key selection would silently validate one package's file list and recreate the exact silent-disarm this fix kills. * chore(#3902): changeset fragment (pr number backfilled after PR creation) * chore(#3902): backfill changeset PR number (4064) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/serene-goats-sprint.md
Normal file
5
.changeset/serene-goats-sprint.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 4064
|
||||
---
|
||||
**The packaging guard stays armed on npm 12 (Node 26)** — npm 12 emits pack --json as an object keyed by package name, so parsed[0] was undefined, the before() hook threw, and all 6 packaging-guard tests (including both does-NOT-ship gates) went dark for Node 26 contributors (#3902)
|
||||
@@ -46,7 +46,37 @@ function resolveTarballFiles() {
|
||||
timeout: 120_000,
|
||||
});
|
||||
const parsed = JSON.parse(raw);
|
||||
return new Set(parsed[0].files.map((f) => f.path.replace(/\\/g, '/')));
|
||||
return packListToPathSet(parsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure projection from `npm pack --json` output to the tarball path set —
|
||||
* extracted so the SHAPE contract is unit-testable without running npm.
|
||||
*/
|
||||
function packListToPathSet(parsed) {
|
||||
// #3902: npm <=11 emits an ARRAY of pack results; npm 12 (bundled with
|
||||
// Node 26) emits an OBJECT keyed by package name. parsed[0] is undefined on
|
||||
// the object shape — which threw in this file's before() hook and silently
|
||||
// disabled the whole packaging guard, including both `does NOT ship`
|
||||
// assertions. Resolve either shape; anything else fails loud.
|
||||
// Single-package assumption: this repo has no workspaces, so npm 12 emits
|
||||
// exactly ONE key. If workspaces are ever adopted, first-key would silently
|
||||
// validate only one package — recreate the silent-disarm this fix kills —
|
||||
// so a multi-key object fails loud instead.
|
||||
let entry;
|
||||
if (Array.isArray(parsed)) {
|
||||
entry = parsed[0];
|
||||
} else {
|
||||
const keys = Object.keys(parsed);
|
||||
if (keys.length !== 1) {
|
||||
throw new Error(`npm pack --json emitted ${keys.length} package keys; expected exactly 1 for this single-package repo`);
|
||||
}
|
||||
entry = parsed[keys[0]];
|
||||
}
|
||||
if (!entry || !Array.isArray(entry.files)) {
|
||||
throw new Error(`npm pack --json returned an unrecognized shape: ${Object.prototype.toString.call(parsed)}`);
|
||||
}
|
||||
return new Set(entry.files.map((f) => f.path.replace(/\\/g, '/')));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,3 +228,36 @@ describe('#2858 — shipped scripts require only shipped paths', () => {
|
||||
`a sibling require within scripts/ must classify as 'shipped'; got '${classification}'`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// ─── #3902: npm 12's pack --json shape must resolve like npm <=11's ──────────
|
||||
|
||||
describe('#3902 packListToPathSet resolves both npm pack --json shapes', () => {
|
||||
const FILES = [{ path: 'gsd-core/bin/gsd-tools.cjs' }, { path: 'lib/Backslash\\Case.cjs' }];
|
||||
|
||||
test('npm <=11 array shape', () => {
|
||||
const set = packListToPathSet([{ files: FILES }]);
|
||||
assert.ok(set.has('gsd-core/bin/gsd-tools.cjs'));
|
||||
assert.ok(set.has('lib/Backslash/Case.cjs'), 'windows separators normalized');
|
||||
});
|
||||
|
||||
test('npm 12 (Node 26) object-keyed shape', () => {
|
||||
// npm 12 emits { "<pkg-name>": { files: [...] } } — parsed[0] is undefined
|
||||
// there, which used to throw in the before() hook and silently disable
|
||||
// the whole packaging guard, including both `does NOT ship` assertions.
|
||||
const set = packListToPathSet({ '@opengsd/gsd-core': { files: FILES } });
|
||||
assert.ok(set.has('gsd-core/bin/gsd-tools.cjs'));
|
||||
assert.ok(set.has('lib/Backslash/Case.cjs'));
|
||||
});
|
||||
|
||||
test('an unrecognized shape fails loud, never silently-empty', () => {
|
||||
assert.throws(() => packListToPathSet({ weird: true }));
|
||||
});
|
||||
|
||||
test('a multi-key object (workspaces) fails loud — first-key would silently validate one package', () => {
|
||||
assert.throws(() => packListToPathSet({
|
||||
'pkg-a': { files: FILES },
|
||||
'pkg-b': { files: FILES },
|
||||
}), /exactly 1/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user