mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-13 09:20:45 +08:00
44 lines
2.7 KiB
JavaScript
44 lines
2.7 KiB
JavaScript
/** Track the actual bundle inputs so a release cannot silently ship stale JS. */
|
|
import { createHash } from 'node:crypto'
|
|
import { readFileSync, writeFileSync } from 'node:fs'
|
|
import { dirname, isAbsolute, relative, resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
export const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
const manifestPath = resolve(root, '.release-build.json')
|
|
export const hashText = text => createHash('sha256').update(text.replace(/\r\n/g, '\n')).digest('hex')
|
|
export const hashFile = path => hashText(readFileSync(path, 'utf8'))
|
|
|
|
export function recordBuild(output, result, buildScript) {
|
|
if (!result.metafile) throw new Error('Build metadata is missing; use Bun >=1.3.13.')
|
|
const inputs = [...new Set([
|
|
'package.json', 'pnpm-lock.yaml', buildScript, 'scripts/build-integrity.mjs',
|
|
...Object.keys(result.metafile.inputs).map(path => relative(root, resolve(path)).replaceAll('\\', '/'))
|
|
.filter(path => path.startsWith('src/')),
|
|
])].sort()
|
|
if (!inputs.some(path => path.startsWith('src/'))) throw new Error('Bundle has no recorded source inputs.')
|
|
for (const path of inputs) {
|
|
if (isAbsolute(path) || path.startsWith('../')) throw new Error(`Build input escapes repository: ${path}`)
|
|
}
|
|
let builds = {}
|
|
try { builds = JSON.parse(readFileSync(manifestPath, 'utf8')).builds ?? {} } catch { /* First build. */ }
|
|
const key = relative(root, output).replaceAll('\\', '/')
|
|
builds[key] = { output: hashFile(output), inputs: Object.fromEntries(inputs.map(path => [path, hashFile(resolve(root, path))])) }
|
|
writeFileSync(manifestPath, `${JSON.stringify({ format: 1, builds: Object.fromEntries(Object.entries(builds).sort()) }, null, 2)}\n`)
|
|
}
|
|
|
|
export function checkBuilds(directory = root) {
|
|
const manifest = JSON.parse(readFileSync(resolve(directory, '.release-build.json'), 'utf8'))
|
|
if (manifest.format !== 1) throw new Error('Unknown build manifest format.')
|
|
const pkg = JSON.parse(readFileSync(resolve(directory, 'package.json'), 'utf8'))
|
|
const outputs = [...new Set(Object.values(pkg.exports).filter(path => path.endsWith('.js')).map(path => path.replace(/^\.\//, '')))]
|
|
for (const output of outputs) {
|
|
const build = manifest.builds?.[output]
|
|
if (!build || hashFile(resolve(directory, output)) !== build.output) throw new Error(`Missing or modified artifact: ${output}; run bun run bundle.`)
|
|
for (const [path, hash] of Object.entries(build.inputs)) {
|
|
if (isAbsolute(path) || path.startsWith('../')) throw new Error(`Invalid build input: ${path}`)
|
|
if (hashFile(resolve(directory, path)) !== hash) throw new Error(`Stale artifact ${output}: ${path} changed; run bun run bundle.`)
|
|
}
|
|
}
|
|
return outputs
|
|
}
|