fix(types): [PKG-FACTORY-01] preserve published factory compatibility

This commit is contained in:
Harvey Zhao committed 2026-09-13 16:53:27 +08:00
1 parent 1117107032
commit c4c7491e16
35 files changed
+670 -229

No files matched your search

+46 -14
View File
@@ -50,8 +50,9 @@ async function main() {
const runtimeFile = path.join(consumer, 'exports.cjs')
fs.writeFileSync(runtimeFile, plugin.label === 'candidate'
? `const assert = require('node:assert/strict');
for (const name of ['artplayer-plugin-ambilight', 'artplayer-plugin-ambilight/legacy']) { const factory = require(name); assert.equal(typeof factory, 'function'); assert.equal(factory.default, factory); assert.equal(typeof factory(), 'function'); }
import('artplayer-plugin-ambilight').then(module => { assert.deepEqual(Object.keys(module), ['default']); assert.equal(module.default.default, module.default); }).catch(error => { console.error(error); process.exitCode = 1; });`
for (const name of ['artplayer-plugin-ambilight', 'artplayer-plugin-ambilight/legacy', 'artplayer-plugin-ambilight/runtime']) { const factory = require(name); assert.equal(typeof factory, 'function'); assert.equal(factory.default, factory); assert.equal(typeof factory(), 'function'); }
assert.equal(require('artplayer-plugin-ambilight/runtime'), require('artplayer-plugin-ambilight'));
Promise.all(['artplayer-plugin-ambilight', 'artplayer-plugin-ambilight/runtime', 'artplayer-plugin-ambilight/legacy'].map(name => import(name))).then(([root, runtime, legacy]) => { assert.deepEqual(Object.keys(root), ['default']); assert.deepEqual(Object.keys(runtime), ['default']); assert.equal(runtime.default, root.default); assert.equal(root.default.default, root.default); assert.equal(legacy.default, require('artplayer-plugin-ambilight/legacy')); }).catch(error => { console.error(error); process.exitCode = 1; });`
: `const assert = require('node:assert/strict'); const value = require('artplayer-plugin-ambilight'); assert.equal(typeof value, '${plugin.version === '1.0.0' ? 'object' : 'function'}'); assert.equal(typeof (value.default || value)({}), 'function');`)
fs.writeFileSync(path.join(output, `${plugin.label}-runtime.log`), run([runtimeFile], consumer))
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
@@ -60,14 +61,24 @@ import('artplayer-plugin-ambilight').then(module => { assert.deepEqual(Object.ke
for (const [compiler, mode] of modes) {
const next = mode.startsWith('nodenext')
const extension = next ? mode.endsWith('-cjs') ? 'cts' : 'mts' : 'ts'
const source = fs.readFileSync(path.join(workspace, plugin.label === 'candidate' ? 'test/types/ambilight.ts' : 'refactor/fixtures/consumers/ambilight-published.ts'), 'utf8')
let source = fs.readFileSync(path.join(workspace, plugin.label === 'candidate' ? 'test/types/ambilight.ts' : 'refactor/fixtures/consumers/ambilight-published.ts'), 'utf8')
if (plugin.label === 'candidate' && mode === 'nodenext-esm') {
source = source
.replace('import ambilight from \'artplayer-plugin-ambilight\'', 'import ambilightModule from \'artplayer-plugin-ambilight\'\nconst ambilight = ambilightModule.default')
.replace('import legacy from \'artplayer-plugin-ambilight/legacy\'', 'import legacyModule from \'artplayer-plugin-ambilight/legacy\'\nconst legacy = legacyModule.default')
}
if (mode.endsWith('-no-interop'))
source = source.replace('import runtime from \'artplayer-plugin-ambilight/runtime\'', 'import runtime = require(\'artplayer-plugin-ambilight/runtime\')')
const filename = path.join(consumer, `consumer.${extension}`)
const historical = path.join(consumer, `historical.${extension}`)
fs.copyFileSync(path.join(workspace, 'refactor/fixtures/consumers/ambilight-published.ts'), historical)
const files = plugin.label === 'candidate' ? [filename, historical] : [filename]
if (mode === 'nodenext-cjs' && plugin.label !== 'published-1.1.0') {
const commonjs = path.join(consumer, 'commonjs.cts')
fs.writeFileSync(commonjs, `import ambilight = require('artplayer-plugin-ambilight'); ambilight({ blur: '50px', opacity: 0.5, frequency: 10, zIndex: 9, duration: 0.3 }); ${plugin.label === 'candidate' ? 'ambilight.default();' : ''}`)
const files = [filename]
if (plugin.label === 'candidate' && mode !== 'bundler-esm' && mode !== 'nodenext-esm') {
const commonjs = path.join(consumer, `commonjs.${extension}`)
fs.writeFileSync(commonjs, `import ambilight = require('artplayer-plugin-ambilight'); import legacy = require('artplayer-plugin-ambilight/legacy'); import runtime = require('artplayer-plugin-ambilight/runtime');
import type Artplayer from 'artplayer';
const replacement: typeof ambilight.default = (_option) => (_art: Artplayer) => ({name: 'artplayerPluginAmbilight', start() {}, stop() {}});
const legacyReplacement: typeof legacy.default = replacement;
ambilight.default({}); legacy.default({}); runtime(); runtime.default();
const option: runtime.Option = {}; runtime(option); void legacyReplacement;`)
files.push(commonjs)
}
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: !mode.endsWith('-no-interop'), 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 }
@@ -78,22 +89,43 @@ import('artplayer-plugin-ambilight').then(module => { assert.deepEqual(Object.ke
const actual = fs.realpathSync(file.fileName)
assert(actual.startsWith(fs.realpathSync(consumer) + path.sep) || (program.isSourceFileDefaultLibrary(file) && path.dirname(actual) === fs.realpathSync(path.dirname(compiler.sys.getExecutingFilePath()))), `Types escaped installed consumer: ${actual}`)
}
return compiler.getPreEmitDiagnostics(program).map(item => ({ code: item.code, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n') }))
return compiler.getPreEmitDiagnostics(program).map(item => ({ code: item.code, line: item.file && item.start !== undefined ? item.file.getLineAndCharacterOfPosition(item.start).line + 1 : null, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n').replaceAll(consumer.replaceAll('\\', '/'), '<consumer>') }))
}
const diagnostics = compile(source)
const historicalFailure = plugin.label === 'published-1.1.0' && mode === 'nodenext-esm'
assert.deepEqual(diagnostics.map(item => item.code), historicalFailure ? [2349, 2349, 2322, 2344] : [], `${plugin.label} ${compiler.version} ${mode}`)
const invalid = plugin.label === 'candidate' ? compile(source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')) : []
if (plugin.label === 'candidate')
assert.equal(invalid.length, 10, 'Installed declarations must reject all invalid uses')
matrix.push({ plugin: plugin.label, compiler: compiler.version, mode, historicalFailure, diagnostics, invalid })
if (plugin.label === 'candidate') {
assert.equal(invalid.length, 15, 'Installed declarations must reject all invalid uses')
const expectedLines = []
let line = 1
for (const text of source.split('\n')) {
if (text.startsWith('// @ts-expect-error'))
expectedLines.push(line)
else line++
}
assert.deepEqual(invalid.map(item => item.line).sort((a, b) => a - b), expectedLines, 'Each invalid statement must fail at its own line')
}
const published = plugin.label === 'candidate' ? compile(fs.readFileSync(path.join(workspace, 'refactor/fixtures/consumers/ambilight-published.ts'), 'utf8')) : diagnostics
assert.deepEqual(published.map(item => item.code), (plugin.label === 'candidate' || plugin.label === 'published-1.1.0') && mode === 'nodenext-esm' ? [2349, 2349, 2322, 2344] : [], 'Preserve exact historical direct-consumer diagnostic codes')
const namespace = plugin.label !== 'published-1.0.0' && mode === 'nodenext-esm'
? compile(`import ambilight from 'artplayer-plugin-ambilight'; import type Artplayer from 'artplayer';
const replacement: typeof ambilight.default = (_option) => (_art: Artplayer) => ({name: 'artplayerPluginAmbilight', start() {}, stop() {}});
ambilight.default({}); void replacement;`)
: []
assert.deepEqual(namespace, [], 'Latest published NodeNext namespace remains valid')
const olderRequire = mode !== 'bundler-esm' && mode !== 'nodenext-esm'
? compile(`import ambilight = require('artplayer-plugin-ambilight'); ambilight({blur: '50px', opacity: 0.5, frequency: 10, zIndex: 9, duration: 0.3});`)
: []
assert.deepEqual(olderRequire.map(item => item.code), mode !== 'bundler-esm' && mode !== 'nodenext-esm' && plugin.label !== 'published-1.0.0' ? [2349] : [], 'Earlier export-assignment typing conflicts are retained and documented')
matrix.push({ plugin: plugin.label, compiler: compiler.version, mode, historicalFailure, diagnostics, invalid, published, namespace, olderRequire })
}
}
finally {
removeConsumer(consumer)
}
}
writeJson(path.join(output, 'report.json'), { suite: 'ambilight-isolated-package-types', introducedBy: 'PKG-AMBILIGHT-04', scope: 'Two actual published plugins and packed candidate installed outside workspace with packed candidate core; offline and frozen reinstall, exact bytes, compiler/default/import= contracts. Published 1.0.0 required option fields became optional in 1.1.0; candidate retains 1.1.0 extraction. This is not proxy, device or complete distribution acceptance.', packages, published: candidates.slice(0, 2).map(pkg => ({ label: pkg.label, archive: pkg.archive, sha256: pkg.sha256 })), matrix })
writeJson(path.join(output, 'report.json'), { suite: 'ambilight-isolated-package-types', introducedBy: 'PKG-FACTORY-01', scope: 'Two actual published plugins and packed candidate installed outside workspace with packed candidate core; offline and frozen reinstall, exact bytes, full factory replacements and required root option; runtime optional calls and self alias. Published 1.1.0 NodeNext ESM namespace and four old direct-call diagnostics are retained. Published 1.0.0 export-assignment and required fields have documented migration. This is not proxy, device or complete distribution acceptance.', packages, published: candidates.slice(0, 2).map(pkg => ({ label: pkg.label, archive: pkg.archive, sha256: pkg.sha256 })), matrix })
console.log(`Ambilight installed types passed: ${matrix.length} cases; ${output}`)
}
+21 -7
View File
@@ -14,14 +14,23 @@ import { readMember } from './releases.mjs'
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
test('Ambilight current types retain inference, optional runtime arguments and ten invalid-use rejections', () => {
function publicSource(source, mode) {
return mode === 'nodenext-esm'
? source.replace('import ambilight from \'artplayer-plugin-ambilight\'', 'import ambilightModule from \'artplayer-plugin-ambilight\'\nconst ambilight = ambilightModule.default')
.replace('import legacy from \'artplayer-plugin-ambilight/legacy\'', 'import legacyModule from \'artplayer-plugin-ambilight/legacy\'\nconst legacy = legacyModule.default')
: source
}
test('Ambilight retains published factory replacements and separates accurate optional runtime calls', () => {
const source = fs.readFileSync('test/types/ambilight.ts', 'utf8')
for (const [compiler, mode] of modes) {
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${compiler.version} ${mode}`)
assert.equal(checkConsumer(compiler, mode, source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')).length, 10)
const adapted = publicSource(source, mode)
assert.deepEqual(checkConsumer(compiler, mode, adapted), [], `${compiler.version} ${mode}`)
assert.equal(checkConsumer(compiler, mode, adapted.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')).length, 15)
}
assert.deepEqual(checkConsumer(ts, 'nodenext-cjs', `import ambilight = require('artplayer-plugin-ambilight'); import legacy = require('artplayer-plugin-ambilight/legacy');
const option: ambilight.Option = {}; ambilight(option); ambilight.default(option); legacy(); legacy.default(option);`), [])
import runtime = require('artplayer-plugin-ambilight/runtime');
const option: ambilight.Option = {}; ambilight.default(option); legacy.default(option); runtime(); runtime.default(option);`), [])
})
test('Ambilight published calls compile against both exact historical declarations and candidate without new inference widening', async () => {
@@ -53,7 +62,7 @@ test('Ambilight published calls compile against both exact historical declaratio
}
})
test('Ambilight editor globals are generated from the public namespace and retain negative checks', async () => {
test('Ambilight editor globals retain the required root factory and negative checks', async () => {
const source = fs.readFileSync('packages/artplayer-plugin-ambilight/types/artplayer-plugin-ambilight.d.ts', 'utf8')
const generated = generatePluginEditorDeclaration(source, 'artplayerPluginAmbilight')
const file = 'docs/assets/ts/artplayer-plugin-ambilight.d.ts'
@@ -61,12 +70,17 @@ test('Ambilight editor globals are generated from the public namespace and retai
assert.equal(formatted.errorCount, 0)
assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), formatted.output || generated)
const core = fs.readFileSync('docs/assets/ts/artplayer.d.ts', 'utf8')
const consumer = `const option: artplayerPluginAmbilight.Option = {}; artplayerPluginAmbilight(); artplayerPluginAmbilight.default(option);
const consumer = `const option: artplayerPluginAmbilight.Option = {}; artplayerPluginAmbilight(option);
const replacement: typeof artplayerPluginAmbilight = (_option) => (_art) => ({name: 'artplayerPluginAmbilight', start() {}, stop() {}});
// @ts-expect-error Required root argument.
artplayerPluginAmbilight();
// @ts-expect-error Optional invocation belongs to the runtime module.
artplayerPluginAmbilight.default(option);
// @ts-expect-error Invalid blur.
artplayerPluginAmbilight({ blur: 2 });`
for (const compiler of [ts, compat]) {
assert.deepEqual(checkPluginEditorDeclaration(generated, core, consumer, compiler), [])
assert.equal(checkPluginEditorDeclaration(generated, core, consumer.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, ''), compiler).length, 1)
assert.equal(checkPluginEditorDeclaration(generated, core, consumer.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, ''), compiler).length, 3)
}
})
+70 -15
View File
@@ -2,6 +2,7 @@ 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 { checkFiles, packedFiles } from '../../scripts/package-check.mjs'
@@ -9,7 +10,46 @@ import { consumerDirectory, removeConsumer, run, workspace, writeJson } from '..
import { verifyCanvasContract } from './canvas-contract.mjs'
import { hash, readMember } from './releases.mjs'
export function canvasConsumerSource(source, mode) {
if (mode === 'nodenext-esm') {
source = source.replace('import canvas from \'artplayer-proxy-canvas\'', 'import canvasModule from \'artplayer-proxy-canvas\'\nconst canvas = canvasModule.default')
.replace('import legacy from \'artplayer-proxy-canvas/legacy\'', 'import legacyModule from \'artplayer-proxy-canvas/legacy\'\nconst legacy = legacyModule.default')
.replace('namespace.default', 'namespace.default.default')
}
if (mode.endsWith('-no-interop'))
source = source.replace('import runtime from \'artplayer-proxy-canvas/runtime\'', 'import runtime = require(\'artplayer-proxy-canvas/runtime\')')
return source
}
export const canvasNamespaceConsumer = `import type Artplayer from 'artplayer';
import canvasModule from 'artplayer-proxy-canvas';
import * as canvasNamespace from 'artplayer-proxy-canvas';
import legacyModule from 'artplayer-proxy-canvas/legacy';
type Callback = (ctx: CanvasRenderingContext2D, video: HTMLVideoElement) => void;
type Factory = (option?: Callback) => (art: Artplayer) => HTMLCanvasElement;
const replacement: Factory = (_option?: Callback) => (_art: Artplayer) => document.createElement('canvas');
canvasModule.default(); canvasNamespace.default.default(); legacyModule.default();
const fromOld: typeof canvasModule = { default: replacement };
const toOld: { default: Factory } = canvasModule;
const namespaceReplacement: typeof canvasNamespace.default = { default: replacement };
const oldNamespace: { default: Factory } = canvasNamespace.default;
const optional: Parameters<typeof canvasModule.default>[0] = undefined;
const result: ReturnType<ReturnType<typeof canvasModule.default>> = document.createElement('canvas');
void [fromOld, toOld, namespaceReplacement, oldNamespace, optional, result];`
export function checkInvalidCanvasStatements(source, diagnostics) {
const expected = []
let line = 1
for (const text of source.split('\n')) {
if (text.startsWith('// @ts-expect-error'))
expected.push(line)
else line++
}
assert.deepEqual(diagnostics.map(item => item.line).sort((a, b) => a - b), expected, 'Each invalid Canvas statement must fail at its own line')
}
async function main() {
assert.equal(process.version.slice(1), fs.readFileSync(path.join(workspace, '.node-version'), 'utf8').trim(), 'Use the repository Node version')
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use yarn test:canvas-types-package')
const yarn = process.env.npm_execpath
assert(yarn && fs.existsSync(yarn))
@@ -50,8 +90,9 @@ async function main() {
const runtimeFile = path.join(consumer, 'exports.cjs')
fs.writeFileSync(runtimeFile, plugin.label === 'candidate'
? `const assert = require('node:assert/strict');
for (const name of ['artplayer-proxy-canvas', 'artplayer-proxy-canvas/legacy']) { const factory = require(name); assert.equal(typeof factory, 'function'); assert.equal(factory.default, factory); assert.equal(typeof factory(), 'function'); }
import('artplayer-proxy-canvas').then(module => { assert.deepEqual(Object.keys(module), ['default']); assert.equal(module.default.default, module.default); }).catch(error => { console.error(error); process.exitCode = 1; });`
for (const name of ['artplayer-proxy-canvas', 'artplayer-proxy-canvas/legacy', 'artplayer-proxy-canvas/runtime']) { const factory = require(name); assert.equal(typeof factory, 'function'); assert.equal(factory.default, factory); assert.equal(typeof factory(), 'function'); }
assert.equal(require('artplayer-proxy-canvas/runtime'), require('artplayer-proxy-canvas'));
Promise.all([import('artplayer-proxy-canvas'), import('artplayer-proxy-canvas/runtime')]).then(([module, runtime]) => { assert.deepEqual(Object.keys(module), ['default']); assert.equal(module.default.default, module.default); assert.equal(runtime.default, module.default); }).catch(error => { console.error(error); process.exitCode = 1; });`
: `const assert = require('node:assert/strict'); const value = require('artplayer-proxy-canvas'); assert.equal(typeof value, '${plugin.version === '1.0.0' ? 'object' : 'function'}'); assert.equal(typeof (value.default || value)(() => {}), 'function');`)
fs.writeFileSync(path.join(output, `${plugin.label}-runtime.log`), run([runtimeFile], consumer))
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
@@ -60,14 +101,19 @@ import('artplayer-proxy-canvas').then(module => { assert.deepEqual(Object.keys(m
for (const [compiler, mode] of modes) {
const next = mode.startsWith('nodenext')
const extension = next ? mode.endsWith('-cjs') ? 'cts' : 'mts' : 'ts'
const source = fs.readFileSync(path.join(workspace, plugin.label === 'candidate' ? 'test/types/canvas.ts' : 'refactor/fixtures/consumers/canvas-published.ts'), 'utf8')
const fixture = fs.readFileSync(path.join(workspace, plugin.label === 'candidate' ? 'test/types/canvas.ts' : 'refactor/fixtures/consumers/canvas-published.ts'), 'utf8')
const source = plugin.label === 'candidate' ? canvasConsumerSource(fixture, mode) : fixture
const filename = path.join(consumer, `consumer.${extension}`)
const historical = path.join(consumer, `historical.${extension}`)
fs.copyFileSync(path.join(workspace, 'refactor/fixtures/consumers/canvas-published.ts'), historical)
fs.writeFileSync(historical, canvasConsumerSource(fs.readFileSync(path.join(workspace, 'refactor/fixtures/consumers/canvas-published.ts'), 'utf8'), mode))
const files = plugin.label === 'candidate' ? [filename, historical] : [filename]
if (mode === 'nodenext-cjs' && plugin.label !== 'published-1.1.0') {
const commonjs = path.join(consumer, 'commonjs.cts')
fs.writeFileSync(commonjs, `import canvas = require('artplayer-proxy-canvas'); canvas((ctx, video) => ctx.drawImage(video, 0, 0)); ${plugin.label === 'candidate' ? 'canvas.default();' : ''}`)
if ((mode === 'nodenext-cjs' && plugin.label === 'published-1.0.0') || (plugin.label === 'candidate' && mode !== 'nodenext-esm' && mode !== 'bundler-esm')) {
const commonjs = path.join(consumer, `commonjs.${extension}`)
fs.writeFileSync(commonjs, plugin.label === 'candidate'
? `import canvas = require('artplayer-proxy-canvas'); import legacy = require('artplayer-proxy-canvas/legacy'); import runtime = require('artplayer-proxy-canvas/runtime');
canvas.default(); legacy.default(); runtime(); runtime.default();
const callback: runtime.Option = (ctx, video) => ctx.drawImage(video, 0, 0); runtime(callback);`
: `import canvas = require('artplayer-proxy-canvas'); canvas((ctx, video) => ctx.drawImage(video, 0, 0));`)
files.push(commonjs)
}
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: !mode.endsWith('-no-interop'), 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 }
@@ -78,7 +124,7 @@ import('artplayer-proxy-canvas').then(module => { assert.deepEqual(Object.keys(m
const actual = fs.realpathSync(file.fileName)
assert(actual.startsWith(fs.realpathSync(consumer) + path.sep) || (program.isSourceFileDefaultLibrary(file) && path.dirname(actual) === fs.realpathSync(path.dirname(compiler.sys.getExecutingFilePath()))), `Types escaped installed consumer: ${actual}`)
}
return compiler.getPreEmitDiagnostics(program).map(item => ({ code: item.code, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n') }))
return compiler.getPreEmitDiagnostics(program).map(item => ({ file: item.file && path.relative(consumer, item.file.fileName), line: item.file && item.start !== undefined ? item.file.getLineAndCharacterOfPosition(item.start).line + 1 : null, code: item.code, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n') }))
}
const diagnostics = compile(source)
const historicalFailure = plugin.label === 'published-1.1.0' && mode === 'nodenext-esm'
@@ -86,19 +132,28 @@ import('artplayer-proxy-canvas').then(module => { assert.deepEqual(Object.keys(m
assert.deepEqual(diagnostics.map(item => item.code), historicalFailure ? [2349, 2349, 2322, 2344, 2344] : [], `${plugin.label} ${compiler.version} ${mode}`)
const invalid = plugin.label === 'candidate' ? compile(source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')) : []
if (plugin.label === 'candidate')
assert.equal(invalid.length, 11, 'Installed declarations must reject all invalid uses')
matrix.push({ plugin: plugin.label, compiler: compiler.version, mode, historicalFailure, diagnostics, invalid })
checkInvalidCanvasStatements(source, invalid)
const namespace = mode === 'nodenext-esm' && plugin.label !== 'published-1.0.0' ? compile(canvasNamespaceConsumer) : []
assert.deepEqual(namespace, [], 'Latest published and candidate NodeNext ESM namespace shapes must accept plain module replacements')
const exportAssignment = mode.includes('commonjs') || mode === 'nodenext-cjs'
? compile(`import canvas = require('artplayer-proxy-canvas'); canvas((context: CanvasRenderingContext2D, video: HTMLVideoElement) => context.drawImage(video, 0, 0));`)
: null
if (exportAssignment)
assert.deepEqual(exportAssignment.map(item => item.code), plugin.label === 'published-1.0.0' ? [] : [2349], 'Preserve the approved 1.0 export= versus latest default-export distinction')
matrix.push({ plugin: plugin.label, compiler: compiler.version, mode, historicalFailure, diagnostics, invalid, namespace, exportAssignment })
}
}
finally {
removeConsumer(consumer)
}
}
writeJson(path.join(output, 'report.json'), { suite: 'canvas-isolated-package-types', introducedBy: 'PKG-CANVAS-04', scope: 'Two actual published proxies and packed candidate installed outside workspace with packed candidate core; offline and frozen reinstall, exact bytes, compiler/default/import= contracts. Published 1.0.0 required callback became optional in 1.1.0; candidate retains optional Parameters and exact HTMLCanvasElement return inference. This is not native proxy media, device or complete distribution acceptance.', packages, published: candidates.slice(0, 2).map(pkg => ({ label: pkg.label, archive: pkg.archive, sha256: pkg.sha256 })), matrix })
writeJson(path.join(output, 'report.json'), { suite: 'canvas-isolated-package-types', introducedBy: 'PKG-CANVAS-04', updatedBy: 'PKG-FACTORY-01', node: process.version, scope: 'Two actual published proxies and packed candidate installed outside workspace with packed candidate core; offline and frozen reinstall, exact bytes and root/legacy/runtime contracts. Root preserves published 1.1.0 pure factory, optional Parameters, exact HTMLCanvasElement result and NodeNext ESM namespace shape. Runtime adds accurate self identity with CommonJS/ESM and old Node10 resolution. Published 1.0.0 required callback/export= differences and 1.1.0 direct NodeNext ESM errors remain recorded. No native media, device or complete distribution acceptance.', packages, published: candidates.slice(0, 2).map(pkg => ({ label: pkg.label, archive: pkg.archive, sha256: pkg.sha256 })), matrix })
console.log(`Canvas installed types passed: ${matrix.length} cases; ${output}`)
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
}
+22 -8
View File
@@ -10,18 +10,21 @@ import compat from 'typescript-compat'
import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from '../../scripts/plugin-editor-types.mjs'
import { checkConsumer } from '../../scripts/typecheck.mjs'
import { verifyCanvasContract } from './canvas-contract.mjs'
import { canvasConsumerSource, canvasNamespaceConsumer, checkInvalidCanvasStatements } from './canvas-package-types.mjs'
import { readMember } from './releases.mjs'
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
test('Canvas current types preserve optional Parameters and exact Canvas result, while rejecting eleven invalid uses', () => {
test('Canvas types preserve the latest plain factory and expose accurate runtime identity separately', () => {
const source = fs.readFileSync('test/types/canvas.ts', 'utf8')
for (const [compiler, mode] of modes) {
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${compiler.version} ${mode}`)
assert.equal(checkConsumer(compiler, mode, source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')).length, 11)
const consumer = canvasConsumerSource(source, mode)
assert.deepEqual(checkConsumer(compiler, mode, consumer), [], `${compiler.version} ${mode}`)
checkInvalidCanvasStatements(consumer, checkConsumer(compiler, mode, consumer.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')))
}
assert.deepEqual(checkConsumer(ts, 'nodenext-cjs', `import canvas = require('artplayer-proxy-canvas'); import legacy = require('artplayer-proxy-canvas/legacy');
const callback: canvas.Option = (ctx, video) => ctx.drawImage(video, 0, 0); canvas(callback); canvas.default(); legacy(); legacy.default(callback);`), [])
assert.deepEqual(checkConsumer(ts, 'nodenext-cjs', `import canvas = require('artplayer-proxy-canvas'); import legacy = require('artplayer-proxy-canvas/legacy'); import runtime = require('artplayer-proxy-canvas/runtime');
const callback: canvas.Option = (ctx, video) => ctx.drawImage(video, 0, 0); canvas.default(callback); legacy.default(); runtime(); runtime.default(callback);`), [])
assert.deepEqual(checkConsumer(ts, 'nodenext-esm', canvasNamespaceConsumer), [])
})
test('Canvas published declarations distinguish required 1.0.0 and optional 1.1.0 callbacks without changing Canvas return assignability', async () => {
@@ -48,11 +51,17 @@ test('Canvas published declarations distinguish required 1.0.0 and optional 1.1.
assert.deepEqual(historical(source), [])
assert.deepEqual(historical(`${source}\n${optional}`), version === '1.0.0' ? [2322, 2554] : [])
assert.deepEqual(checkConsumer(compiler, 'node10-commonjs', `${source}\n${optional}`), [])
if (version === '1.1.0') {
assert.deepEqual(historical(`${source}
type Factory = (option?: (ctx: CanvasRenderingContext2D, video: HTMLVideoElement) => void) => (art: Artplayer) => HTMLCanvasElement;
const replacement: Factory = () => () => document.createElement('canvas');
const fromOld: typeof canvas = replacement; const toOld: Factory = canvas; void [fromOld, toOld];`), [])
}
}
}
})
test('Canvas editor globals are generated from public types with optional callback, media view and negative checking', async () => {
test('Canvas editor globals preserve the plain root factory with an explicit accurate runtime type view', async () => {
const source = fs.readFileSync('packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.ts', 'utf8')
const generated = generatePluginEditorDeclaration(source, 'artplayerProxyCanvas')
const file = 'docs/assets/ts/artplayer-proxy-canvas.d.ts'
@@ -60,14 +69,19 @@ test('Canvas editor globals are generated from public types with optional callba
assert.equal(formatted.errorCount, 0)
assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), formatted.output || generated)
const core = fs.readFileSync('docs/assets/ts/artplayer.d.ts', 'utf8')
const consumer = `artplayerProxyCanvas(); artplayerProxyCanvas.default((ctx, video) => ctx.drawImage(video, 0, 0));
const consumer = `artplayerProxyCanvas(); artplayerProxyCanvas((ctx, video) => ctx.drawImage(video, 0, 0));
const replacement: typeof artplayerProxyCanvas = (_option?: artplayerProxyCanvas.Option) => (_art: Artplayer) => document.createElement('canvas');
const precise = artplayerProxyCanvas as artplayerProxyCanvas.RuntimeFactory; precise.default();
const result: artplayerProxyCanvas.Result = document.createElement('canvas');
const media = result as artplayerProxyCanvas.MediaCanvas; media.play();
void replacement;
// @ts-expect-error Root factory does not require or expose a self alias.
artplayerProxyCanvas.default();
// @ts-expect-error Callback must be a function in public types.
artplayerProxyCanvas(42);`
for (const compiler of [ts, compat]) {
assert.deepEqual(checkPluginEditorDeclaration(generated, core, consumer, compiler), [])
assert.equal(checkPluginEditorDeclaration(generated, core, consumer.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, ''), compiler).length, 1)
assert.equal(checkPluginEditorDeclaration(generated, core, consumer.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, ''), compiler).length, 2)
}
})