gongxiangp2/scripts/database-migrator-lock.mjs

102 lines
4.5 KiB
JavaScript

import assert from 'node:assert/strict'
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const MANIFEST = 'workers/database-migrator/package.json'
const LOCK = 'workers/database-migrator/package-lock.json'
const DIRECT_DEPENDENCIES = ['@prisma/client', 'prisma', 'tsx', 'zod']
// Preserve the main lock's exact locations, versions and integrity hashes. In
// particular Prisma 5's nested engines must not resolve to the app's Prisma 6
// engines. npm ci consumes this subset without resolving a new dependency tree.
export function createMigratorLock(rootLock, manifest) {
assert.equal(rootLock.lockfileVersion, 3, 'Main lock must use lockfileVersion 3')
assert.deepEqual(Object.keys(manifest.dependencies).sort(), DIRECT_DEPENDENCIES,
'Migrator direct dependencies must be explicitly reviewed')
assert.ok(!manifest.devDependencies && !manifest.optionalDependencies,
'Migrator manifest cannot introduce unreviewed dependency groups')
const packages = {}
const visit = packagePath => {
if (packages[packagePath]) return
const record = rootLock.packages[packagePath]
assert.ok(record && !record.link, `Missing or linked dependency: ${packagePath}`)
assert.ok(record.version && record.resolved && record.integrity,
`Dependency must have a pinned version, source and integrity: ${packagePath}`)
const copy = structuredClone(record)
// These packages are all runtime dependencies of the one-shot image.
delete copy.dev
delete copy.devOptional
packages[packagePath] = copy
const resolve = name => {
let parent = packagePath
while (parent) {
const candidate = `${parent}/node_modules/${name}`
if (rootLock.packages[candidate]) return candidate
const index = parent.lastIndexOf('/node_modules/')
parent = index < 0 ? '' : parent.slice(0, index)
}
const candidate = `node_modules/${name}`
assert.ok(rootLock.packages[candidate], `Unresolved dependency ${name} from ${packagePath}`)
return candidate
}
for (const name of Object.keys({ ...record.dependencies, ...record.optionalDependencies })) {
visit(resolve(name))
}
for (const name of Object.keys(record.peerDependencies ?? {})) {
// A newly introduced optional peer could change runtime behavior even
// when npm accepts its absence. Review it instead of silently omitting it.
if (record.peerDependenciesMeta?.[name]?.optional) {
assert.ok(packagePath === 'node_modules/@prisma/client' && name === 'prisma',
`Unreviewed optional peer ${name} from ${packagePath}`)
}
visit(resolve(name))
}
}
for (const [name, version] of Object.entries(manifest.dependencies)) {
const packagePath = `node_modules/${name}`
assert.equal(rootLock.packages[packagePath]?.version, version,
`${name} differs from the main lock; review and update the migrator manifest`)
visit(packagePath)
}
return {
name: manifest.name,
version: manifest.version,
lockfileVersion: 3,
requires: true,
packages: {
'': {
name: manifest.name,
version: manifest.version,
dependencies: manifest.dependencies,
engines: manifest.engines,
},
...Object.fromEntries(Object.entries(packages).sort(([a], [b]) => a.localeCompare(b, 'en'))),
},
}
}
async function main() {
const [mode = '--check', ...extra] = process.argv.slice(2)
assert.ok(['--check', '--write'].includes(mode) && extra.length === 0,
'Usage: node scripts/database-migrator-lock.mjs [--check|--write]')
const rootLock = JSON.parse(await readFile(path.join(ROOT, 'package-lock.json'), 'utf8'))
const manifest = JSON.parse(await readFile(path.join(ROOT, MANIFEST), 'utf8'))
const expected = createMigratorLock(rootLock, manifest)
if (mode === '--write') {
await writeFile(path.join(ROOT, LOCK), `${JSON.stringify(expected, null, 2)}\n`)
} else {
const actual = JSON.parse(await readFile(path.join(ROOT, LOCK), 'utf8'))
assert.deepEqual(actual, expected,
'Migrator lock differs from the main lock. Run node scripts/database-migrator-lock.mjs --write and review it.')
}
console.log(`Migrator lock ${mode === '--write' ? 'written' : 'verified'}: ${Object.keys(expected.packages).length - 1} pinned packages`)
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch(error => { console.error(error.message); process.exitCode = 1 })
}