test(toolchain): [MOD-01] evaluate isolated Bun installs and retain Yarn

This commit is contained in:
Harvey Zhao committed 2026-09-15 01:14:46 +08:00
1 parent ddf3d6ccbc
commit 18215b97c0
14 files changed
+2401 -5

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
import assert from 'node:assert/strict'
import { execFileSync, spawnSync } from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { hash } from './releases.mjs'
const root = fileURLToPath(new URL('../../', import.meta.url))
const parent = path.join(root, 'refactor/.cache/bun-evaluation')
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'))
const write = (file, value) => fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`)
assert.equal(process.versions.node, fs.readFileSync(path.join(root, '.node-version'), 'utf8').trim())
const phase = process.argv[2]
assert(['prepare', 'yarn', 'bun-frozen', 'bun-migrate', 'bun-repeat'].includes(phase), 'Use prepare, yarn, bun-frozen, bun-migrate or bun-repeat')
fs.mkdirSync(parent, { recursive: true })
if (phase === 'prepare') {
assert.equal(process.argv.length, 3)
const output = fs.mkdtempSync(path.join(parent, 'run-'))
const checkoutRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'artplayer-install-'))
assert(!path.resolve(checkoutRoot).startsWith(path.resolve(root)), 'Install checkouts must stay outside the repository')
const source = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', windowsHide: true }).trim()
const snapshot = { task: 'MOD-01', source, createdAt: new Date().toISOString(), node: process.versions.node, checkoutRoot, scope: 'Detached checkouts outside the repository with independent node_modules; default Yarn unchanged', rootFilesAtPreparation: {}, checkouts: {}, sourceFiles: {} }
for (const file of ['package.json', 'yarn.lock']) snapshot.rootFilesAtPreparation[file] = hash(fs.readFileSync(path.join(root, file)))
for (const name of ['yarn', 'bun-frozen', 'bun-migrate']) {
const checkout = path.join(checkoutRoot, name)
const log = execFileSync('git', ['-c', 'core.autocrlf=false', '-c', 'core.eol=lf', 'worktree', 'add', '--detach', checkout, source], { cwd: root, encoding: 'utf8', windowsHide: true })
fs.writeFileSync(path.join(output, `${name}-checkout.log`), log)
assert(!fs.existsSync(path.join(checkout, 'node_modules')))
snapshot.checkouts[name] = checkout
snapshot.sourceFiles[name] = Object.fromEntries(['package.json', 'yarn.lock'].map(file => [file, hash(fs.readFileSync(path.join(checkout, file)))]))
}
write(path.join(output, 'snapshot.json'), snapshot)
write(path.join(parent, 'latest.json'), { output: path.relative(root, output).replaceAll('\\', '/') })
console.log(`Prepared isolated install comparison: ${output}`)
}
else {
const output = fs.realpathSync(path.resolve(root, process.argv[3] || ''))
assert.equal(path.dirname(output), fs.realpathSync(parent), 'Use a direct evaluation directory')
const snapshot = read(path.join(output, 'snapshot.json'))
const checkoutPhase = phase === 'bun-repeat' ? 'bun-migrate' : phase
const checkout = fs.realpathSync(snapshot.checkouts[checkoutPhase])
assert.equal(path.dirname(checkout), fs.realpathSync(snapshot.checkoutRoot))
assert(!checkout.startsWith(fs.realpathSync(root) + path.sep), 'Install checkout must not inherit the original node_modules')
assert.equal(path.basename(checkout), checkoutPhase)
if (phase === 'bun-repeat')
assert(fs.existsSync(path.join(checkout, 'bun.lock')), 'Repeat requires the generated Bun lock')
else
assert(!fs.existsSync(path.join(checkout, 'node_modules')), 'Initial installation requires a clean checkout')
const executable = phase === 'yarn' ? process.execPath : fs.realpathSync(process.argv[4] || '')
const yarn = process.env.npm_execpath
if (phase === 'yarn') {
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Run through pinned Yarn')
assert(yarn && fs.existsSync(yarn))
}
else {
assert.equal(execFileSync(executable, ['--version'], { encoding: 'utf8', windowsHide: true }).trim(), '1.4.2', 'This evaluation pins Bun 1.4.2')
}
const cache = process.env.ARTPLAYER_EVAL_CACHE ? fs.realpathSync(process.env.ARTPLAYER_EVAL_CACHE) : path.join(output, `${phase}-cache`)
assert(path.resolve(cache).startsWith(fs.realpathSync(parent) + path.sep), 'Keep install caches inside the evaluation area')
const coldPackageCache = !fs.existsSync(cache) || fs.readdirSync(cache).length === 0
const args = phase === 'yarn'
? [yarn, 'install', '--frozen-lockfile', '--non-interactive', '--cache-folder', cache]
: ['install', ...(['bun-frozen', 'bun-repeat'].includes(phase) ? ['--frozen-lockfile'] : []), '--cache-dir', cache, '--backend=copyfile']
const guardedFiles = ['package.json', 'yarn.lock', ...(phase === 'bun-repeat' ? ['bun.lock'] : [])]
const identities = Object.fromEntries(guardedFiles.map(file => [file, hash(fs.readFileSync(path.join(checkout, file)))]))
const start = performance.now()
const log = fs.openSync(path.join(output, `${phase}-install.log`), 'wx')
let result
try {
result = spawnSync(executable, args, { cwd: checkout, encoding: 'utf8', stdio: ['ignore', log, log], timeout: 600000, windowsHide: true, env: { ...process.env, NODE_PATH: '', PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1', BUN_TELEMETRY: '0' } })
}
finally { fs.closeSync(log) }
const after = Object.fromEntries(Object.keys(identities).map(file => [file, hash(fs.readFileSync(path.join(checkout, file)))]))
assert.deepEqual(after, identities, 'Evaluation modified source manifest or Yarn lock')
const record = { task: 'MOD-01', phase, source: snapshot.source, node: process.versions.node, executable, args, durationMs: performance.now() - start, exitCode: result.status, signal: result.signal, error: result.error?.message, sourceFilesUnchanged: true, bunLock: fs.existsSync(path.join(checkout, 'bun.lock')) ? hash(fs.readFileSync(path.join(checkout, 'bun.lock'))) : null, log: `${phase}-install.log`, coldPackageCache, browserDownloadExcluded: true }
write(path.join(output, `${phase}-install.json`), record)
console.log(JSON.stringify(record))
process.exitCode = result.status ?? 1
}
@@ -0,0 +1,38 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { hash } from './releases.mjs'
const output = path.resolve(process.argv[2])
const snapshot = JSON.parse(fs.readFileSync(path.join(output, 'snapshot.json'), 'utf8'))
function inventory(root) {
const files = {}
function visit(relative) {
const file = path.join(root, relative)
const stat = fs.lstatSync(file)
assert(!stat.isSymbolicLink(), `Unexpected generated link: ${relative}`)
if (stat.isDirectory()) {
for (const name of fs.readdirSync(file)) visit(`${relative}/${name}`)
}
else {
files[relative] = { bytes: stat.size, sha256: hash(fs.readFileSync(file)) }
}
}
for (const name of fs.readdirSync(path.join(root, 'packages'))) {
for (const directory of ['dist', 'types']) {
const relative = `packages/${name}/${directory}`
if (fs.existsSync(path.join(root, relative)))
visit(relative)
}
}
for (const relative of ['docs/compiled', 'docs/document', 'docs/assets/ts', 'docs/assets/js']) visit(relative)
return files
}
const yarn = inventory(snapshot.checkouts.yarn)
const bun = inventory(snapshot.checkouts['bun-migrate'])
const files = [...new Set([...Object.keys(yarn), ...Object.keys(bun)])].sort()
const differences = files.filter(file => yarn[file]?.sha256 !== bun[file]?.sha256).map(file => ({ file, yarn: yarn[file] || null, bun: bun[file] || null }))
const report = { task: 'MOD-01', source: snapshot.source, command: 'yarn ci:build under Node for both installed dependency sets', scope: 'All package dist/types and docs compiled/document/assets-ts/assets-js; exact bytes with no normalization', counts: { yarn: Object.keys(yarn).length, bun: Object.keys(bun).length, identical: files.length - differences.length }, differences, inventories: { yarn, bun } }
fs.writeFileSync(path.join(output, 'build-comparison.json'), `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ counts: report.counts, differences: differences.map(item => item.file) }, null, 2))
+37
View File
@@ -0,0 +1,37 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
const directory = path.resolve(process.argv[2])
const read = name => JSON.parse(fs.readFileSync(path.join(directory, name), 'utf8'))
const yarn = read('yarn-graph.json')
const bun = read('bun-graph.json')
const unique = values => [...new Set(values)].sort()
function differences(left, right) {
const a = new Set(left)
const b = new Set(right)
return { yarnOnly: [...a].filter(value => !b.has(value)).sort(), bunOnly: [...b].filter(value => !a.has(value)).sort() }
}
const tools = graph => Object.fromEntries(graph.rootTools.map(edge => [edge.name, edge.resolved]))
const leftTools = tools(yarn)
const rightTools = tools(bun)
assert.deepEqual(Object.keys(leftTools).sort(), Object.keys(rightTools).sort())
const rootToolChanges = Object.keys(leftTools).filter(name => leftTools[name] !== rightTools[name]).map(name => ({ name, yarn: leftTools[name], bun: rightTools[name] }))
const edges = graph => unique(graph.requests.map(edge => `${edge.parent} ${edge.field} ${edge.name}@${edge.range} -> ${edge.resolved}`))
const resources = graph => unique(graph.resources.map(item => `${item.package} ${item.path} ${item.bytes} ${item.sha256}`))
const missing = graph => unique(graph.missing.map(edge => `${edge.parent} ${edge.field} ${edge.name}@${edge.range} optional=${edge.optional}`))
const report = {
task: 'MOD-01',
source: read('snapshot.json').source,
counts: Object.fromEntries([['yarn', yarn], ['bun', bun]].map(([name, graph]) => [name, { packageInstances: graph.packages.length, uniquePackages: unique(graph.packages.map(pkg => `${pkg.name}@${pkg.version}`)).length, resolvedEdges: graph.requests.length, uniqueResources: resources(graph).length }])),
rootToolChanges,
packages: differences(yarn.packages.map(pkg => `${pkg.name}@${pkg.version}`), bun.packages.map(pkg => `${pkg.name}@${pkg.version}`)),
requests: differences(edges(yarn), edges(bun)),
missing: differences(missing(yarn), missing(bun)),
resources: differences(resources(yarn), resources(bun)),
missingRequired: Object.fromEntries([['yarn', yarn], ['bun', bun]].map(([name, graph]) => [name, graph.missing.filter(edge => !edge.optional && edge.field !== 'peerDependencies')])),
note: 'Physical hoisting differences are excluded from semantic comparisons. Missing optional/platform and peer requests remain explicit; this report alone does not determine build compatibility.',
}
fs.writeFileSync(path.join(directory, 'comparison.json'), `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ counts: report.counts, rootToolChanges, resources: report.resources, missingRequired: report.missingRequired }, null, 2))
+85
View File
@@ -0,0 +1,85 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { hash } from './releases.mjs'
export function installedGraph(checkout) {
const root = fs.realpathSync(checkout)
const packages = new Map()
const requests = []
const missing = []
function manifestAt(location) {
const actual = fs.realpathSync(location)
assert(actual.startsWith(root + path.sep), 'Installed dependency escaped isolated checkout')
return { directory: actual, manifest: JSON.parse(fs.readFileSync(path.join(actual, 'package.json'), 'utf8')) }
}
function resolve(from, name) {
const require = createRequire(path.join(from, 'package.json'))
// Audit npm packages even when their names also identify Node builtins.
for (const location of require.resolve.paths('artplayer-install-audit-probe')) {
const directory = path.join(location, name)
if (fs.existsSync(path.join(directory, 'package.json')))
return manifestAt(directory)
}
return null
}
function visit(directory, manifest, workspace = false) {
const key = path.relative(root, directory).replaceAll('\\', '/') || '.'
if (packages.has(key))
return
packages.set(key, { path: key, name: manifest.name, version: manifest.version, license: manifest.license || null, workspace, lifecycle: Object.fromEntries(Object.entries(manifest.scripts || {}).filter(([name]) => ['preinstall', 'install', 'postinstall', 'prepare'].includes(name))) })
const fields = workspace ? ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] : ['dependencies', 'optionalDependencies', 'peerDependencies']
for (const field of fields) {
for (const [name, range] of Object.entries(manifest[field] || {})) {
const dependency = resolve(directory, name)
const edge = { from: key, parent: `${manifest.name}@${manifest.version}`, field, name, range, optional: field === 'optionalDependencies' || (field === 'peerDependencies' && manifest.peerDependenciesMeta?.[name]?.optional === true) }
if (!dependency) {
missing.push(edge)
continue
}
requests.push({ ...edge, resolved: `${dependency.manifest.name}@${dependency.manifest.version}`, to: path.relative(root, dependency.directory).replaceAll('\\', '/') })
visit(dependency.directory, dependency.manifest)
}
}
}
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
visit(root, manifest, true)
for (const name of fs.readdirSync(path.join(root, 'packages'))) {
const directory = path.join(root, 'packages', name)
if (fs.existsSync(path.join(directory, 'package.json'))) {
const entry = manifestAt(directory)
// A workspace first reached through a runtime dependency still needs its dev/peer audit.
packages.delete(path.relative(root, directory).replaceAll('\\', '/'))
visit(directory, entry.manifest, true)
}
}
const resources = []
for (const pkg of packages.values()) {
if (pkg.workspace)
continue
const directory = path.join(root, pkg.path)
function walk(folder) {
for (const entry of fs.readdirSync(folder, { withFileTypes: true })) {
if (entry.name === 'node_modules' || entry.isSymbolicLink())
continue
const file = path.join(folder, entry.name)
if (entry.isDirectory())
walk(file)
else if (entry.isFile() && /\.(?:node|wasm|exe)$|(?:worker|worklet)[^/\\]*\.[cm]?js$/i.test(entry.name))
resources.push({ package: `${pkg.name}@${pkg.version}`, path: path.relative(directory, file).replaceAll('\\', '/'), bytes: fs.statSync(file).size, sha256: hash(fs.readFileSync(file)) })
}
}
walk(directory)
}
return { node: process.versions.node, root, packages: [...packages.values()].sort((a, b) => a.path.localeCompare(b.path)), requests, missing, resources, rootTools: requests.filter(edge => edge.from === '.' && edge.field === 'devDependencies') }
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
assert.equal(process.argv.length, 4, 'Use <checkout> <report.json>')
const graph = installedGraph(process.argv[2])
fs.writeFileSync(process.argv[3], `${JSON.stringify(graph, null, 2)}\n`)
console.log(`Installed graph: ${graph.packages.length} package instances, ${graph.requests.length} resolved edges, ${graph.resources.length} native/worker/WASM resources; ${graph.missing.length} absent optional/peer/required requests retained`)
}
+47
View File
@@ -0,0 +1,47 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
// eslint-disable-next-line test/no-import-node-test -- Verify install comparison cannot borrow ancestor dependencies.
import test from 'node:test'
import { consumerDirectory, removeConsumer } from '../../scripts/package-consumer.mjs'
import { installedGraph } from './install-graph.mjs'
function write(directory, manifest) {
fs.mkdirSync(directory, { recursive: true })
fs.writeFileSync(path.join(directory, 'package.json'), JSON.stringify(manifest))
}
test('installation audit rejects a dependency resolved from the parent checkout', () => {
const directory = consumerDirectory()
try {
const root = path.join(directory, 'checkout')
write(root, { name: 'root', version: '1.0.0', dependencies: { borrowed: '1.0.0' } })
fs.mkdirSync(path.join(root, 'packages'))
write(path.join(directory, 'node_modules/borrowed'), { name: 'borrowed', version: '1.0.0' })
assert.throws(() => installedGraph(root), /escaped isolated checkout/)
}
finally { removeConsumer(directory) }
})
test('installation audit records workspace peers, nested resolution and resource identities', () => {
const directory = consumerDirectory()
try {
write(directory, { name: 'root', version: '1.0.0', devDependencies: { compiler: '1.0.0' }, dependencies: { buffer: '5.7.1' } })
write(path.join(directory, 'node_modules/buffer'), { name: 'buffer', version: '5.7.1' })
write(path.join(directory, 'node_modules/compiler'), { name: 'compiler', version: '1.0.0', dependencies: { engine: '2.0.0' }, scripts: { postinstall: 'node install.js' } })
write(path.join(directory, 'node_modules/compiler/node_modules/engine'), { name: 'engine', version: '2.0.0' })
fs.writeFileSync(path.join(directory, 'node_modules/compiler/node_modules/engine/code.wasm'), 'fixture')
write(path.join(directory, 'packages/plugin'), { name: 'plugin', version: '1.0.0', peerDependencies: { host: '^1' }, peerDependenciesMeta: { host: { optional: true } } })
const graph = installedGraph(directory)
assert.equal(graph.rootTools[0].resolved, 'compiler@1.0.0')
assert.equal(graph.requests.find(item => item.name === 'buffer').resolved, 'buffer@5.7.1')
assert.equal(graph.requests.find(item => item.name === 'engine').resolved, 'engine@2.0.0')
assert.equal(graph.missing[0].name, 'host')
assert.equal(graph.missing[0].optional, true)
assert.equal(graph.resources.length, 1)
assert.equal(graph.resources[0].package, 'engine@2.0.0')
assert.equal(graph.resources[0].bytes, 7)
assert.equal(graph.packages.find(item => item.name === 'compiler').lifecycle.postinstall, 'node install.js')
}
finally { removeConsumer(directory) }
})