fix(dash): [PKG-DASH-05] align nullable SDK metadata and validate real types

This commit is contained in:
Harvey Zhao committed 2026-09-12 06:07:03 +08:00
1 parent c4b5f5f454
commit 2ad00730ee
25 files changed
+1255 -25

No files matched your search

+1 -1
View File
@@ -70,7 +70,7 @@ const option: dash.Option = { quality: { getName: level => String(level.height)
assert.equal(invalid.length, 8, 'Installed package must reject all invalid uses')
matrix.push({ compiler: compiler.version, mode, diagnostics, invalid })
}
const report = { task: 'PKG-DASH-04', scope: 'Packed current core and DASH, offline Yarn install and frozen reinstall outside workspace; isolated public type acceptance only, not complete package runtime/SDK/media release validation.', packages, matrix }
const report = { suite: 'dash-isolated-package-types', introducedBy: 'PKG-DASH-04', scope: 'Packed current core and DASH, offline Yarn install and frozen reinstall outside workspace; isolated public type acceptance only, not complete package runtime/SDK/media release validation.', packages, matrix }
writeJson(path.join(output, 'report.json'), report)
console.log(`DASH installed type matrix passed: ${matrix.length} modes, eight invalid uses rejected in each; ${output}`)
}
+93
View File
@@ -0,0 +1,93 @@
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 ts from 'typescript'
import compat from 'typescript-compat'
import { archiveFiles, ensureArchive, hash, readMember, refactorDir } from './releases.mjs'
const root = path.resolve(refactorDir, '..')
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
async function copyRelease(release, directory, members) {
const archive = await ensureArchive(release)
if (!members)
assert.deepEqual(archiveFiles(archive), Object.keys(release.files).sort())
for (const member of members || Object.keys(release.files)) {
const bytes = readMember(archive, member)
assert.equal(hash(bytes), release.files[member], member)
const destination = path.resolve(directory, 'node_modules', release.name, member.slice('package/'.length))
assert(destination.startsWith(path.resolve(directory, 'node_modules') + path.sep))
fs.mkdirSync(path.dirname(destination), { recursive: true })
fs.writeFileSync(destination, bytes)
}
}
export async function checkDashSDKTypes() {
const sdkInputs = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/dash-sdk.json')))
const dependencies = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/dash-type-dependencies.json')))
const directory = fs.mkdtempSync(path.join(refactorDir, '.cache/dash-sdk-types-'))
const results = []
try {
for (const { release } of sdkInputs.sdks) {
const workspace = path.join(directory, release.version)
await copyRelease(release, workspace, ['package/package.json', 'package/index.d.ts'])
if (release.version === '5.2.1') {
for (const dependency of dependencies.releases)
await copyRelease(dependency, workspace)
const manifest = JSON.parse(fs.readFileSync(path.join(workspace, 'node_modules/dashjs/package.json')))
assert.equal(manifest.dependencies['@svta/cml-request'], '1.0.12')
for (const dependency of dependencies.releases) {
const metadata = JSON.parse(fs.readFileSync(path.join(workspace, 'node_modules', dependency.name, 'package.json')))
for (const [name, version] of Object.entries(metadata.peerDependencies || {}))
assert(dependencies.releases.some(peer => peer.name === name && peer.version === version), `Missing exact peer ${name}@${version}`)
}
}
const fixture = path.join(refactorDir, `fixtures/consumers/dash-sdk-v${release.version[0]}.ts`)
const source = fs.readFileSync(fixture, 'utf8')
assert(source.includes('// Candidate plugin consumer.'))
for (const [compiler, mode] of modes) {
const next = mode.startsWith('nodenext')
const filename = path.join(workspace, `consumer.${next ? mode.endsWith('-cjs') ? 'cts' : 'mts' : 'ts'}`)
const options = { 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 }
function compile(code) {
fs.writeFileSync(filename, code)
const program = compiler.createProgram([filename], options)
const files = program.getSourceFiles().map(file => path.resolve(file.fileName))
assert(files.includes(path.join(workspace, 'node_modules/dashjs/index.d.ts')), 'Actual SDK declaration was not loaded')
const hasPlugin = files.some(file => fs.realpathSync(file) === fs.realpathSync(path.join(root, 'packages/artplayer-plugin-dash-control/types/artplayer-plugin-dash-control.d.ts')))
assert.equal(hasPlugin, code.includes('// Candidate plugin consumer.'), 'Only plugin consumers must load the candidate declaration')
const diagnostics = compiler.getPreEmitDiagnostics(program).map(item => ({
file: item.file ? path.relative(workspace, item.file.fileName).replaceAll('\\', '/').replace(/^consumer\.[cm]?ts$/, 'consumer.ts') : null,
code: item.code,
line: item.file && item.start !== undefined ? item.file.getLineAndCharacterOfPosition(item.start).line + 1 : null,
message: compiler.flattenDiagnosticMessageText(item.messageText, '\n').replaceAll(workspace.replaceAll('\\', '/'), '<consumer>').replaceAll(root.replaceAll('\\', '/'), '<workspace>'),
}))
return { diagnostics, files: files.filter(file => file.startsWith(path.join(workspace, 'node_modules'))).map(file => path.relative(workspace, file).replaceAll('\\', '/')) }
}
const sdkSource = source.split('// Candidate plugin consumer.')[0]
.replace(/^import .+ from '(?:artplayer|artplayer-plugin-dash-control)'\r?\n/gm, '')
const sdkOnly = compile(sdkSource)
const candidate = compile(source)
const invalid = compile(source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, ''))
results.push({ sdk: release.version, compiler: compiler.version, mode, sdkOnly, candidate, invalid })
}
}
return results
}
finally {
const resolved = fs.realpathSync(directory)
assert.equal(path.dirname(resolved), fs.realpathSync(path.join(refactorDir, '.cache')))
assert(path.basename(resolved).startsWith('dash-sdk-types-'))
fs.rmSync(resolved, { recursive: true, force: true })
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const results = await checkDashSDKTypes()
const report = path.join(refactorDir, '.cache/dash-sdk-type-diagnostics.json')
fs.writeFileSync(report, `${JSON.stringify(results, null, 2)}\n`)
console.log(results.map(row => ({ sdk: row.sdk, compiler: row.compiler, mode: row.mode, sdkOnly: row.sdkOnly.diagnostics.length, candidate: row.candidate.diagnostics.length, invalid: row.invalid.diagnostics.length })))
console.log(report)
}
+23
View File
@@ -0,0 +1,23 @@
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 -- Compare exact actual SDK compiler diagnostics.
import test from 'node:test'
import { checkDashSDKTypes } from './dash-sdk-types.mjs'
import { refactorDir } from './releases.mjs'
test('actual DASH SDK types retain upstream diagnostics while candidate options add none', async () => {
const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/dash-sdk-type-diagnostics.json')))
const results = await checkDashSDKTypes()
assert.equal(results.length, 10)
assert.equal(baseline.matrix.length, results.length)
for (const row of results) {
const expected = baseline.matrix.find(item => item.sdk === row.sdk && item.compiler === row.compiler && item.mode === row.mode)
assert(expected, `Missing frozen SDK row ${row.sdk}/${row.compiler}/${row.mode}`)
assert.deepEqual(row.sdkOnly, expected.sdkOnly, 'Upstream SDK declaration diagnostics or resolved files changed')
assert.deepEqual(row.candidate, row.sdkOnly, 'Candidate plugin added errors to an actual SDK consumer')
const errors = row.invalid.diagnostics.filter(item => item.file === 'consumer.ts')
assert.deepEqual(errors.map(item => item.code), [2769, 2339], 'Invalid formatter return and missing SDK field must both be rejected')
assert.deepEqual(row.invalid.diagnostics.filter(item => item.file !== 'consumer.ts'), row.sdkOnly.diagnostics)
}
})
+2 -2
View File
@@ -30,8 +30,8 @@ export function readMember(archive, member) {
}
export async function ensureArchive(release) {
assert(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(release.name) && /^\d+\.\d+\.\d+$/.test(release.version), 'Invalid release identifier')
const archive = path.join(cacheDir, `${release.name}-${release.version}.tgz`)
assert(/^(?:@[a-z0-9]+(?:[.-][a-z0-9]+)*\/)?[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(release.name) && /^\d+\.\d+\.\d+$/.test(release.version), 'Invalid release identifier')
const archive = path.join(cacheDir, `${release.name.replace('/', '+')}-${release.version}.tgz`)
if (!fs.existsSync(archive)) {
const url = new URL(release.tarball)
assert.equal(url.origin, 'https://registry.npmjs.org', 'Unexpected registry')
+1 -1
View File
@@ -11,7 +11,7 @@ test('release integrity rejects corrupted bytes and inconsistent frozen digests'
})
test('release cache rejects unsafe package and version path identifiers before I/O', async () => {
for (const name of ['../hls.js', '.hls', 'hls..js', 'hls/js', 'hls\\js', 'hls.js/evil', '-hls'])
for (const name of ['../hls.js', '.hls', 'hls..js', 'hls/js', 'hls\\js', 'hls.js/evil', '-hls', '@svta/../outside', '@../outside', '@svta/cml/request', '@svta\\cml-request', '@svta+cml-request', '@svta/', '@svta/.hidden'])
await assert.rejects(ensureArchive({ name, version: '1.5.17' }), /Invalid release identifier/)
for (const version of ['../1.5.17', '1.5.17/evil', 'latest'])
await assert.rejects(ensureArchive({ name: 'hls.js', version }), /Invalid release identifier/)