@xemahq/repo-build-tooling 0.3.0 → 0.5.0
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/repo-build-tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Dev-time build tooling shared by every Xema repository. Ships as plain ESM with zero dependencies so the published artifact is the reviewed source.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Neuralchowder Inc. <developer@xema.dev> (https://xema.dev)",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
39
39
|
import { execFileSync } from 'node:child_process';
|
|
40
40
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
41
|
-
import { join
|
|
41
|
+
import { join } from 'node:path';
|
|
42
42
|
import { pathToFileURL } from 'node:url';
|
|
43
43
|
|
|
44
44
|
const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies'];
|
|
@@ -75,19 +75,33 @@ export function satisfies(version, range) {
|
|
|
75
75
|
return v[0] === base[0];
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Every `package.json` in the repository, EXCLUDING `node_modules`, `dist` and
|
|
80
|
+
* `.git`.
|
|
81
|
+
*
|
|
82
|
+
* `-prune` rather than `-not -path`, and the difference is not a style
|
|
83
|
+
* preference. A `-not -path` exclusion on a node_modules glob FILTERS the
|
|
84
|
+
* results while still
|
|
85
|
+
* DESCENDING into every one of them, so this walked hundreds of thousands of
|
|
86
|
+
* installed files to discard all of them — and, because it descended, it raced
|
|
87
|
+
* anything writing there. Measured 2026-09-18 on xema-base: the walk entered
|
|
88
|
+
* `node_modules/.cache/xema-declared-kernel/.staging-<version>-XXXX/`, a
|
|
89
|
+
* temporary directory another step deleted mid-walk, `find` exited non-zero,
|
|
90
|
+
* and `execFileSync` threw. The gate did not report a violation — it CRASHED,
|
|
91
|
+
* which reads in CI as a failing check rather than an absent one.
|
|
92
|
+
*
|
|
93
|
+
* Pruning cannot race what it never enters.
|
|
94
|
+
*/
|
|
95
|
+
export function listManifests(root = REPO_ROOT) {
|
|
79
96
|
return execFileSync(
|
|
80
97
|
'find',
|
|
81
98
|
[
|
|
82
|
-
|
|
83
|
-
'-name',
|
|
84
|
-
'
|
|
85
|
-
'-
|
|
86
|
-
'-
|
|
87
|
-
'
|
|
88
|
-
'-not',
|
|
89
|
-
'-path',
|
|
90
|
-
'*/dist/*',
|
|
99
|
+
root,
|
|
100
|
+
'(', '-name', 'node_modules', '-o', '-name', 'dist', '-o', '-name', '.git', ')',
|
|
101
|
+
'-prune',
|
|
102
|
+
'-o',
|
|
103
|
+
'-name', 'package.json',
|
|
104
|
+
'-print',
|
|
91
105
|
],
|
|
92
106
|
{ encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 },
|
|
93
107
|
)
|
|
@@ -131,16 +145,30 @@ export function findViolations({ rewrite = false } = {}) {
|
|
|
131
145
|
}
|
|
132
146
|
if (mutated) writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
133
147
|
}
|
|
134
|
-
return violations;
|
|
148
|
+
return { violations, scanned: manifests.length };
|
|
135
149
|
}
|
|
136
150
|
|
|
137
151
|
function main() {
|
|
138
152
|
const rewrite = process.argv.includes('--fix');
|
|
139
|
-
const violations = findViolations({ rewrite });
|
|
153
|
+
const { violations, scanned } = findViolations({ rewrite });
|
|
154
|
+
|
|
155
|
+
// A scan that read NOTHING and a scan that found nothing wrong print the
|
|
156
|
+
// same green and exit the same 0. Only the corpus separates them, so this
|
|
157
|
+
// refuses rather than reporting a pass it cannot justify. REPO_ROOT's own
|
|
158
|
+
// manifest is asserted to exist above, so zero here means the walk failed.
|
|
159
|
+
if (scanned === 0) {
|
|
160
|
+
console.error(
|
|
161
|
+
`::error::scanned 0 manifests under ${REPO_ROOT} — the walk found nothing, ` +
|
|
162
|
+
'not even this repository\'s own package.json. Refusing to report a pass ' +
|
|
163
|
+
'over an empty corpus.',
|
|
164
|
+
);
|
|
165
|
+
return 1;
|
|
166
|
+
}
|
|
140
167
|
|
|
141
168
|
if (violations.length === 0) {
|
|
142
169
|
console.log(
|
|
143
|
-
|
|
170
|
+
`workspace ranges match their local packages in all ${scanned} manifest(s) — ` +
|
|
171
|
+
'every build edge is visible to pnpm.',
|
|
144
172
|
);
|
|
145
173
|
return 0;
|
|
146
174
|
}
|
|
@@ -10,9 +10,12 @@
|
|
|
10
10
|
* that specific wrong implementation.
|
|
11
11
|
*/
|
|
12
12
|
import assert from 'node:assert/strict';
|
|
13
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { dirname, join, relative } from 'node:path';
|
|
13
16
|
import test from 'node:test';
|
|
14
17
|
|
|
15
|
-
import { satisfies } from './check-workspace-range-matches-local.mjs';
|
|
18
|
+
import { listManifests, satisfies } from './check-workspace-range-matches-local.mjs';
|
|
16
19
|
|
|
17
20
|
test('^0.y.z pins the MINOR — the rule the whole check turns on', () => {
|
|
18
21
|
// The real defect: a client moved 0.2.0 -> 0.3.0 and fell out of every
|
|
@@ -52,3 +55,51 @@ test('a prerelease is compared on its release part, never string-wise', () => {
|
|
|
52
55
|
assert.equal(satisfies('0.3.0-rc.1', '^0.2.0'), false);
|
|
53
56
|
assert.equal(satisfies('0.2.5-rc.1', '^0.2.0'), true);
|
|
54
57
|
});
|
|
58
|
+
|
|
59
|
+
// ── The WALK, not the predicate ───────────────────────────────────────────
|
|
60
|
+
// A `-not -path` exclusion on a node_modules glob filters results while still
|
|
61
|
+
// DESCENDING into
|
|
62
|
+
// every one of them. That is not merely slow: it made the gate race a sibling
|
|
63
|
+
// step that was deleting a temp directory under `node_modules`, so `find`
|
|
64
|
+
// exited non-zero and the check CRASHED rather than reporting a violation.
|
|
65
|
+
// A crash reads in CI as a failing check rather than an absent one.
|
|
66
|
+
//
|
|
67
|
+
// Two-sided on purpose. Asserting only that the pruned paths are absent would
|
|
68
|
+
// also pass if the walk returned NOTHING — the empty-corpus failure this file
|
|
69
|
+
// already warns about, one function over.
|
|
70
|
+
test('listManifests does not DESCEND into node_modules — an untraversable dir there must not break the walk', () => {
|
|
71
|
+
const root = mkdtempSync(join(tmpdir(), 'xema-prune-'));
|
|
72
|
+
const blocked = join(root, 'node_modules', 'blocked');
|
|
73
|
+
try {
|
|
74
|
+
const write = (rel) => {
|
|
75
|
+
mkdirSync(join(root, dirname(rel)), { recursive: true });
|
|
76
|
+
writeFileSync(join(root, rel), '{"name":"x","version":"1.0.0"}\n');
|
|
77
|
+
};
|
|
78
|
+
write('package.json');
|
|
79
|
+
write('packages/real/package.json');
|
|
80
|
+
write('packages/real/dist/package.json');
|
|
81
|
+
write('node_modules/installed/package.json');
|
|
82
|
+
|
|
83
|
+
// Reproduces the real failure: a directory under node_modules the walk
|
|
84
|
+
// cannot traverse. A FILTERING walk descends, `find` exits non-zero on
|
|
85
|
+
// "Permission denied", and execFileSync THROWS -- the gate crashes instead
|
|
86
|
+
// of reporting a violation. A PRUNING walk never enters it.
|
|
87
|
+
//
|
|
88
|
+
// This is what the previous version of this test missed: `-not -path`
|
|
89
|
+
// filters the OUTPUT, so both walks return the identical set and an
|
|
90
|
+
// output assertion cannot tell them apart. Only descent is observable,
|
|
91
|
+
// and this is how you observe it.
|
|
92
|
+
mkdirSync(blocked, { recursive: true });
|
|
93
|
+
chmodSync(blocked, 0o000);
|
|
94
|
+
|
|
95
|
+
const found = listManifests(root).map((f) => relative(root, f)).sort();
|
|
96
|
+
|
|
97
|
+
// Control: the walk really ran and found the real manifests.
|
|
98
|
+
assert.deepEqual(found, ['package.json', 'packages/real/package.json']);
|
|
99
|
+
assert.equal(found.some((f) => f.includes('node_modules')), false);
|
|
100
|
+
assert.equal(found.some((f) => f.includes('dist')), false);
|
|
101
|
+
} finally {
|
|
102
|
+
try { chmodSync(blocked, 0o755); } catch { /* never created */ }
|
|
103
|
+
rmSync(root, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// `node_modules/` but leaves the SECOND one inside the path,
|
|
27
27
|
// producing leaked emits like:
|
|
28
28
|
//
|
|
29
|
-
// require("@xemahq/
|
|
29
|
+
// require("@xemahq/contracts/policy/node_modules/
|
|
30
30
|
// @xemahq/kernel-contracts/execution-context/dist/lib/subject")
|
|
31
31
|
//
|
|
32
32
|
// …which crashes the pod at boot with ERR_PACKAGE_PATH_NOT_EXPORTED
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// via `@ApiProperty({ enum: ... })`, the plugin emits the
|
|
39
39
|
// declaration file path under the package's published layout:
|
|
40
40
|
//
|
|
41
|
-
// require("@xemahq/
|
|
41
|
+
// require("@xemahq/contracts/capability/dist/lib/capability-grant")
|
|
42
42
|
//
|
|
43
43
|
// This crashes at boot with the same ERR_PACKAGE_PATH_NOT_EXPORTED
|
|
44
44
|
// whenever the contracts package's published `exports` map does NOT
|
|
@@ -438,6 +438,83 @@ async function* walk(dir) {
|
|
|
438
438
|
}
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
// ── WHERE THE PLUGIN'S REQUIRES LIVE, AND WHY THAT IS THE WHOLE SCOPE ────────
|
|
442
|
+
//
|
|
443
|
+
// This file's own header states the scope: "Post-build sanitizer for
|
|
444
|
+
// @nestjs/swagger plugin v11.x output", and "the swagger CLI plugin emits
|
|
445
|
+
// `_OPENAPI_METADATA_FACTORY()` blocks that contain `require("<path>")` calls".
|
|
446
|
+
// Every leak it exists to repair is therefore INSIDE such a block.
|
|
447
|
+
//
|
|
448
|
+
// Shape 2 did not honour that. It rewrote `require("@xemahq/<pkg>/dist/<rest>")`
|
|
449
|
+
// ANYWHERE in the emitted file, on the assumption recorded above — "every
|
|
450
|
+
// package they have ever matched exposes a root export". That assumption is
|
|
451
|
+
// measurably false today, and a HAND-WRITTEN deep import compiles to exactly
|
|
452
|
+
// the shape the regex matches, so it was rewritten too:
|
|
453
|
+
//
|
|
454
|
+
// src: import { RealmTokenVerifier }
|
|
455
|
+
// from '@xemahq/platform-common/dist/nestjs/auth/realm-token-verifier';
|
|
456
|
+
// dist: const realm_token_verifier_1 = require("@xemahq/platform-common");
|
|
457
|
+
//
|
|
458
|
+
// The module loads, the binding is not on the root barrel, and the service dies
|
|
459
|
+
// at DI time with "RealmTokenVerifier is not a constructor". That is what took
|
|
460
|
+
// xema-shell-api's rollout down on 2026-09-17 — and every check was green,
|
|
461
|
+
// because typecheck, jest and the pre-push gate all read the SOURCE, and
|
|
462
|
+
// nothing executes the rewritten dist before the pod does.
|
|
463
|
+
//
|
|
464
|
+
// Measured across the four packages shape 2 matches in shipped source, asking
|
|
465
|
+
// each published tarball whether its ROOT entry exports the symbol:
|
|
466
|
+
//
|
|
467
|
+
// @xemahq/platform-common RealmTokenVerifier NOT on root
|
|
468
|
+
// @xemahq/biome-supply-chain CosignErrorCode NOT on root
|
|
469
|
+
// @xemahq/xema-service-nest buildResourcePath NOT on root
|
|
470
|
+
// @xemahq/biome-database-nest assertMigrationHistoryCompatible NOT on root
|
|
471
|
+
//
|
|
472
|
+
// so the blind rewrite was wrong for all four. (Only two reach a shipped dist:
|
|
473
|
+
// xema-shell-api, which fired, and biome-host-api, which is armed on the
|
|
474
|
+
// bundle-fetch ERROR path. The other nine occurrences are in tests, which run
|
|
475
|
+
// from TypeScript and never see this rewriter.)
|
|
476
|
+
//
|
|
477
|
+
// The fix is a SCOPE correction, not more proof machinery: rewrite only what
|
|
478
|
+
// the plugin emitted, and REFUSE anything else. `reExportsReach` cannot stand
|
|
479
|
+
// in for this — it proves a module is REACHED, not that a BINDING is
|
|
480
|
+
// re-exported, and `nestjs/index.js` requires `./auth` while re-exporting only
|
|
481
|
+
// some of its names, so it would have called the bad rewrite provable.
|
|
482
|
+
//
|
|
483
|
+
// A refusal here is the whole point: it converts a production CrashLoopBackOff
|
|
484
|
+
// into a build failure in the repository that wrote the import.
|
|
485
|
+
function openApiFactoryRanges(src) {
|
|
486
|
+
const ranges = [];
|
|
487
|
+
const token = '_OPENAPI_METADATA_FACTORY';
|
|
488
|
+
let from = 0;
|
|
489
|
+
for (;;) {
|
|
490
|
+
const hit = src.indexOf(token, from);
|
|
491
|
+
if (hit === -1) break;
|
|
492
|
+
from = hit + token.length;
|
|
493
|
+
const open = src.indexOf('{', from);
|
|
494
|
+
if (open === -1) break;
|
|
495
|
+
// Brace-match. String and comment bodies inside a metadata factory are
|
|
496
|
+
// emitted by tsc and contain no unbalanced braces in practice; a miscount
|
|
497
|
+
// can only ever END the range early, which fails CLOSED (the require is
|
|
498
|
+
// then treated as hand-written and refused) rather than widening the
|
|
499
|
+
// rewrite to code the plugin did not emit.
|
|
500
|
+
let depth = 0;
|
|
501
|
+
let i = open;
|
|
502
|
+
for (; i < src.length; i += 1) {
|
|
503
|
+
if (src[i] === '{') depth += 1;
|
|
504
|
+
else if (src[i] === '}') {
|
|
505
|
+
depth -= 1;
|
|
506
|
+
if (depth === 0) break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
ranges.push([open, Math.min(i + 1, src.length)]);
|
|
510
|
+
from = i + 1;
|
|
511
|
+
}
|
|
512
|
+
return ranges;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const inAnyRange = (ranges, offset) =>
|
|
516
|
+
ranges.some(([start, end]) => offset >= start && offset < end);
|
|
517
|
+
|
|
441
518
|
async function scrubFile(path) {
|
|
442
519
|
const src = await readFile(path, 'utf8');
|
|
443
520
|
let count = 0;
|
|
@@ -455,10 +532,35 @@ async function scrubFile(path) {
|
|
|
455
532
|
count += 1;
|
|
456
533
|
return `require("${kept}")`;
|
|
457
534
|
});
|
|
458
|
-
|
|
535
|
+
// Shape 2 is scoped to the plugin's own emits. Anything else matching this
|
|
536
|
+
// shape is a hand-written deep import, and rewriting it silently changes
|
|
537
|
+
// which module the service loads — see the note above `openApiFactoryRanges`.
|
|
538
|
+
const factoryRanges = openApiFactoryRanges(out);
|
|
539
|
+
const handWritten = [];
|
|
540
|
+
out = out.replace(FLAT_LEAK_REGEX, (match, pkg, offset) => {
|
|
541
|
+
if (!inAnyRange(factoryRanges, offset)) {
|
|
542
|
+
handWritten.push(match.slice('require("'.length, -2));
|
|
543
|
+
return match;
|
|
544
|
+
}
|
|
459
545
|
count += 1;
|
|
460
546
|
return `require("${pkg}")`;
|
|
461
547
|
});
|
|
548
|
+
if (handWritten.length > 0) {
|
|
549
|
+
throw new ScrubFailure(
|
|
550
|
+
`${path}\n` +
|
|
551
|
+
` ${handWritten.length} hand-written deep import(s) into a package's dist/:\n` +
|
|
552
|
+
handWritten.map((spec) => ` ${spec}`).join('\n') +
|
|
553
|
+
`\n\n These are NOT swagger-plugin emits — they are outside every\n` +
|
|
554
|
+
` _OPENAPI_METADATA_FACTORY block, so this rewriter is not entitled to\n` +
|
|
555
|
+
` touch them. Rewriting one to the bare package specifier (which is what\n` +
|
|
556
|
+
` this step used to do, silently) loads the package root instead, and any\n` +
|
|
557
|
+
` binding the root barrel does not re-export becomes \`undefined\` at\n` +
|
|
558
|
+
` runtime — a DI crash in the pod, with every build check green.\n\n` +
|
|
559
|
+
` Fix the IMPORT, in source: use the package's public surface. If the\n` +
|
|
560
|
+
` symbol is not on it, the package is missing an export — add it there\n` +
|
|
561
|
+
` rather than reaching through dist/.`,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
462
564
|
out = out.replace(RELATIVE_WORKSPACE_REGEX, (_match, pkgName, subpath) => {
|
|
463
565
|
count += 1;
|
|
464
566
|
return `require("@xemahq/${pkgName}/${subpath}")`;
|
|
@@ -628,7 +730,7 @@ async function selfTest() {
|
|
|
628
730
|
throw new Error(`self-test: detector missed the ${shape} shape: ${literal}`);
|
|
629
731
|
}
|
|
630
732
|
}
|
|
631
|
-
const clean = 'require("@xemahq/
|
|
733
|
+
const clean = 'require("@xemahq/contracts");require("./local");require("../shared/util");';
|
|
632
734
|
if (findLeaks(clean).length !== 0) {
|
|
633
735
|
throw new Error(`self-test: detector matched clean output: ${JSON.stringify(findLeaks(clean))}`);
|
|
634
736
|
}
|
|
@@ -637,16 +739,57 @@ async function selfTest() {
|
|
|
637
739
|
const green = join(root, 'green');
|
|
638
740
|
await mkdir(green, { recursive: true });
|
|
639
741
|
const greenFile = join(green, 'dto.js');
|
|
640
|
-
|
|
742
|
+
// The plugin emits its requires INSIDE `_OPENAPI_METADATA_FACTORY`, so the
|
|
743
|
+
// green fixture has to as well — a bare literal at top level is a
|
|
744
|
+
// HAND-WRITTEN import and is now refused, which is the point of the scope.
|
|
745
|
+
await writeFile(
|
|
746
|
+
greenFile,
|
|
747
|
+
`class Dto {\n static _OPENAPI_METADATA_FACTORY() {\n return { x: { required: true, type: () => ${samples[0][1]} } };\n }\n}\n`,
|
|
748
|
+
'utf8',
|
|
749
|
+
);
|
|
641
750
|
const greenResult = await run([green]);
|
|
642
751
|
if (greenResult.totalRewrites < 1) {
|
|
643
752
|
throw new Error(`self-test: expected at least 1 rewrite, got ${greenResult.totalRewrites}`);
|
|
644
753
|
}
|
|
754
|
+
// THE CASE THAT PINS THE SCOPE, both polarities in one fixture.
|
|
755
|
+
//
|
|
756
|
+
// Same package, same deep path, twice: once as the plugin emits it (inside
|
|
757
|
+
// `_OPENAPI_METADATA_FACTORY`) and once as tsc emits a hand-written
|
|
758
|
+
// `import ... from '@xemahq/<pkg>/dist/...'` (a top-level const). The
|
|
759
|
+
// rewriter must take the first and REFUSE the whole file for the second.
|
|
760
|
+
//
|
|
761
|
+
// Without this the fix is unpinned: the old blind pass rewrote both, and
|
|
762
|
+
// that is what put `require("@xemahq/platform-common")` into
|
|
763
|
+
// xema-shell-api's dist and crashlooped it in production with every build
|
|
764
|
+
// check green.
|
|
765
|
+
const mixed = join(root, 'mixed');
|
|
766
|
+
await mkdir(mixed, { recursive: true });
|
|
767
|
+
await writeFile(
|
|
768
|
+
join(mixed, 'dto.js'),
|
|
769
|
+
'const realm_token_verifier_1 = require("@xemahq/platform-common/dist/nestjs/auth/realm-token-verifier");\n' +
|
|
770
|
+
'class Dto {\n static _OPENAPI_METADATA_FACTORY() {\n' +
|
|
771
|
+
' return { x: { type: () => require("@xemahq/capability-contracts/dist/lib/capability-grant") } };\n }\n}\n',
|
|
772
|
+
'utf8',
|
|
773
|
+
);
|
|
774
|
+
const refusal = await expectFailure('hand-written deep dist import', () => run([mixed]));
|
|
775
|
+
if (!refusal.message.includes('realm-token-verifier')) {
|
|
776
|
+
throw new Error(
|
|
777
|
+
`self-test: the refusal must NAME the offending specifier; got: ${refusal.message}`,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
// And it must refuse the HAND-WRITTEN one, not the plugin's — if the scope
|
|
781
|
+
// check were inverted this assertion is what catches it.
|
|
782
|
+
if (refusal.message.includes('capability-grant')) {
|
|
783
|
+
throw new Error(
|
|
784
|
+
'self-test: the plugin-emitted require inside the factory was reported as hand-written — the scope test is inverted.',
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
645
788
|
const greenAfter = await readFile(greenFile, 'utf8');
|
|
646
789
|
// The NESTED pass unwraps to `@xemahq/subject-contracts/dist/lib/x`, which
|
|
647
790
|
// the FLAT pass then reduces to the bare specifier — the two passes chain,
|
|
648
791
|
// and the verification proves the chain terminated clean.
|
|
649
|
-
if (greenAfter.
|
|
792
|
+
if (!greenAfter.includes('require("@xemahq/subject-contracts")')) {
|
|
650
793
|
throw new Error(`self-test: unexpected rewrite result: ${greenAfter.trim()}`);
|
|
651
794
|
}
|
|
652
795
|
if (findLeaks(greenAfter).length !== 0) {
|