* 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,
|
timeout: 120_000,
|
||||||
});
|
});
|
||||||
const parsed = JSON.parse(raw);
|
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}'`);
|
`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