From 111710703259e69c7d28d7f503aecca359252d97 Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Sun, 13 Sep 2026 16:47:51 +0800 Subject: [PATCH] fix(asr): validate generated editor declarations semantically --- docs/assets/ts/artplayer-plugin-asr.d.ts | 69 +++++++++++++------ ...2026-09-13-PKG-ASR-05-editor-generation.md | 19 +++++ scripts/build-ts.js | 2 +- test/editor-types.test.js | 30 ++++++++ 4 files changed, 97 insertions(+), 23 deletions(-) create mode 100644 refactor/changes/2026-09-13-PKG-ASR-05-editor-generation.md diff --git a/docs/assets/ts/artplayer-plugin-asr.d.ts b/docs/assets/ts/artplayer-plugin-asr.d.ts index 769e0b30f..37fb89acc 100644 --- a/docs/assets/ts/artplayer-plugin-asr.d.ts +++ b/docs/assets/ts/artplayer-plugin-asr.d.ts @@ -1,26 +1,51 @@ -interface AudioChunk { - pcm: ArrayBuffer - wav: ArrayBuffer +// Generated from the package public declaration by yarn build:ts. Do not edit. +/* eslint-disable ts/no-redeclare -- Callable and public type namespace intentionally merge. */ +declare namespace artplayerPluginAsrDefinitions { + export interface AudioChunk { + pcm: ArrayBuffer + wav: ArrayBuffer + } + export interface AsrPluginOption { + length?: number + interval?: number + sampleRate?: number + autoHideTimeout?: number + onAudioChunk?: (chunk: AudioChunk) => void | Promise + } + export interface AsrPluginInstance { + name: 'artplayerPluginAsr' + stop: () => void + hide: () => void + append: (subtitle: string) => void + } + /** Historical factory shape, including void stop and callback results. */ + export type Factory = (option?: AsrPluginOption) => (art: Artplayer) => AsrPluginInstance + /** Accurate asynchronous view available through the /runtime entry. */ + export interface RuntimeOption extends Omit { + /** Capture the media stream without taking ownership of its playback route. */ + audioInput?: { + type: 'capture' + } + onAudioChunk?: (chunk: AudioChunk) => string | void | null | Promise + } + export interface RuntimeResult extends Omit { + stop: () => Promise + } + export interface RuntimeFactory { + (option?: RuntimeOption): (art: Artplayer) => RuntimeResult + readonly default: RuntimeFactory + } + export function artplayerPluginAsr(option?: AsrPluginOption): (art: Artplayer) => AsrPluginInstance } - -interface AsrPluginOption { - length?: number - interval?: number - sampleRate?: number - autoHideTimeout?: number - onAudioChunk?: (chunk: AudioChunk) => void | Promise +declare const artplayerPluginAsr: typeof artplayerPluginAsrDefinitions.artplayerPluginAsr +declare namespace artplayerPluginAsr { + export type AudioChunk = artplayerPluginAsrDefinitions.AudioChunk + export type AsrPluginOption = artplayerPluginAsrDefinitions.AsrPluginOption + export type AsrPluginInstance = artplayerPluginAsrDefinitions.AsrPluginInstance + export type Factory = artplayerPluginAsrDefinitions.Factory + export type RuntimeOption = artplayerPluginAsrDefinitions.RuntimeOption + export type RuntimeResult = artplayerPluginAsrDefinitions.RuntimeResult + export type RuntimeFactory = artplayerPluginAsrDefinitions.RuntimeFactory } - -interface AsrPluginInstance { - name: 'artplayerPluginAsr' - stop: () => void - hide: () => void - append: (subtitle: string) => void -} - -declare function artplayerPluginAsr(option?: AsrPluginOption): (art: Artplayer) => AsrPluginInstance - -export default artplayerPluginAsr - export = artplayerPluginAsr export as namespace artplayerPluginAsr; diff --git a/refactor/changes/2026-09-13-PKG-ASR-05-editor-generation.md b/refactor/changes/2026-09-13-PKG-ASR-05-editor-generation.md new file mode 100644 index 000000000..f4623ee5a --- /dev/null +++ b/refactor/changes/2026-09-13-PKG-ASR-05-editor-generation.md @@ -0,0 +1,19 @@ +# PKG-ASR-05 编辑器生成修复检查点 + +完整执行 `yarn build:ts` 时发现 ASR 尚走旧文本拼接路径。新增的公开命名类型和 +原有 `export default` 被直接保留,再拼接 `export =`,在严格独立编辑器编译中 +产生 TS2309。此问题已修复,不只登记后推迟处理。 + +ASR 现在使用现有语义生成器:公共类型归入全局命名空间,默认工厂仍保持历史 +同步声明,准确 RuntimeOption/RuntimeResult/RuntimeFactory 作为独立类型可用。 +运行时源码、npm 根声明及播放器行为没有改变。 + +`test/editor-types.test.js` 重建旧文本生成结果并断言 TS2309;对新声明验证普通 +工厂替换、历史 void stop、精确异步 stop 与显式 capture 输入。TS 5.9.3 和 4.3.5 +均在 strict、skipLibCheck=false 下通过,并各拒绝三个指定反例。生成产物与 +格式化结果逐字核对。两项编辑器测试通过,日志为 +`refactor/.cache/asr-editor-tests.log`,生成日志为 +`refactor/.cache/asr-editor-generation.log`。 + +此修复作为 ASR-05 的独立本地检查点提交;ASR-05 的真实设备等剩余验证仍未完成。 +未将静态声明测试描述为新的浏览器播放证据,也未推送或发布。 diff --git a/scripts/build-ts.js b/scripts/build-ts.js index 6fafdf813..a97963dc5 100644 --- a/scripts/build-ts.js +++ b/scripts/build-ts.js @@ -53,7 +53,7 @@ console.log(`✨ Built ${artplayerTSoutput}`); const type = pluginsTS[index] const { name, file } = parsePluginInfo(type) const source = String(fs.readFileSync(type)) - const semanticPlugin = ['artplayerPluginMultipleSubtitles', 'artplayerPluginVttThumbnail', 'artplayerPluginHlsControl', 'artplayerPluginAudioTrack', 'artplayerPluginDashControl', 'artplayerPluginAds', 'artplayerPluginAmbilight', 'artplayerProxyCanvas', 'artplayerProxyMediabunny', 'artplayerPluginDocumentPip', 'ArtplayerToolIframe', 'ArtplayerToolThumbnail'].includes(name) + const semanticPlugin = ['artplayerPluginAsr', 'artplayerPluginMultipleSubtitles', 'artplayerPluginVttThumbnail', 'artplayerPluginHlsControl', 'artplayerPluginAudioTrack', 'artplayerPluginDashControl', 'artplayerPluginAds', 'artplayerPluginAmbilight', 'artplayerProxyCanvas', 'artplayerProxyMediabunny', 'artplayerPluginDocumentPip', 'ArtplayerToolIframe', 'ArtplayerToolThumbnail'].includes(name) const localTypes = name === 'artplayerProxyMediabunny' ? { './media': fs.readFileSync(path.join(path.dirname(type), 'media.d.ts'), 'utf8') } : {} const code = semanticPlugin ? generatePluginEditorDeclaration(source, name, localTypes) diff --git a/test/editor-types.test.js b/test/editor-types.test.js index 433e90dee..d2a22cf3d 100644 --- a/test/editor-types.test.js +++ b/test/editor-types.test.js @@ -5,6 +5,7 @@ import { test } from 'node:test' import { ESLint } from 'eslint' import compat from 'typescript-compat' import { asGlobalDeclaration, checkCoreEditorDeclaration, generateCoreEditorDeclaration } from '../scripts/editor-types.mjs' +import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from '../scripts/plugin-editor-types.mjs' test('editor declarations are reproducible, standalone and preserve constructor/named types in both compilers', async () => { const generated = generateCoreEditorDeclaration() @@ -19,3 +20,32 @@ test('editor declarations are reproducible, standalone and preserve constructor/ assert(checkCoreEditorDeclaration(corrupted).some(diagnostic => diagnostic.code === 2304), 'Invalid output must not be hidden by skipLibCheck') assert.throws(() => asGlobalDeclaration('export { default } from "./missing"', 'Artplayer'), /unresolved exports/) }) + +test('ASR editor generation supports named types without mixing default and export assignment', async () => { + const source = fs.readFileSync('packages/artplayer-plugin-asr/types/artplayer-plugin-asr.d.ts', 'utf8') + const core = fs.readFileSync('docs/assets/ts/artplayer.d.ts', 'utf8') + const old = `${source.replace(/^import.*$/gim, '')}\nexport = artplayerPluginAsr;\nexport as namespace artplayerPluginAsr;\n` + assert(checkPluginEditorDeclaration(old, core).some(item => item.code === 2309), 'The old textual generator must reproduce its conflicting export assignment') + const generated = generatePluginEditorDeclaration(source, 'artplayerPluginAsr') + const file = 'docs/assets/ts/artplayer-plugin-asr.d.ts' + const [formatted] = await new ESLint({ fix: true, fixTypes: ['layout'] }).lintText(generated, { filePath: file }) + assert.equal(formatted.errorCount, 0) + assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), formatted.output || generated) + const consumer = `declare const art: Artplayer; +const factory: typeof artplayerPluginAsr = () => () => ({ name: 'artplayerPluginAsr', stop() {}, hide() {}, append(_text: string) {} }); +const oldResult: void = factory()(art).stop(); +const option: artplayerPluginAsr.RuntimeOption = { audioInput: { type: 'capture' }, onAudioChunk: async () => 'subtitle' }; +declare const runtime: artplayerPluginAsr.RuntimeFactory; +const stopped: Promise = runtime(option)(art).stop(); +// @ts-expect-error Root stop retains historical void type. +const rootPromise: Promise = artplayerPluginAsr()(art).stop(); +// @ts-expect-error Capture input is runtime-only. +artplayerPluginAsr({ audioInput: { type: 'capture' } }); +// @ts-expect-error Unknown capture mode. +const invalid: artplayerPluginAsr.RuntimeOption = { audioInput: { type: 'direct' } }; +void [oldResult, stopped, rootPromise, invalid];` + for (const compiler of [undefined, compat]) { + assert.deepEqual(checkPluginEditorDeclaration(generated, core, consumer, compiler), []) + assert.equal(checkPluginEditorDeclaration(generated, core, consumer.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, ''), compiler).length, 3) + } +})