mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
124 lines
8.4 KiB
JavaScript
124 lines
8.4 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { execFileSync } from 'node:child_process'
|
|
import fs from 'node:fs'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import ts from 'typescript'
|
|
import { ensureArchive, hash, readMember, refactorDir } from './releases.mjs'
|
|
|
|
const root = path.resolve(refactorDir, '..')
|
|
const fixtures = path.join(refactorDir, 'fixtures/consumers')
|
|
const parent = os.tmpdir()
|
|
const reportPath = path.join(refactorDir, 'baselines/consumers.json')
|
|
const fixtureNames = ['runtime.cjs', 'public.ts', 'language.ts', 'optional-chapter.ts', 'legacy-plugin.ts']
|
|
export const runtimeChecks = [
|
|
'DIST.cjs', 'DIST.cjs-legacy', 'DIST.esm', 'DIST.esm-default-only', 'DIST.esm-legacy',
|
|
'DIST.global-core', 'DIST.amd-core', 'DIST.global-core-legacy', 'DIST.amd-core-legacy',
|
|
'DIST.global-chapter', 'DIST.amd-chapter', 'DIST.global-chapter-legacy', 'DIST.amd-chapter-legacy',
|
|
'SSR.import-template', 'SSR.constructor-browser-only', 'DIST.i18n-cjs-esm-global',
|
|
'DIST.exports-boundary', 'DIST.direct-esm-file',
|
|
]
|
|
|
|
export async function runConsumers() {
|
|
const releases = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/releases.json'), 'utf8')).releases
|
|
fs.mkdirSync(parent, { recursive: true })
|
|
const dir = fs.mkdtempSync(path.join(parent, 'artplayer-published-'))
|
|
try {
|
|
const provenance = []
|
|
for (const release of releases) {
|
|
const archive = await ensureArchive(release)
|
|
for (const [member, digest] of Object.entries(release.files)) {
|
|
const bytes = readMember(archive, member)
|
|
assert.equal(hash(bytes), digest, `Published member changed: ${member}`)
|
|
const target = path.resolve(dir, 'node_modules', release.name, member.slice('package/'.length))
|
|
assert(target.startsWith(path.join(dir, 'node_modules', release.name) + path.sep), 'Unsafe consumer path')
|
|
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
fs.writeFileSync(target, bytes)
|
|
}
|
|
provenance.push({ name: release.name, version: release.version, integrity: release.integrity, sha256: release.sha256 })
|
|
}
|
|
for (const name of fixtureNames) fs.copyFileSync(path.join(fixtures, name), path.join(dir, name))
|
|
fs.writeFileSync(path.join(dir, 'package.json'), '{"private":true,"type":"module"}\n')
|
|
const runtime = JSON.parse(execFileSync(process.execPath, [path.join(dir, 'runtime.cjs')], { cwd: dir, encoding: 'utf8', timeout: 20000, maxBuffer: 1024 * 1024 }))
|
|
assert.equal(ts.version, JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).devDependencies.typescript, 'Unexpected TypeScript compiler')
|
|
const types = []
|
|
for (const [mode, module, moduleResolution, packageType] of [
|
|
['node10-commonjs', ts.ModuleKind.CommonJS, ts.ModuleResolutionKind.Node10, 'commonjs'],
|
|
['nodenext-cjs', ts.ModuleKind.NodeNext, ts.ModuleResolutionKind.NodeNext, 'commonjs'],
|
|
['nodenext-esm', ts.ModuleKind.NodeNext, ts.ModuleResolutionKind.NodeNext, 'module'],
|
|
['bundler-esm', ts.ModuleKind.ESNext, ts.ModuleResolutionKind.Bundler, 'module'],
|
|
]) {
|
|
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ private: true, type: packageType }))
|
|
for (const name of fixtureNames.filter(name => name.endsWith('.ts'))) {
|
|
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], target: ts.ScriptTarget.ES2020, module, moduleResolution, esModuleInterop: true }
|
|
const program = ts.createProgram([path.join(dir, name)], options)
|
|
const declarations = program.getSourceFiles().filter(file => /\/(?:artplayer|artplayer-plugin-chapter)\/types\//.test(file.fileName.replaceAll('\\', '/')) && file.isDeclarationFile)
|
|
assert(declarations.every(file => file.fileName.startsWith(path.join(dir, 'node_modules').replaceAll('\\', '/'))), 'Types escaped isolated published packages')
|
|
if (name !== 'legacy-plugin.ts' || mode !== 'node10-commonjs') assert(declarations.length > 0, 'Published declarations were not loaded')
|
|
const diagnostics = ts.getPreEmitDiagnostics(program).map(d => ({
|
|
file: d.file ? path.relative(dir, d.file.fileName).replaceAll('\\', '/') : null,
|
|
line: d.file && d.start !== undefined ? d.file.getLineAndCharacterOfPosition(d.start).line + 1 : null,
|
|
code: d.code, message: ts.flattenDiagnosticMessageText(d.messageText, '\n').replaceAll(dir.replaceAll('\\', '/'), '<consumer>').replaceAll(dir, '<consumer>'),
|
|
}))
|
|
types.push({ mode, packageType, fixture: name, diagnostics, declarations: declarations.map(file => path.relative(dir, file.fileName).replaceAll('\\', '/')).sort() })
|
|
}
|
|
}
|
|
return { schemaVersion: 1, task: 'BASE-05', sourceCommit: '2b7054d8', capturedAt: new Date().toISOString(), node: process.version, typescript: ts.version, releases: provenance,
|
|
compilerOptions: { strict: true, skipLibCheck: false, noEmit: true, types: [], lib: ['es2020', 'dom'], target: 'es2020', esModuleInterop: true },
|
|
fixtureHashAlgorithm: 'sha256-lf', fixtures: Object.fromEntries(fixtureNames.map(name => [name, hash(fs.readFileSync(path.join(fixtures, name), 'utf8').replaceAll('\r\n', '\n'))])), runtime, types }
|
|
}
|
|
finally {
|
|
const resolved = fs.realpathSync(dir)
|
|
assert(path.dirname(resolved) === fs.realpathSync(parent) && path.basename(resolved).startsWith('artplayer-published-'), 'Unsafe consumer cleanup')
|
|
fs.rmSync(resolved, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
export function consumerProfile(report) {
|
|
return { releases: report.releases, fixtures: report.fixtures, typescript: report.typescript, compilerOptions: report.compilerOptions, runtime: report.runtime, types: report.types }
|
|
}
|
|
|
|
export function validateConsumerReport(report) {
|
|
assert.equal(report.schemaVersion, 1)
|
|
assert.equal(report.task, 'BASE-05')
|
|
assert.deepEqual(report.runtime.checks, runtimeChecks, 'Missing runtime consumer checks')
|
|
assert.equal(report.fixtureHashAlgorithm, 'sha256-lf')
|
|
assert.equal(report.compilerOptions.skipLibCheck, false)
|
|
assert.equal(report.compilerOptions.strict, true)
|
|
const releases = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/releases.json'), 'utf8')).releases
|
|
assert.deepEqual(report.releases, releases.map(({ name, version, integrity, sha256 }) => ({ name, version, integrity, sha256 })), 'Wrong published consumers')
|
|
assert.equal(report.types.length, 16)
|
|
const cases = new Set(report.types.map(item => `${item.mode}/${item.fixture}`))
|
|
assert.equal(cases.size, 16)
|
|
for (const mode of ['node10-commonjs', 'nodenext-cjs', 'nodenext-esm', 'bundler-esm']) {
|
|
for (const fixture of fixtureNames.filter(name => name.endsWith('.ts'))) {
|
|
const result = report.types.find(item => item.mode === mode && item.fixture === fixture)
|
|
assert(result, 'Missing TS consumer case')
|
|
if (mode !== 'nodenext-esm' && ['public.ts', 'language.ts'].includes(fixture)) assert.deepEqual(result.diagnostics, [], 'Previously valid consumer failed')
|
|
if (mode !== 'nodenext-esm' && fixture === 'optional-chapter.ts') assert.deepEqual(result.diagnostics.map(d => d.code), [2554], 'Historical optional-argument result changed')
|
|
if (mode === 'node10-commonjs' && fixture === 'legacy-plugin.ts') assert.deepEqual(result.diagnostics.map(d => d.code), [2307], 'Historical legacy resolution changed')
|
|
}
|
|
}
|
|
for (const name of fixtureNames) assert.equal(report.fixtures[name], hash(fs.readFileSync(path.join(fixtures, name), 'utf8').replaceAll('\r\n', '\n')), 'Consumer fixture changed; review baseline')
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
const result = await runConsumers()
|
|
validateConsumerReport(result)
|
|
if (process.argv.includes('--capture')) {
|
|
assert(!fs.existsSync(reportPath), 'Do not overwrite frozen consumer results')
|
|
fs.writeFileSync(reportPath, `${JSON.stringify(result, null, 2)}\n`)
|
|
}
|
|
else if (process.argv.includes('--inspect')) {
|
|
console.log(JSON.stringify(result, null, 2))
|
|
}
|
|
else {
|
|
assert(process.argv.slice(2).every(arg => arg === '--check'), 'Use --capture, --inspect or --check')
|
|
const frozen = JSON.parse(fs.readFileSync(reportPath, 'utf8'))
|
|
validateConsumerReport(frozen)
|
|
assert.deepEqual(consumerProfile(result), consumerProfile(frozen), 'Published consumer behavior differs')
|
|
console.log(`Published consumers rerun: ${result.runtime.checks.length} runtime checks, ${result.types.length} strict TS cases; historical diagnostics remain explicit`)
|
|
}
|
|
}
|