mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-18 19:01:20 -08:00
test(toolchain): [MOD-01] evaluate isolated Bun installs and retain Yarn
This commit is contained in:
1 parent
ddf3d6ccbc
commit
18215b97c0
14 files changed
+2401
-5
No files matched your search
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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`)
|
||||
}
|
||||
@@ -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) }
|
||||
})
|
||||
Reference in new issue
Block a user