mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-15 00:47:44 -08:00
refactor(multiple-subtitles): [PKG-MULTI-SUB-04] expose compatible runtime type entry
This commit is contained in:
1 parent
7f226ff822
commit
d44e81ce22
34 files changed
+3346
-60
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,67 @@
|
||||
# PKG-MULTI-SUB-04 公开类型与实际安装检查点
|
||||
|
||||
本次处理 API-06/09/11。根入口和 legacy 保留旧的必填 subtitles 数组、可选轨道字段、
|
||||
同步 LegacyResult 推导以及可被普通函数替换的工厂类型;新增 `/runtime` 入口提供实际的
|
||||
Promise<Result>、tracks(names?) 和 reset()。Result/RuntimeOption 由公开定义统一维护,
|
||||
内部实现复用,parser 与资源宿主边界仍为私有。没有把 Promise 伪装成同步结果。
|
||||
|
||||
## 最终入口与旧用法
|
||||
|
||||
- `import subtitles from 'artplayer-plugin-multiple-subtitles'` 和 `/legacy` 的旧默认导入、
|
||||
Parameters、ReturnType 和替换函数仍通过;旧返回声明只保留 name,运行时始终异步。
|
||||
- 新代码可 `import subtitles from 'artplayer-plugin-multiple-subtitles/runtime'`,通过
|
||||
`await subtitles({ subtitles: [...] })(art)` 无断言取得 tracks/reset;options 必传,{} 合法。
|
||||
- root/runtime 复用同一产物及函数。现代和 legacy 函数增加可写、可枚举、可配置的
|
||||
`.default` 自引用,兼容旧 CommonJS 对象的有效 default 调用;现代直接调用继续有效。
|
||||
- root/legacy 使用独立 .d.mts/.d.cts 转发默认声明;runtime 的 .d.cts 用 export= 提供
|
||||
直接及 default 调用;typesVersions 支持 classic 解析。包内新增五个声明文件。
|
||||
- 编辑器通过正常 build:ts 语义生成旧全局工厂及命名类型空间。RuntimeFactory 是类型,
|
||||
没有新增同名全局函数。onParser 保留原字段类型及不调用的运行时行为。
|
||||
|
||||
## 实际兼容冲突仍待决
|
||||
|
||||
真实 1.0.0/1.1.0 包的 CommonJS 是对象.default,但声明 export= 将模块描述成直接函数;
|
||||
1.2.0 运行时是直接函数,声明却是默认导出模块。安装消费者已分别复现:
|
||||
|
||||
| 类型写法 | 1.0.0 / 1.1.0 | 1.2.0 | 本次候选 |
|
||||
| --- | --- | --- | --- |
|
||||
| default import,Parameters/ReturnType,普通函数替换 | 通过 classic | 通过 classic | 五种模式通过 |
|
||||
| import=require 的原始模块直接提取/替换 | 类型通过;实际直接调用失败 | 类型失败 | 类型失败 |
|
||||
| import=require 后取 .default 再提取/替换 | 类型失败;实际调用有效 | 类型通过 | 类型通过;实际也可调用 |
|
||||
| NodeNext ESM 默认导入 | 通过 | 原声明 8 项诊断 | 候选通过 |
|
||||
|
||||
不能将原始模块同时改成“可调用且必须有 default”的类型来静默结束任务:这样普通替换
|
||||
函数会缺少 default;若 default 可选,则已有无断言 default 调用失败。两个失败分别有
|
||||
2322 和 2722 的编译复现,TS 5.9.3/4.3.5 覆盖。当前候选保留 1.2 的模块声明,旧 JS 两种
|
||||
有效入口均可运行,但未声称兼容所有旧类型提取。需要明确接受这项类型迁移差异才能关闭
|
||||
对应边界;此前 Ads 的参数推导修正授权不自动覆盖此项。04 和 TYPE/EXPORT 风险保持 open。
|
||||
|
||||
## 已执行验证
|
||||
|
||||
- 本包 274 项通过;源码/main/legacy 正常、生命周期、合并各 39 项通过。新增两项验证
|
||||
script global/CJS 自别名、属性描述符、真实 async 注册、void 方法及销毁清理。
|
||||
- 公开类型五模式正例通过,移除 expect-error 各准确拒绝 12 个非法用法;实际旧三个
|
||||
tarball 声明与默认导入/替换消费者分别通过 TS 5.9.3/4.3.5 classic 检查。
|
||||
- strict 生产源码 TS 5.9.3/5.1.6 及每编译器 9 个负例通过;编辑器每编译器 2 个负例。
|
||||
- 新增 `yarn test:multiple-subtitles-types-package`:三个已发布 tarball × 五模式,
|
||||
候选实际 pack × 七模式,共 22 格;旧 1.2 NodeNext ESM 的失败单独登记,不计为兼容成功。
|
||||
使用仓库外消费者、packed core、offline install/frozen reinstall、锁字节一致和全部
|
||||
已安装成员字节核对,断言无 workspace 符号链接或类型逃逸。实际 Node 验证 root/runtime
|
||||
CJS/ESM 的身份以及 legacy default/require 的身份,不只检查 manifest 路径存在。
|
||||
- 初次矩阵复现旧 1.2 NodeNext 无上下文 callback 的 7019 诊断;另一次核对发现旧 TS
|
||||
诊断顺序不同。核实具体旧包错误后固定各编译器预期,没有把候选失败列为允许项。
|
||||
- Chromium 153.0.8010.12 / Firefox 155.0 / Windows WebKit 26.6,发布/候选核心,
|
||||
三入口每个 36 项、共 108 项通过。覆盖 SRT 显示/选择、取消、Blob、HTTP、视频切源、
|
||||
内嵌时间戳。无 skip/flaky;不是物理 Safari、移动设备或全部旧核心验收。
|
||||
- 完整 CI 2344 项通过(1989 单元、14 工程、341 基线),严格生产 TS 353 文件;显式
|
||||
lint 通过。正常 build 生成三个 dist 及 docs 副本,逐字一致;正常 build:ts 更新编辑器。
|
||||
|
||||
完整检查及内容指纹见[证据](../baselines/multiple-subtitles-public-types.json)。新增脚本已接入
|
||||
本包测试和主 baseline glob;实际安装脚本是独立较慢命令,最终分发集成仍归 06。
|
||||
没有新依赖、锁或版本变更。包内 README/ARCHITECTURE 同步入口和模块维护说明。
|
||||
|
||||
## 后续与回退
|
||||
|
||||
04 保持 doing,等待上述历史类型取舍结论;实体解码、完整核心/设备/编辑器验证仍归 05/06。
|
||||
本次独立本地检查点提交,不推送、不发布。回退恢复旧声明及 manifest,移除 runtime 入口和
|
||||
default 别名;此前请求、时间戳与资源修复保持。不能将本检查点视为整个包或项目已可发布。
|
||||
@@ -0,0 +1,11 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
// eslint-disable-next-line ts/no-require-imports -- Exercise historical export-assignment consumers.
|
||||
import plugin = require('artplayer-plugin-multiple-subtitles')
|
||||
|
||||
const vtt = plugin
|
||||
declare const art: Artplayer
|
||||
const options: Parameters<typeof vtt>[0] = { subtitles: [] }
|
||||
const result: ReturnType<ReturnType<typeof vtt>> = { name: 'multipleSubtitles' }
|
||||
const replacement: typeof vtt = (_option: Parameters<typeof vtt>[0]) => (_art: Artplayer) => result
|
||||
void vtt(options)(art).name
|
||||
void replacement
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Factory } from 'artplayer-plugin-multiple-subtitles'
|
||||
|
||||
declare const replacement: Factory
|
||||
const required: Factory & { default: Factory } = replacement
|
||||
const optional: Factory & { default?: Factory } = replacement
|
||||
optional.default({ subtitles: [] })
|
||||
void required
|
||||
@@ -0,0 +1,9 @@
|
||||
import Artplayer from 'artplayer'
|
||||
import subtitles from 'artplayer-plugin-multiple-subtitles'
|
||||
|
||||
const option: Parameters<typeof subtitles>[0] = { subtitles: [{ url: '/en.vtt', name: 'en', type: 'vtt', encoding: 'utf-8', onParser: (...args) => args }, {}] }
|
||||
const art = new Artplayer({ container: '#player', url: '/video.mp4', plugins: [subtitles(option)] })
|
||||
const result: ReturnType<ReturnType<typeof subtitles>> = { name: 'multipleSubtitles' }
|
||||
const replacement: typeof subtitles = (_option: Parameters<typeof subtitles>[0]) => (_art: Artplayer) => result
|
||||
void subtitles(option)(art).name
|
||||
void replacement
|
||||
+1
-1
@@ -515,7 +515,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)
|
||||
- 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-07: [记录](baselines/multiple-subtitles-timestamps.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md)
|
||||
- PKG-ADS-01: [记录](baselines/ads-release.json) [记录](baselines/ads-contract.md) [记录](baselines/ads-contract-validation.json) [记录](changes/2026-09-12-PKG-ADS-01-contract.md) [记录](scripts/ads-contract.test.mjs)
|
||||
- PKG-ADS-02: [记录](changes/2026-09-12-PKG-ADS-02-tests.md) [记录](ads-validation.md) [记录](baselines/ads-validation.json)
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-MULTI-SUB-04 公开类型与仓库外安装检查点(doing)
|
||||
|
||||
保留 root/legacy 的旧必填 subtitles、同步 LegacyResult 提取与可替换工厂;新增 runtime
|
||||
入口提供真实 Promise<Result> 和 tracks/reset,共享同一实现。新增可写 default 自别名、
|
||||
分开的 ESM/CJS 声明、classic typesVersions 和语义生成的编辑器类型,包内文档同步。
|
||||
实际旧三个 tarball 各五模式、候选 pack 七模式,共 22 格安装检查;offline/frozen 重装、
|
||||
全成员字节、类型未逃逸、真实 Node 入口身份通过。旧 1.2 NodeNext ESM 失败独立登记。
|
||||
公开声明每模式 12 个反例、编辑器各 2 个反例、严格运行时各 9 个反例通过;本包 274 项,
|
||||
源码/main/legacy 各 39 项,三浏览器两核心三入口共 108 项通过。完整 CI 2344 项通过
|
||||
(1989 单元、14 工程、341 基线),严格生产 TS 353 文件;实际 build 与 docs 副本一致。
|
||||
见[变更与待决表](changes/2026-09-13-PKG-MULTI-SUB-04-public-types.md)和
|
||||
[证据](baselines/multiple-subtitles-public-types.json)。没有新依赖、锁或版本修改。
|
||||
旧 1.0/1.1 原始 CommonJS 模块类型提取与 1.2 default 模块声明矛盾;必填/可选 default
|
||||
两种替代均有编译失败证据。JS 调用已兼容,纯旧类型差异尚未获接受,04 与 TYPE/EXPORT
|
||||
风险保持 open。VTT 的相同历史声明问题仍待决;不能扩用此前 Ads 的参数类型授权。
|
||||
223 项仍为 121 done、14 doing、88 todo;198 项风险。独立本地检查点,不推送、不发布。
|
||||
下一步处理类型取舍及其迁移说明;实体解码、完整设备/核心/编辑器仍待 05/06。
|
||||
|
||||
|
||||
## PKG-MULTI-SUB-07 内嵌时间戳与显示修复完成
|
||||
|
||||
修复数字时间戳被文本包装成 NaN;合法 VTT class 标记保留原生 processing instruction,
|
||||
|
||||
+12
-4
@@ -4256,9 +4256,13 @@
|
||||
"refactor/baselines/multiple-subtitles-release.json",
|
||||
"refactor/baselines/multiple-subtitles-contract.md",
|
||||
"test/multiple-subtitles.test.js",
|
||||
"refactor/scripts/multiple-subtitles-contract.test.mjs"
|
||||
"refactor/scripts/multiple-subtitles-contract.test.mjs",
|
||||
"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"
|
||||
],
|
||||
"compatibleResolution": "Preserve valid old consumers while exposing accurate methods and async types; review onParser variance separately.",
|
||||
"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."
|
||||
},
|
||||
{
|
||||
@@ -4332,9 +4336,13 @@
|
||||
"refactor/baselines/multiple-subtitles-release.json",
|
||||
"refactor/baselines/multiple-subtitles-contract.md",
|
||||
"test/multiple-subtitles.test.js",
|
||||
"refactor/scripts/multiple-subtitles-contract.test.mjs"
|
||||
"refactor/scripts/multiple-subtitles-contract.test.mjs",
|
||||
"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"
|
||||
],
|
||||
"compatibleResolution": "Preserve valid historical runtime import forms and explicitly assess type-only extraction conflicts.",
|
||||
"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": "Actual installed CJS/ESM/global/legacy consumers and their declarations are verified."
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import ts from 'typescript'
|
||||
import compat from 'typescript-compat'
|
||||
import { checkFiles, packedFiles } from '../../scripts/package-check.mjs'
|
||||
import { consumerDirectory, removeConsumer, run, workspace, writeJson } from '../../scripts/package-consumer.mjs'
|
||||
import { verifyMultipleSubtitlesContract } from './multiple-subtitles-contract.mjs'
|
||||
import { hash, readMember } from './releases.mjs'
|
||||
|
||||
async function main() {
|
||||
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use yarn test:multiple-subtitles-types-package')
|
||||
const yarn = process.env.npm_execpath
|
||||
assert(yarn && fs.existsSync(yarn))
|
||||
const output = fs.mkdtempSync(path.join(workspace, 'refactor/.cache/multiple-subtitles-package-types-'))
|
||||
console.log(`Multiple subtitles isolated package evidence: ${output}`)
|
||||
const contract = await verifyMultipleSubtitlesContract()
|
||||
const packages = []
|
||||
for (const name of ['artplayer', 'artplayer-plugin-multiple-subtitles']) {
|
||||
const archive = path.join(output, `${name}.tgz`)
|
||||
fs.writeFileSync(path.join(output, `${name}-pack.log`), run([yarn, 'pack', '--filename', archive], path.join(workspace, 'packages', name)))
|
||||
const manifest = JSON.parse(readMember(archive, 'package/package.json'))
|
||||
const files = packedFiles(archive)
|
||||
checkFiles(manifest, files, name === contract.baseline.release.name ? Object.keys(contract.baseline.release.files) : [])
|
||||
packages.push({ name, archive, sha256: hash(fs.readFileSync(archive)), files: Object.fromEntries(files.map(member => [member, hash(readMember(archive, member))])) })
|
||||
}
|
||||
const matrix = []
|
||||
const core = packages[0]
|
||||
const candidates = [
|
||||
...[...contract.baseline.previous].reverse().concat(contract.baseline.release).map(release => ({ ...release, archive: contract.archives.get(release.version), label: `published-${release.version}` })),
|
||||
{ ...packages[1], label: 'candidate' },
|
||||
]
|
||||
for (const plugin of candidates) {
|
||||
const consumer = consumerDirectory()
|
||||
try {
|
||||
writeJson(path.join(consumer, 'package.json'), { name: 'multiple-subtitles-isolated-consumer', private: true, dependencies: Object.fromEntries([core, plugin].map(pkg => [pkg.name, `file:${pkg.archive.replaceAll('\\', '/')}`])) })
|
||||
fs.copyFileSync(path.join(workspace, 'yarn.lock'), path.join(consumer, 'yarn.lock'))
|
||||
fs.writeFileSync(path.join(output, `${plugin.label}-install.log`), run([yarn, 'install', '--offline', '--ignore-scripts', '--non-interactive'], consumer))
|
||||
const lock = fs.readFileSync(path.join(consumer, 'yarn.lock'))
|
||||
fs.writeFileSync(path.join(output, `${plugin.label}-frozen-install.log`), run([yarn, 'install', '--offline', '--frozen-lockfile', '--ignore-scripts', '--non-interactive'], consumer))
|
||||
assert.deepEqual(fs.readFileSync(path.join(consumer, 'yarn.lock')), lock)
|
||||
fs.writeFileSync(path.join(output, `${plugin.label}-yarn.lock`), lock)
|
||||
for (const pkg of [core, plugin]) {
|
||||
const root = path.join(consumer, 'node_modules', pkg.name)
|
||||
assert.equal(fs.realpathSync(root), root, 'Installed package must not be a workspace link')
|
||||
for (const [member, expected] of Object.entries(pkg.files))
|
||||
assert.equal(hash(fs.readFileSync(path.join(root, member.slice(8)))), expected, `Installed bytes differ: ${member}`)
|
||||
}
|
||||
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-multiple-subtitles', 'artplayer-plugin-multiple-subtitles/legacy', 'artplayer-plugin-multiple-subtitles/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-multiple-subtitles/runtime'), require('artplayer-plugin-multiple-subtitles'));
|
||||
Promise.all(['artplayer-plugin-multiple-subtitles', 'artplayer-plugin-multiple-subtitles/runtime', 'artplayer-plugin-multiple-subtitles/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-multiple-subtitles/legacy')); assert.equal(legacy.default.default, legacy.default); }).catch(error => { console.error(error); process.exitCode = 1; });`
|
||||
: `const assert = require('node:assert/strict'); const value = require('artplayer-plugin-multiple-subtitles'); assert.equal(typeof value, '${plugin.version === '1.2.0' ? 'function' : 'object'}'); assert.equal(typeof (value.default || value)({}), 'function'); ${plugin.version === '1.2.0' ? '' : 'assert.throws(() => value({}), { name: \'TypeError\', message: \'value is not a function\' });'} assert.equal(typeof value.default, '${plugin.version === '1.2.0' ? 'undefined' : 'function'}');`)
|
||||
fs.writeFileSync(path.join(output, `${plugin.label}-runtime.log`), run([runtimeFile], consumer))
|
||||
const candidateModes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
|
||||
if (plugin.label === 'candidate')
|
||||
candidateModes.push([ts, 'node10-commonjs-no-interop'], [compat, 'node10-commonjs-no-interop'])
|
||||
const historicalModes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
|
||||
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')
|
||||
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({});`)
|
||||
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 }
|
||||
function compile(code) {
|
||||
fs.writeFileSync(filename, code)
|
||||
const program = compiler.createProgram(files, options)
|
||||
for (const file of program.getSourceFiles()) {
|
||||
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') }))
|
||||
}
|
||||
const diagnostics = compile(source)
|
||||
const historicalFailure = plugin.version === '1.2.0' && mode === 'nodenext-esm'
|
||||
const expected = historicalFailure ? [2322, 2344, 7019, 2349, 2344, 2322, 2344, 2349] : []
|
||||
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')
|
||||
const commonjs = {}
|
||||
if (mode === 'node10-commonjs' || mode === 'nodenext-cjs') {
|
||||
const fixture = fs.readFileSync(path.join(workspace, 'refactor/fixtures/consumers/multiple-subtitles-commonjs.ts'), 'utf8')
|
||||
const oldAssignment = plugin.label !== 'candidate' && plugin.version !== '1.2.0'
|
||||
for (const form of ['direct', 'default']) {
|
||||
const consumer = form === 'direct' ? fixture : fixture.replace('const vtt = plugin', 'const vtt = plugin.default')
|
||||
const diagnostics = compile(consumer)
|
||||
const expected = form === 'default'
|
||||
? oldAssignment ? [2339] : []
|
||||
: oldAssignment ? [] : compiler === compat ? [2344, 2322, 2344, 2741, 2344, 2349] : [2322, 2344, 2344, 2322, 2344, 2349]
|
||||
writeJson(path.join(output, `${plugin.label}-${compiler.version}-${mode}-require-${form}.json`), { diagnostics, expected })
|
||||
assert.deepEqual(diagnostics.map(item => item.code), expected, `${plugin.label} ${compiler.version} ${mode} require ${form}`)
|
||||
commonjs[form] = { diagnostics, typeChecks: diagnostics.length === 0 }
|
||||
}
|
||||
}
|
||||
matrix.push({ plugin: plugin.label, compiler: compiler.version, mode, historicalFailure, diagnostics, invalid, 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 })
|
||||
console.log(`Multiple subtitles installed matrix verified: ${matrix.length} cases, including ${matrix.filter(item => item.historicalFailure).length} expected historical declaration failure; ${output}`)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
@@ -0,0 +1,89 @@
|
||||
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 -- Public declaration compatibility runner.
|
||||
import test from 'node:test'
|
||||
import { ESLint } from 'eslint'
|
||||
import ts from 'typescript'
|
||||
import compat from 'typescript-compat'
|
||||
import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from '../../scripts/plugin-editor-types.mjs'
|
||||
import { checkConsumer } from '../../scripts/typecheck.mjs'
|
||||
import { verifyMultipleSubtitlesContract } from './multiple-subtitles-contract.mjs'
|
||||
import { readMember } from './releases.mjs'
|
||||
|
||||
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
|
||||
|
||||
test('Multiple subtitles editor globals keep old extraction and expose explicit asynchronous types', async () => {
|
||||
const source = fs.readFileSync('packages/artplayer-plugin-multiple-subtitles/types/artplayer-plugin-multiple-subtitles.d.ts', 'utf8')
|
||||
const generated = generatePluginEditorDeclaration(source, 'artplayerPluginMultipleSubtitles')
|
||||
const file = 'docs/assets/ts/artplayer-plugin-multiple-subtitles.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 core = fs.readFileSync('docs/assets/ts/artplayer.d.ts', 'utf8')
|
||||
const consumer = `declare const art: Artplayer;
|
||||
const option: artplayerPluginMultipleSubtitles.Option = { subtitles: [{name: 'en'}] };
|
||||
const old: artplayerPluginMultipleSubtitles.LegacyResult = artplayerPluginMultipleSubtitles(option)(art);
|
||||
declare const runtime: artplayerPluginMultipleSubtitles.RuntimeFactory;
|
||||
const pending: Promise<artplayerPluginMultipleSubtitles.Result> = runtime({})(art);
|
||||
pending.then(result => { result.tracks(['en']); result.reset(); });
|
||||
// @ts-expect-error Historical extraction remains synchronous.
|
||||
const changed: Promise<artplayerPluginMultipleSubtitles.Result> = old;
|
||||
// @ts-expect-error URL is text.
|
||||
artplayerPluginMultipleSubtitles({ subtitles: [{ url: 1 }] });`
|
||||
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, 2)
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
test('Multiple subtitles runtime CommonJS supports direct and default calls', () => {
|
||||
assert.deepEqual(checkConsumer(ts, 'nodenext-cjs', `import runtime = require('artplayer-plugin-multiple-subtitles/runtime');
|
||||
import legacy = require('artplayer-plugin-multiple-subtitles');
|
||||
import type Artplayer from 'artplayer'; declare const art: Artplayer;
|
||||
const first = runtime({})(art); const second = runtime.default({})(art);
|
||||
const promise: Promise<{name: 'multipleSubtitles', tracks(names?: string[]): void, reset(): void}> = first;
|
||||
legacy.default({subtitles: []})(art).name; void [promise, second];`), [])
|
||||
})
|
||||
|
||||
test('Multiple subtitles raw callable module alternatives retain the required-default versus replacement conflict', () => {
|
||||
const source = fs.readFileSync('refactor/fixtures/consumers/multiple-subtitles-export-alternatives.ts', 'utf8')
|
||||
for (const [compiler, mode] of [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [compat, 'node10-commonjs']]) {
|
||||
assert.deepEqual(checkConsumer(compiler, mode, source).map(item => ({ line: item.line, code: item.code })), [{ line: 4, code: 2322 }, { line: 6, code: 2722 }])
|
||||
}
|
||||
})
|
||||
|
||||
test('Multiple subtitles actual three historical declarations retain default-import and replacement consumers', async () => {
|
||||
const contract = await verifyMultipleSubtitlesContract()
|
||||
const consumer = fs.readFileSync('refactor/fixtures/consumers/multiple-subtitles-published.ts', 'utf8')
|
||||
for (const release of [contract.baseline.release, ...contract.baseline.previous]) {
|
||||
const old = readMember(contract.archives.get(release.version), `package/${release.manifest.types.replace(/^\.\//, '')}`).toString()
|
||||
for (const compiler of [ts, compat]) {
|
||||
const filename = path.resolve('test/types/multiple-subtitles-historical-consumer.ts')
|
||||
const declaration = path.resolve('test/types/multiple-subtitles-historical.d.ts')
|
||||
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: true, module: compiler.ModuleKind.CommonJS, moduleResolution: compiler.ModuleResolutionKind.NodeJs, target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'] }
|
||||
function historical(source) {
|
||||
const host = compiler.createCompilerHost(options)
|
||||
const read = host.getSourceFile.bind(host)
|
||||
host.getSourceFile = (file, language, ...rest) => path.resolve(file) === filename
|
||||
? compiler.createSourceFile(file, source.replace('from \'artplayer-plugin-multiple-subtitles\'', 'from \'./multiple-subtitles-historical\''), language, true)
|
||||
: path.resolve(file) === declaration ? compiler.createSourceFile(file, old, language, true) : read(file, language, ...rest)
|
||||
const exists = host.fileExists.bind(host)
|
||||
host.fileExists = file => path.resolve(file) === declaration || exists(file)
|
||||
return compiler.getPreEmitDiagnostics(compiler.createProgram([filename, declaration], options, host)).map(error => error.code)
|
||||
}
|
||||
assert.deepEqual(historical(consumer), [])
|
||||
assert.deepEqual(historical(`${consumer}\nsubtitles();`), [2554])
|
||||
assert.deepEqual(historical(`${consumer}\nconst absent: Parameters<typeof subtitles>[0] = undefined;`), [2322])
|
||||
assert.deepEqual(checkConsumer(compiler, 'node10-commonjs', consumer), [])
|
||||
}
|
||||
}
|
||||
})
|
||||
+3
-1
@@ -2325,7 +2325,9 @@
|
||||
"acceptance": "严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容",
|
||||
"evidence": [
|
||||
"baselines/multiple-subtitles-runtime-types.json",
|
||||
"changes/2026-09-13-PKG-MULTI-SUB-04-runtime-types.md"
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user