Files
ArtPlayer/scripts/build-types.mjs
T

91 lines
4.6 KiB
JavaScript

import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { ESLint } from 'eslint'
import ts from 'typescript'
const workspace = fileURLToPath(new URL('../', import.meta.url))
const normalize = value => value.replaceAll('\\', '/')
export async function generateCoreDeclarations({ root = workspace, write = false } = {}) {
const packageRoot = path.join(root, 'packages/artplayer')
const sourceRoot = path.join(packageRoot, 'public')
const outputRoot = path.join(packageRoot, 'types')
const files = ts.sys.readDirectory(sourceRoot, ['.ts', '.mts', '.cts']).sort()
assert(files.length > 0, 'No public TypeScript sources found')
assert(files.every(file => !/\.d\.[cm]?ts$/.test(file)), 'Public sources must be TypeScript, not copied declaration artifacts')
const options = {
strict: true,
skipLibCheck: false,
types: [],
target: ts.ScriptTarget.ES2020,
lib: ['lib.es2020.d.ts', 'lib.dom.d.ts', 'lib.dom.iterable.d.ts'],
module: ts.ModuleKind.NodeNext,
moduleResolution: ts.ModuleResolutionKind.NodeNext,
declaration: true,
emitDeclarationOnly: true,
noEmitOnError: true,
rootDir: sourceRoot,
outDir: outputRoot,
newLine: ts.NewLineKind.LineFeed,
}
const outputs = new Map()
const program = ts.createProgram(files, options)
const formatHost = { getCurrentDirectory: () => root, getCanonicalFileName: name => name, getNewLine: () => '\n' }
const errors = ts.getPreEmitDiagnostics(program)
assert.equal(errors.length, 0, ts.formatDiagnosticsWithColorAndContext(errors, formatHost))
for (const source of program.getSourceFiles()) {
if (program.isSourceFileDefaultLibrary(source))
continue
assert(files.includes(source.fileName), `Public declarations depend on a non-public source: ${normalize(path.relative(root, source.fileName))}`)
}
const emitted = program.emit(undefined, (file, text) => {
const relative = normalize(path.relative(outputRoot, file))
assert(!relative.startsWith('../') && !path.isAbsolute(relative) && /\.d\.[cm]?ts$/.test(relative), `Unexpected declaration output: ${file}`)
outputs.set(relative, text)
})
assert.equal(emitted.emitSkipped, false, ts.formatDiagnosticsWithColorAndContext(emitted.diagnostics, formatHost))
assert.equal(outputs.size, files.length, 'Each public source must emit one declaration')
const eslint = new ESLint({ cwd: workspace, fix: true, fixTypes: ['layout'] })
const generated = new Map()
for (const [relative, content] of outputs) {
const source = `public/${relative.replace('.d.', '.')}`
// Declaration emit omits line comments. This bridge intentionally merges
// the constructor value, instance alias and CJS named-type namespace.
const bridge = ['artplayer.d.cts', 'runtime.d.cts', 'runtime.d.ts'].includes(relative) ? '/* eslint-disable ts/no-redeclare -- Constructor, instance and named types share the export. */\n' : ''
const code = `// Generated from ${source} by yarn build:types. Do not edit.\n${bridge}${content}`
const [result] = await eslint.lintText(code, { filePath: path.join(workspace, 'packages/artplayer/types', relative) })
if (result.errorCount) {
const formatter = await eslint.loadFormatter('stylish')
throw new Error(formatter.format([result]))
}
generated.set(relative, result.output || code)
}
const existing = ts.sys.readDirectory(outputRoot, ['.d.ts', '.d.mts', '.d.cts'])
for (const file of existing)
assert(generated.has(normalize(path.relative(outputRoot, file))), `Declaration has no public source: ${file}`)
const changed = []
for (const [relative, text] of generated) {
const file = path.join(outputRoot, relative)
const actual = fs.existsSync(file) ? fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n') : null
if (actual === text)
continue
changed.push(relative)
if (write) {
fs.mkdirSync(path.dirname(file), { recursive: true })
fs.writeFileSync(file, text)
}
}
assert(write || changed.length === 0, `Generated core declarations are stale: ${changed.join(', ')}. Run yarn build:types.`)
return { files: [...generated.keys()].sort(), changed }
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
assert(process.argv.length === 3 && ['--write', '--check'].includes(process.argv[2]), 'Use build-types.mjs --write or --check')
const result = await generateCoreDeclarations({ write: process.argv[2] === '--write' })
console.log(`Core declarations ${process.argv[2]}: ${result.files.length} files; ${result.changed.length} changed`)
}