105 lines
5 KiB
JavaScript
105 lines
5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-dependency-names.mjs — every declared dependency must be a REAL package name.
|
|
*
|
|
* WHY (a P0 found 2026-09-22 by cloning the repo and running the documented first command)
|
|
* `bun install` FAILED in a fresh clone:
|
|
*
|
|
* error: GET https://registry.npmjs.org/@deployment%2fcapacitor%2fcli - 405
|
|
* error: @deployment/capacitor/cli@7 failed to resolve
|
|
*
|
|
* The rename of `capacitor/` → `deployment/capacitor/` had done a blind string replace that also
|
|
* rewrote three NPM PACKAGE NAMES: @capacitor/{android,cli,core} became @deployment/capacitor/*,
|
|
* which do not exist on npm. A folder rename reached into a namespace it had no business touching.
|
|
*
|
|
* ★ THE SHAPE THIS GUARDS — AN INHERITOR-ONLY FAILURE. The local tree worked PERFECTLY, because
|
|
* node_modules/@capacitor/* had been installed BEFORE the rename. Every build passed, every gate
|
|
* passed, `check:no-abs-paths` scanned 783 files and passed. The breakage lived ONLY in the
|
|
* DECLARED names, so it was invisible to everyone except someone installing from scratch — which
|
|
* is precisely the person the north star is about ("the path Doxa inherits").
|
|
*
|
|
* WHAT IT CHECKS, and why this is enough to catch that class OFFLINE:
|
|
* 1. the declared name resolves under node_modules/, and
|
|
* 2. the installed package's OWN `name` field equals the declared key.
|
|
* A corrupted name fails (1) because nothing was ever installed under it. A name that resolves to
|
|
* a DIFFERENT package fails (2) — the case a path-based check alone would miss.
|
|
*
|
|
* WHY NOT JUST RUN `bun install` IN CI. That needs a network and a clean cache to be meaningful,
|
|
* and a warm lockfile hides exactly this defect. This runs in milliseconds, offline, against the
|
|
* tree as it stands. It does NOT replace the clone test — see the note at the foot of the
|
|
* standalone-gate table — it makes the cheap half of it automatic.
|
|
*
|
|
* NEGATIVE-TESTED: renaming a declared dependency to a nonexistent scope makes this FAIL naming
|
|
* the key; restoring it returns PASS. A package.json with no dependencies FAILS (vacuous guard).
|
|
*/
|
|
import { readFileSync, existsSync } from 'node:fs'
|
|
import { resolve, dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
|
|
let pkg
|
|
try {
|
|
pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
|
|
} catch (e) {
|
|
console.error(`DEPENDENCY-NAMES: FAIL — cannot read package.json (${String(e.message).slice(0, 60)}).`)
|
|
process.exit(1)
|
|
}
|
|
|
|
const declared = []
|
|
for (const section of ['dependencies', 'devDependencies']) {
|
|
for (const [name, range] of Object.entries(pkg[section] || {})) declared.push({ section, name, range })
|
|
}
|
|
|
|
// Vacuous guard: "0 dependencies" must never read as "0 problems".
|
|
const MIN_DECLARED = 5
|
|
if (declared.length < MIN_DECLARED) {
|
|
console.error(`DEPENDENCY-NAMES: FAIL — only ${declared.length} declared dependency(ies) ` +
|
|
`(expected >= ${MIN_DECLARED}). package.json is empty or unreadable; refusing to pass vacuously.`)
|
|
process.exit(1)
|
|
}
|
|
|
|
const problems = []
|
|
let verified = 0
|
|
let uninstalled = 0
|
|
|
|
for (const { section, name, range } of declared) {
|
|
const pj = join(ROOT, 'node_modules', name, 'package.json')
|
|
if (!existsSync(pj)) {
|
|
// Not installed. That is the CORRUPTED-NAME signature when node_modules is otherwise populated:
|
|
// nothing could ever have been installed under a name that does not exist upstream.
|
|
uninstalled++
|
|
problems.push({ section, name, range, why: 'declared but NOT INSTALLED — is this a real package name?' })
|
|
continue
|
|
}
|
|
let real
|
|
try { real = JSON.parse(readFileSync(pj, 'utf8')).name } catch { real = null }
|
|
if (real !== name) {
|
|
problems.push({ section, name, range, why: `resolves to a package whose own name is ${JSON.stringify(real)}` })
|
|
continue
|
|
}
|
|
verified++
|
|
}
|
|
|
|
console.log(`DEPENDENCY-NAMES: ${declared.length} declared, ${verified} verified against node_modules.`)
|
|
|
|
// If node_modules is absent entirely, this gate cannot judge — say so rather than fail or pass.
|
|
if (uninstalled === declared.length) {
|
|
console.log('DEPENDENCY-NAMES: SKIP — node_modules/ is empty or absent, so nothing can be cross-checked.')
|
|
console.log(' Run `bun install` first. An absent tree is not evidence either way.')
|
|
process.exit(0)
|
|
}
|
|
|
|
if (problems.length) {
|
|
console.error(`\nDEPENDENCY-NAMES: FAIL — ${problems.length} declared dependency(ies) do not check out:`)
|
|
for (const p of problems) console.error(` ✗ ${p.section}: ${p.name}@${p.range}\n ${p.why}`)
|
|
console.error(
|
|
'\n A declared name that does not exist upstream breaks `bun install` in a FRESH CLONE while\n' +
|
|
' working perfectly here, because the real package is already in node_modules from before the\n' +
|
|
' change. Check whether a rename or a search-and-replace rewrote a package name: that is how\n' +
|
|
' @capacitor/cli became @deployment/capacitor/cli (2026-09-22).'
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log('DEPENDENCY-NAMES: PASS — every declared dependency is a real, correctly-named package.')
|