232 lines
8.4 KiB
JavaScript
232 lines
8.4 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Compare npm lockfile versions for P2P stack packages against optional local
|
||
* Holepunch clones (same root as sync-holepunch-clones.mjs).
|
||
*
|
||
* Writes:
|
||
* docs/audit/holepunch-lockfile-drift.json
|
||
* docs/audit/holepunch-lockfile-drift-summary.ndjson (one row per package)
|
||
*
|
||
* Env:
|
||
* BARE_OS_HOLEPUNCH_CLONES_ROOT — default ~/dev/pearcli/holepunch-repos/holepunchto_repos
|
||
* BARE_OS_HOLEPUNCH_LOCKFILE_DRIFT_TIER1_FAIL=1 — fail when a tier1 lockfile package
|
||
* has semverMismatchWithClone=true (uses docs/audit/holepunch-drift-repos.json tier1Repos)
|
||
*
|
||
* Config: docs/audit/holepunch-drift-repos.json
|
||
* lockfilePackages: [{ "cloneName": "protomux", "lockKey": "protomux" }, ...]
|
||
* If omitted, defaults are derived from suggestedCriticalRepos + common wire deps.
|
||
*/
|
||
import fs from 'node:fs'
|
||
import path from 'node:path'
|
||
import process from 'node:process'
|
||
import { fileURLToPath } from 'node:url'
|
||
|
||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||
const lockPath = path.join(root, 'package-lock.json')
|
||
const driftConfigPath = path.join(root, 'docs/audit/holepunch-drift-repos.json')
|
||
const outJson = path.join(root, 'docs/audit/holepunch-lockfile-drift.json')
|
||
const outNd = path.join(root, 'docs/audit/holepunch-lockfile-drift-summary.ndjson')
|
||
|
||
const defaultClones = path.join(
|
||
process.env.HOME || '',
|
||
'dev/pearcli/holepunch-repos/holepunchto_repos'
|
||
)
|
||
const clonesRoot = String(
|
||
process.env.BARE_OS_HOLEPUNCH_CLONES_ROOT || defaultClones
|
||
).trim()
|
||
|
||
/** @type {{ cloneName: string, lockKey: string }[]} */
|
||
const DEFAULT_LOCK_PACKAGES = [
|
||
{ cloneName: 'protomux', lockKey: 'protomux' },
|
||
{ cloneName: 'hyperdrive', lockKey: 'hyperdrive' },
|
||
{ cloneName: 'hypercore', lockKey: 'hypercore' },
|
||
{ cloneName: 'hyperswarm', lockKey: 'hyperswarm' },
|
||
{ cloneName: 'udx-native', lockKey: 'udx-native' },
|
||
{ cloneName: 'corestore', lockKey: 'corestore' },
|
||
{ cloneName: 'compact-encoding', lockKey: 'compact-encoding' },
|
||
{ cloneName: 'secret-stream', lockKey: '@hyperswarm/secret-stream' }
|
||
]
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} lock
|
||
* @param {string} key npm package name (e.g. protomux or @hyperswarm/secret-stream)
|
||
*/
|
||
function lockedVersion(lock, key) {
|
||
const k = 'node_modules/' + key
|
||
const p = lock.packages && lock.packages[k]
|
||
if (!p || typeof p !== 'object') return ''
|
||
return String(/** @type {{ version?: string }} */ (p).version || '').trim()
|
||
}
|
||
|
||
function loadLockfilePackages() {
|
||
if (!fs.existsSync(driftConfigPath)) return DEFAULT_LOCK_PACKAGES
|
||
try {
|
||
const j = JSON.parse(fs.readFileSync(driftConfigPath, 'utf8'))
|
||
if (Array.isArray(j.lockfilePackages) && j.lockfilePackages.length) {
|
||
return j.lockfilePackages
|
||
.map((row) => {
|
||
if (!row || typeof row !== 'object') return null
|
||
const r = /** @type {{ cloneName?: string, lockKey?: string }} */ (row)
|
||
const cn = String(r.cloneName || '').trim()
|
||
const lk = String(r.lockKey || r.cloneName || '').trim()
|
||
if (!cn || !lk) return null
|
||
return { cloneName: cn, lockKey: lk }
|
||
})
|
||
.filter(Boolean)
|
||
}
|
||
if (Array.isArray(j.suggestedCriticalRepos)) {
|
||
return j.suggestedCriticalRepos
|
||
.map((n) => {
|
||
const name = String(n || '').trim()
|
||
if (!name) return null
|
||
return { cloneName: name, lockKey: name }
|
||
})
|
||
.filter(Boolean)
|
||
}
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
return DEFAULT_LOCK_PACKAGES
|
||
}
|
||
|
||
/**
|
||
* @returns {Set<string>}
|
||
*/
|
||
function loadTier1RepoSet() {
|
||
if (!fs.existsSync(driftConfigPath)) return new Set()
|
||
try {
|
||
const j = JSON.parse(fs.readFileSync(driftConfigPath, 'utf8'))
|
||
if (!Array.isArray(j.tier1Repos)) return new Set()
|
||
return new Set(
|
||
j.tier1Repos
|
||
.map((v) => String(v || '').trim())
|
||
.filter(Boolean)
|
||
)
|
||
} catch {
|
||
return new Set()
|
||
}
|
||
}
|
||
|
||
function main() {
|
||
if (!fs.existsSync(lockPath)) {
|
||
console.error('report-holepunch-lockfile-drift: missing', lockPath)
|
||
process.exit(1)
|
||
}
|
||
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'))
|
||
const pkgs = loadLockfilePackages()
|
||
const tier1Repos = loadTier1RepoSet()
|
||
const strictTier1Fail =
|
||
String(process.env.BARE_OS_HOLEPUNCH_LOCKFILE_DRIFT_TIER1_FAIL || '')
|
||
.trim()
|
||
.toLowerCase() === '1' ||
|
||
String(process.env.BARE_OS_HOLEPUNCH_LOCKFILE_DRIFT_TIER1_FAIL || '')
|
||
.trim()
|
||
.toLowerCase() === 'true'
|
||
const atMs = Date.now()
|
||
const rootExists = fs.existsSync(clonesRoot)
|
||
|
||
/** @type {Record<string, unknown>[]} */
|
||
const rows = []
|
||
for (const { cloneName, lockKey } of pkgs) {
|
||
const lv = lockedVersion(lock, lockKey)
|
||
const cloneDir = path.join(clonesRoot, cloneName)
|
||
const pj = path.join(cloneDir, 'package.json')
|
||
let cloneVersion = ''
|
||
if (fs.existsSync(pj)) {
|
||
try {
|
||
const p = JSON.parse(fs.readFileSync(pj, 'utf8'))
|
||
cloneVersion = String(p.version || '').trim()
|
||
} catch {
|
||
cloneVersion = ''
|
||
}
|
||
}
|
||
const semverMismatch =
|
||
lv && cloneVersion && lv !== cloneVersion ? true : false
|
||
const tier1 = tier1Repos.has(cloneName)
|
||
rows.push({
|
||
cloneName,
|
||
lockKey,
|
||
lockfileVersion: lv || null,
|
||
clonePath: cloneDir,
|
||
cloneExists: fs.existsSync(cloneDir),
|
||
clonePackageJsonVersion: cloneVersion || null,
|
||
semverMismatchWithClone: semverMismatch,
|
||
tier1
|
||
})
|
||
}
|
||
const tier1MismatchRows = rows.filter(
|
||
(r) => r.tier1 && r.semverMismatchWithClone
|
||
)
|
||
|
||
const report = {
|
||
schema: 1,
|
||
atMs,
|
||
lockfilePath: path.relative(root, lockPath),
|
||
clonesRoot,
|
||
clonesRootExists: rootExists,
|
||
driftConfigPath: path.relative(root, driftConfigPath),
|
||
note:
|
||
'lockfileVersion is the hoisted workspace entry in package-lock.json (lockfile v3). clonePackageJsonVersion is the local clone’s package.json when present; mismatch is informational until you bump npm deps or refresh clones.',
|
||
strictTier1Fail,
|
||
tier1MismatchCount: tier1MismatchRows.length,
|
||
rows
|
||
}
|
||
fs.mkdirSync(path.dirname(outJson), { recursive: true })
|
||
fs.writeFileSync(outJson, JSON.stringify(report, null, 2) + '\n')
|
||
const ndLines = rows.map((r) =>
|
||
JSON.stringify({
|
||
type: 'bare_os_holepunch_lockfile_drift',
|
||
schema: 1,
|
||
atMs,
|
||
clonesRoot,
|
||
...r
|
||
})
|
||
)
|
||
fs.writeFileSync(outNd, ndLines.join('\n') + (ndLines.length ? '\n' : ''))
|
||
const dashPath = path.join(root, 'docs/audit/holepunch-lockfile-drift-dashboard.md')
|
||
const dashLines = [
|
||
'# Holepunch lockfile drift dashboard',
|
||
'',
|
||
`_Generated at \`${new Date(atMs).toISOString()}\`. Regenerate with \`node scripts/report-holepunch-lockfile-drift.mjs\` (see [scripts/README.md](../../scripts/README.md))._`,
|
||
'',
|
||
'| Clone (package) | Tier1 | Lockfile version | Clone `package.json` version | Semver mismatch vs clone |',
|
||
'| --- | --- | --- | --- | --- |'
|
||
]
|
||
for (const r of rows) {
|
||
dashLines.push(
|
||
'| ' +
|
||
[
|
||
r.cloneName,
|
||
r.tier1 ? 'yes' : 'no',
|
||
r.lockfileVersion ?? '—',
|
||
r.clonePackageJsonVersion ?? '—',
|
||
r.semverMismatchWithClone ? 'yes' : 'no'
|
||
].join(' | ') +
|
||
' |'
|
||
)
|
||
}
|
||
dashLines.push(
|
||
'',
|
||
'**Note:** `lockfileVersion` is the hoisted workspace entry in `package-lock.json` (lockfile v3). `clonePackageJsonVersion` is the local mirror’s `package.json` when present; mismatches are informational unless `BARE_OS_HOLEPUNCH_LOCKFILE_DRIFT_TIER1_FAIL=1`. Workspace packages (`bare-os-booter`, `bare-os-protocol`, `bare-os-seeder`) may nest a newer major (for example **`[email protected]`**) while the root hoist stays on **2.x** for dependents that have not moved.',
|
||
'',
|
||
`**Tier1 strict mode:** ${strictTier1Fail ? 'enabled' : 'disabled'} · tier1 mismatches: ${tier1MismatchRows.length}.`,
|
||
'',
|
||
'**Upgrade workflow hint:** expected churn often appears first in `udx-native` and `blind-peering`; when they drift, refresh local clones, review lockfile bumps, then regenerate this dashboard and `holepunch-runtime-compat.json` together before release gating.'
|
||
)
|
||
fs.writeFileSync(dashPath, dashLines.join('\n') + '\n')
|
||
console.log(
|
||
'report-holepunch-lockfile-drift: wrote',
|
||
path.relative(root, outJson),
|
||
'packages=',
|
||
rows.length
|
||
)
|
||
if (strictTier1Fail && tier1MismatchRows.length) {
|
||
console.error(
|
||
`report-holepunch-lockfile-drift: tier1 mismatch strict mode failed (${tier1MismatchRows.length} mismatches)`
|
||
)
|
||
process.exit(1)
|
||
}
|
||
}
|
||
|
||
main()
|