fix(multiple-subtitles): [PKG-MULTI-SUB-04] preserve historical type shapes

This commit is contained in:
Harvey Zhao committed 2026-09-13 16:59:44 +08:00
1 parent 58d2c95d8e
commit 87cfcf5a6c
17 files changed
+223 -63

No files matched your search

@@ -7,7 +7,10 @@ Both methods return undefined. Root and legacy declarations retain the historica
`LegacyResult` for existing extraction and replacement-function consumers. The additive
`artplayer-plugin-multiple-subtitles/runtime` entry describes the actual `Promise<Result>`
and selection methods. It shares the root implementation, not another plugin instance.
The factory has a writable `.default` self alias for historical CommonJS access.
The factory has a writable `.default` self alias for historical CommonJS access. Only
`RuntimeFactory` requires that property; adding it to the historical root function type
would invalidate replacement functions. Root option and registration result remain exact
to npm 1.2.0; the runtime entry requires an option object but allows omitted `subtitles`.
## Current module boundaries
@@ -31,9 +34,11 @@ The factory has a writable `.default` self alias for historical CommonJS access.
The pinned upstream reference and full adaptation check are in
`../../refactor/baselines/multiple-subtitles-vendor/` and
`../../refactor/scripts/multiple-subtitles-vendor.mjs`.
- `types/artplayer-plugin-multiple-subtitles.d.ts` owns public data types and the historical
callable signature. Root/legacy `.d.mts` and `.d.cts` forward that signature; `runtime.d.*`
forwards the precise asynchronous factory. Classic resolution uses typesVersions.
- `types/artplayer-plugin-multiple-subtitles.d.ts` owns public data types and the latest
published function signature. Root/legacy exports use a types-first mapping to this file,
preserving the old NodeNext ESM namespace. Runtime `.d.mts` supplies a callable default;
runtime `.d.cts` and classic `.d.ts` use export-assignment with merged namespace type
aliases. Classic subpath resolution uses typesVersions. Root and runtime share JS files.
- `scripts/build-ts.js` generates the editor's global factory and named type namespace from
this declaration through the semantic generator. Never edit the generated editor file.
- `../../docs/assets/example/multiple.subtitles.js` demonstrates the real plugin name,
@@ -111,7 +116,15 @@ Public declaration tests check exact old extraction/replacement signatures, accu
types and negative uses with TS 5.9.3 and 4.3.5. The package test packs core and plugin, installs
outside the workspace and checks Node10/NodeNext/Bundler modes plus real CJS/ESM identities.
Historical 1.0.0/1.1.0 export-assignment declarations and 1.2.0 default-module declarations
disagree on raw `import = require` extraction. Task 04 retains that unresolved boundary;
default-import compatibility must not be presented as proof of every historical module form.
disagree on raw `import = require` extraction. The approved policy in
`../../refactor/type-compatibility-policy.md` and ADR-025 prioritizes latest npm 1.2.0.
Earlier consumers have the explicit runtime-entry migration in README; this is a declared
type migration, not proof that contradictory historical declarations all work unchanged.
Installed tests retain eight original NodeNext ESM direct-consumer diagnostics for both
1.2.0 and the candidate, while a namespace consumer remains valid. Frozen direct/default
CommonJS consumers preserve each old version's diagnostics as well. Candidate positives
check both factory-assignment directions and accurate asynchronous runtime methods in
seven compiler modes, including classic CommonJS without interop. Removing suppression
comments must reject all 16 invalid statements at their exact lines in every mode.
Tasks 05/06 still own complete core/device combinations and demo acceptance.
See `../../refactor/baselines/multiple-subtitles-contract.md` for exact historical differences.
@@ -8,7 +8,10 @@ Multiple subtitles plugin for ArtPlayer
## TypeScript
The root and `/legacy` type entries preserve historical parameter and return-type extraction.
The root and `/legacy` type entries preserve the latest published 1.2.0 function shape:
a required `{ subtitles: TrackOption[] }` argument and a synchronous `{ name: 'multipleSubtitles' }`
registration result. This includes parameter extraction and replacement-factory assignment in
both directions; the root factory type has no required `.default` property.
Registration has always been asynchronous. For accurate Promise and selection-method types,
use the additive runtime entry, which loads the same implementation:
@@ -27,6 +30,36 @@ result.reset()
The option object is required; `{}` means no tracks. `tracks()` clears the selection;
`reset()` restores all original tracks. Both methods return `void`. The existing `onParser`
field remains declared for compatibility but is not invoked by the implementation.
### Earlier declarations and module resolution
Versions 1.0.0 and 1.1.0 declared `export =`, whereas 1.2.0 declared a default export.
The root follows 1.2.0 under the approved [type compatibility policy](../../refactor/type-compatibility-policy.md).
For consumers migrating an earlier `import = require`, use the accurate runtime entry and
await registration:
```ts
import type Artplayer from 'artplayer'
import multipleSubtitles = require('artplayer-plugin-multiple-subtitles/runtime')
async function registerSubtitles(art: Artplayer) {
const result = await multipleSubtitles({ subtitles: [] })(art)
result.reset()
}
```
This `import = require` form works with classic Node resolution without `esModuleInterop`.
The runtime default import in the first example requires that flag in classic CommonJS;
NodeNext ESM and bundler resolution use its ESM declaration. Runtime types also expose the
existing writable `.default` self alias. JavaScript entrypoints and valid historical
`require('artplayer-plugin-multiple-subtitles').default(...)` calls remain supported.
The old 1.0.0/1.1.0 declaration allowed calling the raw `require` object, but its shipped
JavaScript did not; that mismatch is not a supported runtime contract.
NodeNext ESM intentionally retains the 1.2.0 root declaration's module namespace shape
(`import root from 'artplayer-plugin-multiple-subtitles'` exposes the declared factory as
`root.default`). It does not silently become a callable default type. Prefer the `/runtime`
default import for accurate native ESM calls; the same applies to `/legacy` declarations.
See [maintenance notes](ARCHITECTURE.md) for module responsibilities and validation commands.
## License
@@ -19,24 +19,14 @@
],
"exports": {
".": {
"import": {
"types": "./types/artplayer-plugin-multiple-subtitles.d.mts",
"default": "./dist/artplayer-plugin-multiple-subtitles.mjs"
},
"require": {
"types": "./types/artplayer-plugin-multiple-subtitles.d.cts",
"default": "./dist/artplayer-plugin-multiple-subtitles.js"
}
"types": "./types/artplayer-plugin-multiple-subtitles.d.ts",
"import": "./dist/artplayer-plugin-multiple-subtitles.mjs",
"require": "./dist/artplayer-plugin-multiple-subtitles.js"
},
"./legacy": {
"import": {
"types": "./types/artplayer-plugin-multiple-subtitles.d.mts",
"default": "./dist/artplayer-plugin-multiple-subtitles.legacy.js"
},
"require": {
"types": "./types/artplayer-plugin-multiple-subtitles.d.cts",
"default": "./dist/artplayer-plugin-multiple-subtitles.legacy.js"
}
"types": "./types/artplayer-plugin-multiple-subtitles.d.ts",
"import": "./dist/artplayer-plugin-multiple-subtitles.legacy.js",
"require": "./dist/artplayer-plugin-multiple-subtitles.legacy.js"
},
"./runtime": {
"import": {
@@ -1,5 +0,0 @@
import type { Factory } from './artplayer-plugin-multiple-subtitles.js'
declare const artplayerPluginMultipleSubtitles: Factory
export default artplayerPluginMultipleSubtitles
export type { Factory, LegacyResult, Option, Result, RuntimeFactory, RuntimeOption, TrackOption } from './artplayer-plugin-multiple-subtitles.js'
@@ -1,5 +0,0 @@
import type { Factory } from './artplayer-plugin-multiple-subtitles.js'
declare const artplayerPluginMultipleSubtitles: Factory
export default artplayerPluginMultipleSubtitles
export type { Factory, LegacyResult, Option, Result, RuntimeFactory, RuntimeOption, TrackOption } from './artplayer-plugin-multiple-subtitles.js'
@@ -1,4 +1,11 @@
import type { RuntimeFactory } from './artplayer-plugin-multiple-subtitles.js'
declare const artplayerPluginMultipleSubtitles: RuntimeFactory
// eslint-disable-next-line ts/no-redeclare -- Ambient merge exposes types on the CommonJS factory.
declare namespace artplayerPluginMultipleSubtitles {
type Result = import('./artplayer-plugin-multiple-subtitles.js').Result
type RuntimeFactory = import('./artplayer-plugin-multiple-subtitles.js').RuntimeFactory
type RuntimeOption = import('./artplayer-plugin-multiple-subtitles.js').RuntimeOption
type TrackOption = import('./artplayer-plugin-multiple-subtitles.js').TrackOption
}
export = artplayerPluginMultipleSubtitles
@@ -1,5 +1,11 @@
import type { RuntimeFactory } from './artplayer-plugin-multiple-subtitles.js'
declare const artplayerPluginMultipleSubtitles: RuntimeFactory
export default artplayerPluginMultipleSubtitles
export type { Result, RuntimeFactory, RuntimeOption, TrackOption } from './artplayer-plugin-multiple-subtitles.js'
// eslint-disable-next-line ts/no-redeclare -- Ambient merge exposes types on the CommonJS factory.
declare namespace artplayerPluginMultipleSubtitles {
type Result = import('./artplayer-plugin-multiple-subtitles.js').Result
type RuntimeFactory = import('./artplayer-plugin-multiple-subtitles.js').RuntimeFactory
type RuntimeOption = import('./artplayer-plugin-multiple-subtitles.js').RuntimeOption
type TrackOption = import('./artplayer-plugin-multiple-subtitles.js').TrackOption
}
export = artplayerPluginMultipleSubtitles
@@ -0,0 +1,31 @@
{
"schemaVersion": 1,
"task": "PKG-MULTI-SUB-04",
"status": "done",
"capturedAt": "2026-09-13T08:59:40.613Z",
"policy": "type-compatibility-policy.md",
"baseline": "Latest actual npm 1.2.0 root type shape; earlier 1.0.0/1.1.0 conflicts migrated under ADR-025.",
"checks": {
"isolatedReport": "refactor/.cache/multiple-subtitles-package-types-IHnPqy/report.json",
"reportSha256": "7380f5caafae2715d85ba2ad4ca7a00e874bf21ff938f251fa47b27942f1fd55",
"candidateArchiveSha256": "6c975939960ec7a9182157a5b817b6440a25ced25935357b816b0aa1c1860b0f",
"configurations": 22,
"candidatePositiveConfigurations": 7,
"invalidStatementsPerCandidateConfiguration": 16,
"log": "refactor/.cache/multisub04-approved-types.log",
"logSha256": "1d0b0e7fd4c4ed45abffd3ea7fbf513d3b6a301dfb0c46110f7df1680731d695",
"focusedTests": 7
},
"preserves": [
"Whole factory replacement in both directions, required arguments and historical sync result.",
"Latest published valid NodeNext namespace and plain module replacement, old direct-call diagnostics retained.",
"Runtime accurate async result, named types and CommonJS/default identity including old no-interop resolution.",
"Isolated actual tarball installation, exact installed bytes, offline frozen reinstall."
],
"sourceAndDistChanged": false,
"browserRerun": false,
"limitations": [
"Complete shipped archive/deep-entry and example acceptance remains 06; actual browser/core combinations remain 05.",
"Existing runtime/source evidence is linked in the completion note; these checks do not establish devices or final npm release readiness."
]
}
@@ -0,0 +1,26 @@
# PKG-MULTI-SUB-04 类型兼容实施完成
用户批准ADR-025后,Multiple Subtitles按实际npm1.2.0保持根/legacy的必填subtitles、
同步name结果、普通工厂双向赋值和NodeNext namespace。较早1.0.0/1.1.0的export=
差异通过迁移说明处理;不再等待重复授权,也不把实际异步实现改成历史声明的同步行为。
修正根入口类型条件映射,移除候选时期根.d.mts/.d.cts包装。/runtime继续与根共用
JS实现,精确描述异步注册、tracks/select/reset和可写default自别名;Node10的
export=声明现在也提供完整命名类型。README提供可执行的CommonJS async函数迁移
示例,架构文档说明历史类型与精确入口各自职责。
最终安装三历史版本和候选共22配置,候选7配置正例均无诊断,各按语句行拒绝16个
反例。真实归档成员与安装目录逐字核对、仓库外离线安装及frozen重装通过。
最新1.2.0及候选保留旧NodeNext直接调用的8个原始诊断,合法namespace调用与整个
模块双向替换通过;较早export=直接/default对照保留,候选JS身份另行验证。
7项声明、编辑器及内部strict测试通过,定向lint通过。
原有生产TS拆分和资源/异步类型验证仍见
[运行时检查点](2026-09-13-PKG-MULTI-SUB-04-runtime-types.md)与
[公共声明检查点](2026-09-13-PKG-MULTI-SUB-04-public-types.md)。此次源码/dist和根
公共声明内容未变,已验证生成编辑器一致性,没有重复播放测试或新增设备通过声明。
04完成,MULTI-SUB-TYPE-01关闭;MULTI-SUB-EXPORT-01的完整分发与demo部分继续交给06,
核心组合/设备仍由05负责。前次文档中的待确认状态由本记录与ADR-025替代。
最终报告及哈希见[验证记录](../baselines/multiple-subtitles-approved-types.json)。
独立完成提交,不推送或发布。
+3 -3
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 228 项,范围 22 个包及工作区/示例。
状态:todo 78 / doing 16 / blocked 0 / done 134 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 78 / doing 15 / blocked 0 / done 135 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -220,7 +220,7 @@
| PKG-MULTI-SUB-01 | artplayer-plugin-multiple-subtitles<br>核对包契约与历史用法 | BASE-05 | subtitles/onParser、multipleSubtitles 名称及实际返回方法 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结;接续 VENDOR-03,定位 WebVTT parser 来源/修订及 CC0 头,保留解析/序列化行为 | H | done |
| PKG-MULTI-SUB-02 | artplayer-plugin-multiple-subtitles<br>建立特有行为与错误测试 | PKG-MULTI-SUB-01, ENG-03, ENG-05 | VTT/SRT/ASS、编码/重叠 cue/排序/空轨/失败与切换 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-MULTI-SUB-03 | artplayer-plugin-multiple-subtitles<br>整理内部职责与资源 | PKG-MULTI-SUB-02, CORE-15 | 解析与合并/获取/渲染分离,过期请求及对象 URL 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-MULTI-SUB-04 | artplayer-plugin-multiple-subtitles<br>迁移自有源码和公开类型 | PKG-MULTI-SUB-03, ENG-04, ENG-06, CORE-07 | 准确 parser/cue/tree/result 类型与真实异步形状 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | doing |
| PKG-MULTI-SUB-04 | artplayer-plugin-multiple-subtitles<br>迁移自有源码和公开类型 | PKG-MULTI-SUB-03, ENG-04, ENG-06, CORE-07 | 准确 parser/cue/tree/result 类型与真实异步形状 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done |
| PKG-MULTI-SUB-07 | artplayer-plugin-multiple-subtitles<br>修复字幕内嵌时间戳序列化 | PKG-MULTI-SUB-03 | 保留数字时间戳节点、修正无效 NaN 输出、明确旧/新字幕输出差异 | 旧九份实现可复现,候选有效内嵌时间戳及文字/CSS/原生字幕通过,公开工厂与选择接口不变 | H | done |
| PKG-MULTI-SUB-05 | artplayer-plugin-multiple-subtitles<br>验证新旧核心和组合 | PKG-MULTI-SUB-04, CORE-22, PKG-MULTI-SUB-07 | 与核心字幕/偏移/全屏组合,旧名称调用不变 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-MULTI-SUB-06 | artplayer-plugin-multiple-subtitles<br>验证分发并同步文档 | PKG-MULTI-SUB-05, ENG-07 | multiple.subtitles.js、parser 来源许可及打包边界 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
@@ -526,7 +526,7 @@
- PKG-MULTI-SUB-01: [记录](baselines/multiple-subtitles-release.json) [记录](baselines/multiple-subtitles-contract.md) [记录](baselines/multiple-subtitles-contract-validation.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-01-contract.md)
- PKG-MULTI-SUB-02: [记录](baselines/multiple-subtitles-failures.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-02-failures.md)
- PKG-MULTI-SUB-03: [记录](baselines/multiple-subtitles-resources.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-03-resources.md)
- PKG-MULTI-SUB-04: [记录](baselines/multiple-subtitles-runtime-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-runtime-types.md) [记录](baselines/multiple-subtitles-public-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-public-types.md)
- PKG-MULTI-SUB-04: [记录](baselines/multiple-subtitles-runtime-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-runtime-types.md) [记录](baselines/multiple-subtitles-public-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-public-types.md) [记录](type-compatibility-policy.md) [记录](baselines/multiple-subtitles-approved-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-approved-types.md)
- PKG-MULTI-SUB-07: [记录](baselines/multiple-subtitles-timestamps.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md)
- PKG-JASSUB-01: [记录](baselines/jassub-release.json) [记录](baselines/jassub-vendor.json) [记录](baselines/jassub-font-metadata.json) [记录](baselines/jassub-contract.md) [记录](baselines/jassub-contract-validation.json) [记录](changes/2026-09-13-PKG-JASSUB-01-baseline.md)
- PKG-ASR-01: [记录](changes/2026-09-13-PKG-ASR-01-contracts.md) [记录](baselines/asr-contract.md) [记录](baselines/asr-release.json)
+9
View File
@@ -1,5 +1,14 @@
# 进度与证据
## PKG-MULTI-SUB-04 Multiple Subtitles 类型兼容实施完成
已落实用户批准的最新npm 1.2.0根类型及较早1.0.0/1.1.0迁移规则;/runtime准确
描述异步结果和模块形式。22个真实安装配置、7个候选配置正例通过,每配置拒绝
16个指定反例,7项专项测试含编辑器通过。见[完成记录](changes/2026-09-13-PKG-MULTI-SUB-04-approved-types.md)及[证据](baselines/multiple-subtitles-approved-types.json)。
MULTI-SUB-TYPE-01关闭,MULTI-SUB-EXPORT-01仍保留组合/完整分发验证。源码与dist未变,
不新增浏览器设备通过声明。228项:135 done、15 doing、78 todo;不推送或发布。
## PKG-VTT-THUMB-04 VTT Thumbnail 类型兼容实施完成
已落实用户批准的最新npm 1.1.0根类型及较早1.0.x迁移规则;/runtime准确
+1 -1
View File
@@ -194,7 +194,7 @@
| VTT-THUMB-LIFE-01 | open / 已复现 | Fetch completion, setBar listeners and mobile timers have no destroy ownership | PKG-VTT-THUMB-02, PKG-VTT-THUMB-03, PKG-VTT-THUMB-05 |
| VTT-THUMB-TYPE-01 | resolved / 已复现 | Published declarations falsely describe async registration as a synchronous result | PKG-VTT-THUMB-04 |
| VTT-THUMB-EXPORT-01 | open / 已复现 | Older CommonJS default objects and thumbnails control names differ from latest direct export and vtt-thumbnail name | PKG-VTT-THUMB-04, PKG-VTT-THUMB-05, PKG-VTT-THUMB-06 |
| MULTI-SUB-TYPE-01 | open / 已复现 | Declarations omit async registration, tracks/reset and make the subtitles array required | PKG-MULTI-SUB-04 |
| MULTI-SUB-TYPE-01 | resolved / 已复现 | Declarations omit async registration, tracks/reset and make the subtitles array required | PKG-MULTI-SUB-04 |
| MULTI-SUB-MERGE-01 | open / 已复现 | 1.0.0 merges cues by index while later versions concatenate independently timed track cues | PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
| MULTI-SUB-LIFE-01 | open / 已复现 | Pending downloads and final object URL have no destroy owner | PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
| MULTI-SUB-EXPORT-01 | open / 已复现 | Older CommonJS default objects conflict with export-assignment declarations; latest exports a direct factory | PKG-MULTI-SUB-04, PKG-MULTI-SUB-06 |
+13 -5
View File
@@ -4276,7 +4276,7 @@
"id": "MULTI-SUB-TYPE-01",
"title": "Declarations omit async registration, tracks/reset and make the subtitles array required",
"confirmation": "reproduced",
"status": "open",
"status": "resolved",
"owners": [
"PKG-MULTI-SUB-04"
],
@@ -4290,8 +4290,14 @@
"refactor/scripts/multiple-subtitles-types.test.mjs",
"refactor/scripts/multiple-subtitles-package-types.mjs"
],
"compatibleResolution": "Root/legacy retain 1.2.0 default-module extraction and replaceable synchronous factory types; additive runtime entry exposes accurate Promise and methods; writable default self alias preserves historical valid JS forms. Actual installed old 1.0.0/1.1.0 raw export-assignment extraction conflicts with 1.2.0 declarations and remains awaiting explicit resolution. Do not close on default-import success alone.",
"closureCriteria": "Strict old/new consumers and installed type views cover parameters, result methods and factory replacement."
"compatibleResolution": "Approved latest npm 1.2.0 root factory/namespace stays exact; additive runtime supplies accurate asynchronous and module forms. Earlier 1.0.0/1.1.0 type conflicts have explicit migration under ADR-025.",
"closureCriteria": "Strict old/new consumers and installed type views cover parameters, result methods and factory replacement.",
"resolutionEvidence": [
"refactor/baselines/multiple-subtitles-approved-types.json",
"refactor/changes/2026-09-13-PKG-MULTI-SUB-04-approved-types.md",
"refactor/type-compatibility-policy.md"
],
"resolutionRationale": "22 isolated installation configurations, seven zero-diagnostic candidate modes with 16 exact invalid statements each, 7 focused tests. Legal latest namespace and complete factory replacements retained, historical incompatible forms recorded. No production JS changed."
},
{
"id": "MULTI-SUB-MERGE-01",
@@ -4368,9 +4374,11 @@
"refactor/baselines/multiple-subtitles-public-types.json",
"refactor/changes/2026-09-13-PKG-MULTI-SUB-04-public-types.md",
"refactor/scripts/multiple-subtitles-types.test.mjs",
"refactor/scripts/multiple-subtitles-package-types.mjs"
"refactor/scripts/multiple-subtitles-package-types.mjs",
"refactor/baselines/multiple-subtitles-approved-types.json",
"refactor/changes/2026-09-13-PKG-MULTI-SUB-04-approved-types.md"
],
"compatibleResolution": "Root/legacy retain 1.2.0 default-module extraction and replaceable synchronous factory types; additive runtime entry exposes accurate Promise and methods; writable default self alias preserves historical valid JS forms. Actual installed old 1.0.0/1.1.0 raw export-assignment extraction conflicts with 1.2.0 declarations and remains awaiting explicit resolution. Do not close on default-import success alone.",
"compatibleResolution": "Approved latest npm 1.2.0 root factory/namespace stays exact; additive runtime supplies accurate asynchronous and module forms. Earlier 1.0.0/1.1.0 type conflicts have explicit migration under ADR-025. Complete shipped archive/deep-entry and example acceptance remains 06; actual browser/core combinations remain 05.",
"closureCriteria": "Actual installed CJS/ESM/global/legacy consumers and their declarations are verified."
},
{
@@ -62,14 +62,24 @@ Promise.all(['artplayer-plugin-multiple-subtitles', 'artplayer-plugin-multiple-s
for (const [compiler, mode] of (plugin.label === 'candidate' ? candidateModes : historicalModes)) {
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/multiple-subtitles-public.ts' : 'refactor/fixtures/consumers/multiple-subtitles-published.ts'), 'utf8')
let source = fs.readFileSync(path.join(workspace, plugin.label === 'candidate' ? 'test/types/multiple-subtitles-public.ts' : 'refactor/fixtures/consumers/multiple-subtitles-published.ts'), 'utf8')
if (plugin.label === 'candidate' && mode === 'nodenext-esm') {
source = source
.replace('import legacy from \'artplayer-plugin-multiple-subtitles\'', 'import legacyModule from \'artplayer-plugin-multiple-subtitles\'\nconst legacy = legacyModule.default')
.replace('import legacyEntry from \'artplayer-plugin-multiple-subtitles/legacy\'', 'import legacyEntryModule from \'artplayer-plugin-multiple-subtitles/legacy\'\nconst legacyEntry = legacyEntryModule.default')
}
if (mode.endsWith('-no-interop'))
source = source.replace('import runtime from \'artplayer-plugin-multiple-subtitles/runtime\'', 'import runtime = require(\'artplayer-plugin-multiple-subtitles/runtime\')')
const filename = path.join(consumer, `consumer.${extension}`)
const historical = path.join(consumer, `historical.${extension}`)
fs.copyFileSync(path.join(workspace, 'refactor/fixtures/consumers/multiple-subtitles-published.ts'), historical)
const files = plugin.label === 'candidate' ? [filename, historical] : [filename]
if (mode === 'nodenext-cjs' && plugin.label === 'candidate') {
const commonjs = path.join(consumer, 'commonjs.cts')
fs.writeFileSync(commonjs, `import vtt = require('artplayer-plugin-multiple-subtitles'); import legacy = require('artplayer-plugin-multiple-subtitles/legacy'); import runtime = require('artplayer-plugin-multiple-subtitles/runtime'); vtt.default({subtitles: []}); legacy.default({subtitles: []}); runtime({}); runtime.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 vtt = require('artplayer-plugin-multiple-subtitles'); import legacy = require('artplayer-plugin-multiple-subtitles/legacy'); import runtime = require('artplayer-plugin-multiple-subtitles/runtime');
import type Artplayer from 'artplayer'; declare const art: Artplayer;
const replacement: typeof vtt.default = (_option) => (_art: Artplayer) => ({name: 'multipleSubtitles'});
const legacyReplacement: typeof legacy.default = replacement;
vtt.default({subtitles: []}); legacy.default({subtitles: []}); runtime({}); runtime.default({});
const option: runtime.RuntimeOption = {}; const pending: Promise<runtime.Result> = runtime(option)(art); void pending; 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 }
@@ -80,7 +90,7 @@ Promise.all(['artplayer-plugin-multiple-subtitles', 'artplayer-plugin-multiple-s
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.version === '1.2.0' && mode === 'nodenext-esm'
@@ -88,8 +98,23 @@ Promise.all(['artplayer-plugin-multiple-subtitles', 'artplayer-plugin-multiple-s
writeJson(path.join(output, `${plugin.label}-${compiler.version}-${mode}-diagnostics.json`), { diagnostics, historicalFailure })
assert.deepEqual(diagnostics.map(item => item.code), expected, `${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, 12, 'Installed declarations must reject all invalid uses')
if (plugin.label === 'candidate') {
assert.equal(invalid.length, 16, 'Installed declarations must reject all invalid uses')
const expectedLines = []
let line = 1
for (const text of source.split('\n')) {
if (text.trimStart().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/multiple-subtitles-published.ts'), 'utf8')) : diagnostics
assert.deepEqual(published.map(item => item.code), (plugin.label === 'candidate' || plugin.version === '1.2.0') && mode === 'nodenext-esm' ? [2322, 2344, 7019, 2349, 2344, 2322, 2344, 2349] : [], 'Preserve exact historical direct-consumer diagnostic codes')
const namespace = (plugin.label === 'candidate' || plugin.version === '1.2.0') && mode === 'nodenext-esm'
? compile(fs.readFileSync(path.join(workspace, 'refactor/fixtures/consumers/multiple-subtitles-published.ts'), 'utf8').replace('import subtitles from \'artplayer-plugin-multiple-subtitles\'', 'import subtitlesModule from \'artplayer-plugin-multiple-subtitles\'\nconst subtitles = subtitlesModule.default'))
: []
assert.deepEqual(namespace, [], 'Latest published NodeNext namespace remains valid')
const commonjs = {}
if (mode === 'node10-commonjs' || mode === 'nodenext-cjs') {
const fixture = fs.readFileSync(path.join(workspace, 'refactor/fixtures/consumers/multiple-subtitles-commonjs.ts'), 'utf8')
@@ -105,14 +130,14 @@ Promise.all(['artplayer-plugin-multiple-subtitles', 'artplayer-plugin-multiple-s
commonjs[form] = { diagnostics, typeChecks: diagnostics.length === 0 }
}
}
matrix.push({ plugin: plugin.label, compiler: compiler.version, mode, historicalFailure, diagnostics, invalid, commonjs })
matrix.push({ plugin: plugin.label, compiler: compiler.version, mode, historicalFailure, diagnostics, invalid, published, namespace, commonjs })
}
}
finally {
removeConsumer(consumer)
}
}
writeJson(path.join(output, 'report.json'), { suite: 'multiple-subtitles-isolated-package-types', introducedBy: 'PKG-MULTI-SUB-04', scope: 'Three actual published packages in five compiler modes and packed candidate in seven modes, installed outside the workspace with packed core. Offline frozen reinstalls and byte identity verified. Expected old NodeNext ESM and CommonJS module extraction failures remain explicit; these do not count as candidate compatibility success. Raw historical export-assignment type compatibility remains unresolved. Full device and distribution acceptance remains separate.', packages, published: candidates.slice(0, 3).map(pkg => ({ label: pkg.label, archive: pkg.archive, sha256: hash(fs.readFileSync(pkg.archive)), missingEntrypoints: pkg.missingEntrypoints })), matrix })
writeJson(path.join(output, 'report.json'), { suite: 'multiple-subtitles-isolated-package-types', introducedBy: 'PKG-MULTI-SUB-04', scope: 'Three actual published packages in five compiler modes and packed candidate in seven modes, installed outside the workspace with packed core. Offline frozen reinstalls and byte identity verified. Latest npm 1.2.0 factory shape, synchronous result and NodeNext namespace retained under approved ADR-025; its eight old direct-consumer diagnostics are preserved. Earlier 1.0.0/1.1.0 export-assignment conflicts have documented runtime-entry migration. Runtime asynchronous result and CommonJS namespace types pass independently. Full device and distribution acceptance remains separate.', packages, published: candidates.slice(0, 3).map(pkg => ({ label: pkg.label, archive: pkg.archive, sha256: hash(fs.readFileSync(pkg.archive)), missingEntrypoints: pkg.missingEntrypoints })), matrix })
console.log(`Multiple subtitles installed matrix verified: ${matrix.length} cases, including ${matrix.filter(item => item.historicalFailure).length} expected historical declaration failure; ${output}`)
}
@@ -40,8 +40,12 @@ artplayerPluginMultipleSubtitles({ subtitles: [{ url: 1 }] });`
test('Multiple subtitles public extraction stays exact in five modes while runtime methods need no cast', () => {
const source = fs.readFileSync('test/types/multiple-subtitles-public.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, 12)
const adapted = mode === 'nodenext-esm'
? source.replace('import legacy from \'artplayer-plugin-multiple-subtitles\'', 'import legacyModule from \'artplayer-plugin-multiple-subtitles\'\nconst legacy = legacyModule.default')
.replace('import legacyEntry from \'artplayer-plugin-multiple-subtitles/legacy\'', 'import legacyEntryModule from \'artplayer-plugin-multiple-subtitles/legacy\'\nconst legacyEntry = legacyEntryModule.default')
: source
assert.deepEqual(checkConsumer(compiler, mode, adapted), [], `${compiler.version} ${mode}`)
assert.equal(checkConsumer(compiler, mode, adapted.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')).length, 16)
}
})
+5 -2
View File
@@ -2328,7 +2328,7 @@
"ENG-06",
"CORE-07"
],
"status": "doing",
"status": "done",
"risk": "H",
"deliverable": "准确 parser/cue/tree/result 类型与真实异步形状",
"acceptance": "严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容",
@@ -2336,7 +2336,10 @@
"baselines/multiple-subtitles-runtime-types.json",
"changes/2026-09-13-PKG-MULTI-SUB-04-runtime-types.md",
"baselines/multiple-subtitles-public-types.json",
"changes/2026-09-13-PKG-MULTI-SUB-04-public-types.md"
"changes/2026-09-13-PKG-MULTI-SUB-04-public-types.md",
"type-compatibility-policy.md",
"baselines/multiple-subtitles-approved-types.json",
"changes/2026-09-13-PKG-MULTI-SUB-04-approved-types.md"
]
},
{
+18 -3
View File
@@ -1,4 +1,5 @@
import type { Factory, LegacyResult, Option, Result, RuntimeFactory, TrackOption } from 'artplayer-plugin-multiple-subtitles'
import type { RuntimeFactory as EntryFactory, RuntimeOption as EntryOption, Result as EntryResult, TrackOption as EntryTrack } from 'artplayer-plugin-multiple-subtitles/runtime'
import Artplayer from 'artplayer'
import legacy from 'artplayer-plugin-multiple-subtitles'
import legacyEntry from 'artplayer-plugin-multiple-subtitles/legacy'
@@ -12,10 +13,16 @@ type ExactResult = Assert<Equal<ReturnType<ReturnType<typeof legacy>>, LegacyRes
type ActualResult = Assert<Equal<ReturnType<ReturnType<typeof runtime>>, Promise<Result>>>
type OldTrack = Assert<Equal<TrackOption, { url?: string, name?: string, type?: 'vtt' | 'srt' | 'ass', encoding?: string, onParser?: (...args: object[]) => object }>>
type PublishedFactory = (option: { subtitles: TrackOption[] }) => (art: Artplayer) => { name: 'multipleSubtitles' }
type PublishedShape = Assert<Equal<typeof legacy, PublishedFactory>>
type RuntimeEntry = Assert<Equal<[EntryFactory, EntryOption, EntryResult, EntryTrack], [RuntimeFactory, { subtitles?: TrackOption[] }, Result, TrackOption]>>
const option: Option = { subtitles: [{ url: '/en.vtt', name: 'en', onParser: (...args) => args }] }
const art = new Artplayer({ container: '#player', url: '/video.mp4', plugins: [legacy(option), runtime(option)] })
const old: LegacyResult = legacy(option)(art)
const replacement: typeof legacy = (_option: Option) => (_art: Artplayer) => ({ name: 'multipleSubtitles' })
const assignToOld: PublishedFactory = legacy
const assignFromOld: typeof legacy = (_option: { subtitles: TrackOption[] }) => (_art: Artplayer) => ({ name: 'multipleSubtitles' })
legacyEntry(option)
const factory: RuntimeFactory = runtime
const pending: Promise<Result> = factory({})(art)
@@ -25,9 +32,11 @@ pending.then((result) => {
const returned: void = result.tracks(['en'])
result.tracks()
result.reset()
// @ts-expect-error Selection methods stay synchronous after awaited registration.
const selectionPromise: Promise<void> = result.reset()
// @ts-expect-error Selection names are strings.
result.tracks([1])
void returned
void [returned, selectionPromise]
})
// @ts-expect-error Historical subtitles field remains required.
legacy({})
@@ -51,5 +60,11 @@ runtime({ unknown: true })
runtime({})()
// @ts-expect-error Accurate runtime types cannot accept a synchronous registrar.
const wrong: typeof runtime = replacement
void [alias, absent, changed, wrong]
export type { ActualResult, ExactFactory, ExactOptions, ExactResult, OldTrack }
// @ts-expect-error Root replacement factories must not expose a required self alias.
legacy.default(option)
// @ts-expect-error Historical root result has only its name.
old.tracks()
// @ts-expect-error Historical root result has only its name.
old.reset()
void [alias, absent, changed, wrong, assignToOld, assignFromOld]
export type { ActualResult, ExactFactory, ExactOptions, ExactResult, OldTrack, PublishedShape, RuntimeEntry }