Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
152 lines
6.1 KiB
JavaScript
152 lines
6.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* lint-compiled-artifact-sync — fail when a *tracked* compiled artifact under
|
|
* msd-core/bin/lib/ has drifted from its src/*.cts source.
|
|
*
|
|
* ADR-457 compiles src/*.cts to msd-core/bin/lib/*.cjs at build time and expects
|
|
* those artifacts to be gitignored. Most are (see .gitignore). A handful are
|
|
* still tracked because the migration that moved the module into src/ did not
|
|
* also add the emitted .cjs to .gitignore. While a compiled artifact remains
|
|
* tracked, the committed bytes are what ships to anyone who reads the repo
|
|
* without building — so they must match the source.
|
|
*
|
|
* #2653: msd-core/bin/lib/api-coverage.cjs sat four days behind its .cts after
|
|
* PR #2551 changed the source without regenerating the artifact, shipping a
|
|
* module that silently lacked the entire #2366 fix while CI stayed green.
|
|
*
|
|
* This check is deliberately regime-agnostic: it asserts a property of whatever
|
|
* is tracked right now. If the remaining artifacts are later untracked and
|
|
* gitignored (the ADR-457 end state), the tracked set becomes empty and this
|
|
* script passes trivially — no edit required.
|
|
*
|
|
* Usage: node scripts/lint-compiled-artifact-sync.cjs [--check]
|
|
* Exit 0 when every tracked artifact matches a fresh compile; 1 otherwise.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { execFileSync } = require('node:child_process');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
|
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
const LIB_DIR = path.join('msd-core', 'bin', 'lib');
|
|
const SRC_DIR = 'src';
|
|
|
|
/** Frozen reason codes so tests assert on structure, not prose. */
|
|
const REASON = Object.freeze({
|
|
OK: 'ok_artifacts_in_sync',
|
|
DRIFTED: 'fail_artifact_drifted',
|
|
BUILD_FAILED: 'fail_build_failed',
|
|
MISSING_EMIT: 'fail_missing_emit',
|
|
});
|
|
|
|
function git(args) {
|
|
// -c safe.directory=REPO_ROOT: containerized CI checkouts are frequently
|
|
// owned by a different uid than the one running the test process, and git
|
|
// refuses to operate at all on such a repo ("detected dubious ownership")
|
|
// unless explicitly trusted. Scoped per-invocation (not written to any
|
|
// config file) so this never widens trust beyond this one call.
|
|
return execFileSync('git', ['-c', `safe.directory=${REPO_ROOT}`, ...args], { cwd: REPO_ROOT, encoding: 'utf8' });
|
|
}
|
|
|
|
/**
|
|
* Tracked .cjs files under msd-core/bin/lib/ that have a matching src/*.cts.
|
|
* Uses `git ls-files` so the set is derived from what git actually tracks
|
|
* rather than a hand-maintained list that could itself drift.
|
|
*/
|
|
function trackedCompiledArtifacts() {
|
|
const tracked = git(['ls-files', LIB_DIR]).split('\n').filter((l) => l.endsWith('.cjs'));
|
|
const out = [];
|
|
for (const rel of tracked) {
|
|
const stem = path.basename(rel, '.cjs');
|
|
const subdir = path.dirname(path.relative(LIB_DIR, rel));
|
|
const srcRel = path.join(SRC_DIR, subdir === '.' ? '' : subdir, `${stem}.cts`);
|
|
if (fs.existsSync(path.join(REPO_ROOT, srcRel))) out.push({ artifact: rel, source: srcRel });
|
|
}
|
|
return out.sort((a, b) => (a.artifact < b.artifact ? -1 : a.artifact > b.artifact ? 1 : 0));
|
|
}
|
|
|
|
/** Compile the whole project to a throwaway outDir so the work tree is untouched. */
|
|
function compileToTemp() {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-artifact-sync-'));
|
|
try {
|
|
execFileSync(
|
|
process.execPath,
|
|
[
|
|
path.join(REPO_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'),
|
|
'-p', path.join(REPO_ROOT, 'tsconfig.build.json'),
|
|
'--outDir', tmp,
|
|
// A throwaway outDir must not reuse the in-tree incremental state, or
|
|
// tsc skips emit for files it believes are already current.
|
|
'--incremental', 'false',
|
|
'--tsBuildInfoFile', 'null',
|
|
],
|
|
{ cwd: REPO_ROOT, encoding: 'utf8', stdio: 'pipe' },
|
|
);
|
|
return { ok: true, dir: tmp };
|
|
} catch (err) {
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
const detail = [err.stdout, err.stderr].filter(Boolean).join('\n').trim();
|
|
return { ok: false, detail };
|
|
}
|
|
}
|
|
|
|
function main() {
|
|
const pairs = trackedCompiledArtifacts();
|
|
if (pairs.length === 0) {
|
|
console.log('ok compiled-artifact-sync: no tracked compiled artifacts (ADR-457 end state)');
|
|
return 0;
|
|
}
|
|
|
|
const build = compileToTemp();
|
|
if (!build.ok) {
|
|
console.error(`FAIL compiled-artifact-sync: ${REASON.BUILD_FAILED}`);
|
|
console.error(build.detail);
|
|
return 1;
|
|
}
|
|
|
|
const drifted = [];
|
|
const missing = [];
|
|
try {
|
|
for (const { artifact, source } of pairs) {
|
|
const fresh = path.join(build.dir, path.relative(LIB_DIR, artifact));
|
|
if (!fs.existsSync(fresh)) { missing.push({ artifact, source }); continue; }
|
|
const a = fs.readFileSync(path.join(REPO_ROOT, artifact));
|
|
const b = fs.readFileSync(fresh);
|
|
if (!a.equals(b)) drifted.push({ artifact, source, committed: a.length, expected: b.length });
|
|
}
|
|
} finally {
|
|
fs.rmSync(build.dir, { recursive: true, force: true });
|
|
}
|
|
|
|
if (missing.length > 0) {
|
|
console.error(`FAIL compiled-artifact-sync: ${REASON.MISSING_EMIT}`);
|
|
for (const m of missing) console.error(` ${m.artifact} — no emit produced from ${m.source}`);
|
|
return 1;
|
|
}
|
|
|
|
if (drifted.length > 0) {
|
|
console.error(`FAIL compiled-artifact-sync: ${REASON.DRIFTED}`);
|
|
for (const d of drifted) {
|
|
console.error(` ${d.artifact} (${d.committed} bytes) != compile of ${d.source} (${d.expected} bytes)`);
|
|
}
|
|
console.error('');
|
|
console.error('The committed artifact is what ships to anyone reading the repo without');
|
|
console.error('building, so it must match its source. Fix with:');
|
|
console.error(' npm run build:lib && git add ' + drifted.map((d) => d.artifact).join(' '));
|
|
console.error('');
|
|
console.error('Alternatively, per ADR-457 these artifacts are meant to be gitignored —');
|
|
console.error('untracking them (git rm --cached + .gitignore) also resolves this.');
|
|
return 1;
|
|
}
|
|
|
|
console.log(`ok compiled-artifact-sync: ${pairs.length} tracked artifact(s) match their source`);
|
|
return 0;
|
|
}
|
|
|
|
if (require.main === module) process.exitCode = main();
|
|
|
|
module.exports = { REASON, trackedCompiledArtifacts };
|