102 lines
4.5 KiB
JavaScript
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 })
|
|
}
|