test(ci): [CI-TYPES-01] validate installed Audio and HLS types

This commit is contained in:
Harvey Zhao committed 2026-09-15 19:59:44 +08:00
1 parent 210484c443
commit 00c131bff1
16 files changed
+1246 -12

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
# Installed TypeScript consumers
`packages.ts` selects reviewed package fixtures from `test/types/`. It runs only
for packages actually packed and installed by `package-check.mjs`. Adding a
registry entry requires checking the complete fixture against its published type
contract; do not replace historical namespace or overload checks with import-only
smoke tests. Other packages retain their specialized `refactor/scripts/*package-types.mjs`
checks until deliberately integrated.
`types.ts` owns the compiler matrix, strict compiler options, realpath isolation,
and diagnostic accounting. TS 5.9.3 covers node10 CommonJS, NodeNext CommonJS and
ESM, and bundler ESM; TS 4.3.5 covers node10 CommonJS. The single compiler adapter
uses only the shared API and never sends newer resolution modes to TS 4.3.
Both compilers are existing pinned development dependencies; no runtime dependency
is introduced.
Fixtures must use package imports and put each `@ts-expect-error` immediately
before its single-line invalid statement. First compile with directives; then
neutralize directives without moving lines and require diagnostics at every
invalid statement and nowhere else. Missing declarations and widened `any` must
fail. Every parsed file must resolve inside the isolated installation or be a
standard library file from the selected compiler's exact library directory.
The temporary input is removed in `finally`; the caller owns installation cleanup.
Run `yarn test:package --browser` for the installed roster or
`yarn test:package --include=artplayer-plugin-audio-track,artplayer-plugin-hls-control`
for these two additions. `report.json` keeps `typeScope`/`runtimeScope` for the
existing core/chapter checks and separately records `additionalTypeScope` and
`pluginTypes` (compiler modes, installed declaration paths, negative lines/codes).
This type proof does not imply installed runtime or browser acceptance.
Maintain the module with `yarn typecheck:docs-tools`, `yarn lint`, and
`node --test test/installed-types.test.js` (also included in `yarn test:node`).
The tests compile a real isolated package and reject a missing declaration,
`any` widening, and a package link outside the consumer.
+26
View File
@@ -0,0 +1,26 @@
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { checkTypeFixture, typeMatrix } from './types.ts'
const fixtures: Record<string, string> = {
'artplayer-plugin-audio-track': 'audio-track.ts',
'artplayer-plugin-hls-control': 'hls-control.ts',
}
export function installedPluginTypes(directory: string, packages: string[]) {
const results = []
for (const name of packages) {
const fixture = fixtures[name]
if (!fixture)
continue
const source = fs.readFileSync(fileURLToPath(new URL(`../../test/types/${fixture}`, import.meta.url)), 'utf8')
const modes = typeMatrix.map(({ compiler, mode }) => checkTypeFixture(directory, source, compiler, mode))
for (const result of modes)
assert(result.declarations.some(file => file.startsWith(`node_modules/${name}/`)), `Missing installed declaration for ${name}`)
results.push({ name, fixture: path.posix.join('test/types', fixture), fixtureSha256: createHash('sha256').update(source).digest('hex'), modes })
}
return results
}
+71
View File
@@ -0,0 +1,71 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import ts from 'typescript'
import compat from 'typescript-compat'
type Mode = 'node10-commonjs' | 'nodenext-cjs' | 'nodenext-esm' | 'bundler-esm'
// Both pinned compilers provide this stable API; newer resolution modes never
// reach TS 4.3. Keep the version boundary here, outside consumer fixtures.
export const typeMatrix: { compiler: typeof ts, mode: Mode }[] = [
...(['node10-commonjs', 'nodenext-cjs', 'nodenext-esm', 'bundler-esm'] as const).map(mode => ({ compiler: ts, mode })),
{ compiler: compat as unknown as typeof ts, mode: 'node10-commonjs' },
]
function inside(root: string, file: string) {
const relative = path.relative(root, file)
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative))
}
export function checkTypeFixture(directory: string, source: string, compiler: typeof ts, mode: Mode) {
const root = fs.realpathSync(directory)
const next = mode.startsWith('nodenext')
const extension = next ? mode.endsWith('-cjs') ? 'cts' : 'mts' : 'ts'
const file = path.join(root, `installed-consumer.${extension}`)
const options: ts.CompilerOptions = {
strict: true,
noEmit: true,
skipLibCheck: false,
types: [],
esModuleInterop: true,
target: compiler.ScriptTarget.ES2020,
lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'],
module: next ? compiler.ModuleKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleKind.ESNext : compiler.ModuleKind.CommonJS,
moduleResolution: next ? compiler.ModuleResolutionKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleResolutionKind.Bundler : compiler.ModuleResolutionKind.NodeJs,
}
const library = fs.realpathSync(path.dirname(compiler.getDefaultLibFilePath(options)))
function compile(text: string) {
fs.writeFileSync(file, text)
const program = compiler.createProgram([file], options)
const declarations: string[] = []
for (const input of program.getSourceFiles()) {
const resolved = fs.realpathSync(input.fileName)
const standard = path.dirname(resolved) === library && /^lib\..*\.d\.ts$/.test(path.basename(resolved))
assert(inside(root, resolved) || standard, `Consumer input escaped installation: ${resolved}`)
if (input.isDeclarationFile && !standard)
declarations.push(path.relative(root, resolved).replaceAll('\\', '/'))
}
const diagnostics = compiler.getPreEmitDiagnostics(program).map(diagnostic => ({
code: diagnostic.code,
file: diagnostic.file ? fs.realpathSync(diagnostic.file.fileName) : null,
line: diagnostic.file && diagnostic.start !== undefined ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start).line + 1 : null,
message: compiler.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
}))
return { declarations, diagnostics }
}
try {
const valid = compile(source)
assert.deepEqual(valid.diagnostics, [], `Valid installed consumer failed (${compiler.version}/${mode})`)
const expected = source.split(/\r?\n/).flatMap((line, index) => line.includes('@ts-expect-error') ? [index + 2] : [])
assert(expected.length > 0, 'Consumer needs negative statements')
// Preserve lines so every intentionally invalid call must fail at its own location.
const invalid = compile(source.replaceAll('@ts-expect-error', 'expected type error'))
assert(invalid.diagnostics.every(diagnostic => diagnostic.file === file && expected.includes(diagnostic.line ?? 0)), 'Unexpected negative consumer diagnostics')
assert.deepEqual([...new Set(invalid.diagnostics.map(diagnostic => diagnostic.line))].sort((a, b) => a! - b!), expected, 'Every invalid consumer statement must be rejected')
return { compiler: compiler.version, mode, declarations: valid.declarations, negativeStatements: expected.length, negativeDiagnostics: invalid.diagnostics.map(({ code, line }) => ({ code, line })) }
}
finally {
fs.rmSync(file, { force: true })
}
}
+6
View File
@@ -6,6 +6,7 @@ import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { ensureArchive, hash, readMember } from '../refactor/scripts/releases.mjs'
import { installedPackages } from './browser-validation/scope.ts'
import { installedPluginTypes } from './consumers/packages.ts'
import { consumerDirectory, names as defaultNames, readJson, removeConsumer, run, runtimeConsumer, typeConsumers, workspace, writeJson } from './package-consumer.mjs'
export function packageOptions(args) {
@@ -179,15 +180,20 @@ export async function checkPackages({ release = false, include = [] } = {}) {
assert.deepEqual(runtime.observations.api, oldRuntime.observations.api, 'Published API shape/defaults changed')
const types = typeConsumers(installed)
const preciseTypes = typeConsumers(installed, { precise: true })
const pluginTypes = installedPluginTypes(installed, include)
const source = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: workspace, encoding: 'utf8' }).trim()
const publishedPackages = baseline.releases.map(({ name, version, files }) => ({ name, version, files }))
const report = { task: 'ENG-07', capturedAt: new Date().toISOString(), source, toolchain: { yarn: '1.22.22', yarnPath: yarn }, node: process.versions.node, packages, runtimeScope: defaultNames, typeScope: defaultNames, additionalBrowserPackages: include, runtime, publishedPackages, publishedRuntime: oldRuntime, types, preciseTypes, knownRuntimeBlockers: runtime.observations.defaultsWithoutNavigator.resolved ? 0 : 1, knownTypeBlockers: [...types, ...preciseTypes].reduce((sum, result) => sum + result.diagnostics.length, 0) }
report.additionalTypeScope = pluginTypes.map(result => result.name)
report.pluginTypes = pluginTypes
writeJson(path.join(output, 'report.json'), report)
writeJson(path.join(output, 'browser-artifacts.json'), artifacts)
writeJson(path.join(parent, 'latest.json'), { output: path.relative(workspace, output).replaceAll('\\', '/') })
console.log(`Installed tarball contracts passed: ${runtime.checks.length} runtime checks; ${types.filter(t => !t.diagnostics.length).length}/${types.length} legacy type modes; ${preciseTypes.filter(t => !t.diagnostics.length).length}/${preciseTypes.length} precise type modes. Report: ${output}`)
if (include.length)
console.log(`Runtime/type fixture scope: ${defaultNames.join(', ')}. Additional packages require their own consumer and browser acceptance: ${include.join(', ')}.`)
if (pluginTypes.length)
console.log(`Additional installed type fixtures passed: ${pluginTypes.map(result => result.name).join(', ')}; five compiler modes with checked negative statements per package. Runtime/browser acceptance remains separate.`)
if (release) {
assert.equal(report.knownTypeBlockers, 0, 'Known type blockers remain; this candidate is not release-ready')
assert.equal(report.knownRuntimeBlockers, 0, 'Known runtime blockers remain; this candidate is not release-ready')
+1
View File
@@ -13,6 +13,7 @@
"checkJs": true
},
"include": [
"consumers/**/*.ts",
"docs-smoke/**/*.ts",
"build-test.js",
"build-ts.js",