mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-16 09:36:16 -08:00
refactor(jassub): [PKG-JASSUB-04] migrate owned TypeScript and preserve public types
This commit is contained in:
1 parent
38e7b37878
commit
5e445b1af6
33 files changed
+3636
-61
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,67 @@
|
||||
# PKG-JASSUB-04 自有 TS 与兼容类型
|
||||
|
||||
## 结构与运行时边界
|
||||
|
||||
自有 index/registration 迁为严格 TS,职责延续 03 的工厂与资源注册分离。
|
||||
JassubHost 只需要 video/on/off,真实 Artplayer fixture 验证结构兼容;内部返回
|
||||
RuntimeResult,不使用已知不准确的旧根声明来约束实现。私有 jassub.es.d.ts
|
||||
桥接 vendor,只补 adapter 使用的 _destroyed 与最小样式写入契约,保留数值 20
|
||||
经 DOM setter 转字符串的历史行为。allowJs=false,不用 any/ts-nocheck 隐藏自有实现。
|
||||
|
||||
vendor JS 是明确的第三方例外,本次完全不改字节和算法。Worker/WASM/font 来源
|
||||
和 07 修复责任继续保留。两个自有模块经 esbuild 去类型后与 38e7b378 的 JS 完全
|
||||
相同;正常构建 main/legacy/ESM 与该提交一致(仅归一化 LF),docs 副本逐字节一致。
|
||||
因此不以这次类型迁移声称新增浏览器播放验收,03 的原生验证范围和限制仍适用。
|
||||
|
||||
## API-09/11 兼容策略
|
||||
|
||||
实际 npm 1.0.0/1.1.0 根声明相同,本次逐字保留(忽略换行):资源 URL 必填、
|
||||
force-first resize、三个 Promise 返回值和两个扩展索引都没有收窄。工厂完整
|
||||
双向赋值、Parameters/ReturnType 和原有非法调用的错误位置继续验证。
|
||||
|
||||
新增可选 /runtime 与根入口复用同一 JavaScript,准确声明可选 options、同步
|
||||
registration/resize/setVideo/destroy、width/height/top/left/force 顺序,以及
|
||||
EventTarget、字幕/样式/字体方法。sendMessage 实际是 Promise<void>,完成仅代表
|
||||
发出消息;不声称 Worker 已处理。ASS Style 是 libass 数值索引,Start/Duration
|
||||
是毫秒,查询结果没有上游 npm 类型误写的 _index。旧入口消费者不需要迁移,
|
||||
选择准确入口时的推导差异和调用示例写在包内 types/README.md。
|
||||
|
||||
精确 typesVersions 修复旧 Node10 下 /legacy 找不到声明;实际两旧包保留原 2307
|
||||
证据,候选仅允许消除该 import 行的错误。NodeNext ESM 旧根 namespace 行为及
|
||||
直接调用失败保持一致;/runtime 使用 conditional .d.mts/.d.cts 与旧编译器 .d.ts。
|
||||
不添加不存在的 factory.default。在线编辑器旧生成方式同时包含 default/export=
|
||||
而报 2309,两个编译器已复现;接入已有语义生成器并验证生成文件及正反例。
|
||||
|
||||
## 类型审查发现的真实缺陷
|
||||
|
||||
JASSUB-QUERY-01:getEvents/getStyles 的 timeout 和 Worker-error 路径缺少第二个
|
||||
参数,公开 wrapper 先解构 undefined 抛 TypeError,用户回调为零次,两个请求
|
||||
监听器保留;Worker-error 路径还保留定时器。冻结实现的四个失败测试保留此行为。
|
||||
该问题纳入 PKG-JASSUB-07 的独立 vendor 修复,不在 04 中混改,不标为已修复。
|
||||
准确回调类型允许 Error/ErrorEvent 与缺失数组,不能代表错误回调已经可靠送达。
|
||||
|
||||
## 验证、命令与限制
|
||||
|
||||
完整证据与最终安装报告摘要见 [类型验证](../baselines/jassub-types-validation.json)。
|
||||
验证包括:严格 TS 自有项目及全仓流程,实际旧声明正反例、生成编辑器,源码和
|
||||
main/legacy 注册回归,完整 vendor Worker JS/本地 WASM 的字幕查询及部分字段更新。
|
||||
WASM 测试控制消息传输与 fetch,不声称浏览器 Worker、绘制或完整字体加载验收。
|
||||
|
||||
仓库外实际安装两个历史 npm 归档与 Yarn pack 候选,逐文件核对 hash 和非链接,
|
||||
离线安装后强制 frozen 重装并检查锁文件。TS 5.9.3 Node10/NodeNext CJS/ESM/Bundler
|
||||
及 TS 4.3.5 Node10 共 15 组;旧入口七项非法调用、准确入口十四项非法调用逐条
|
||||
验证。CJS/ESM 根/legacy 工厂和 root/runtime 同一对象从实际安装包执行,不启动
|
||||
vendor 或模拟一次浏览器播放。历史 raw 错误与正确 namespace 消费均单独记录。
|
||||
|
||||
复跑:yarn test:jassub、yarn test:jassub-types、yarn test:jassub-types-package、
|
||||
yarn typecheck、yarn build:ts artplayer-plugin-jassub、yarn build artplayer-plugin-jassub。
|
||||
新增脚本复用既有依赖,无新增依赖或锁变更;类型测试进入既有 baseline glob,
|
||||
运行测试进入 test:unit/test:jassub。隔离安装脚本是可复跑的分包验证,尚不声称
|
||||
全部生态安装矩阵已经接入远程 CI 或运行通过。
|
||||
|
||||
## 剩余任务与回退
|
||||
|
||||
07 继续 vendor 生命周期、时钟和查询错误清理;05 继续新旧核心/浏览器/真实设备
|
||||
组合;06 继续完整分发和许可通知,VENDOR-04/05 保留。版本仍由各包独立 major
|
||||
任务统一落地。本任务没有发布、推送或减少最终三轮复盘要求。
|
||||
回退本任务并正常重建即可恢复旧 JS 入口/类型生成选择;03 修复仍是独立提交。
|
||||
@@ -0,0 +1,8 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { RuntimeFactory, RuntimeResult } from '../../../packages/artplayer-plugin-jassub/types/runtime-api.js'
|
||||
import factory from '../../../packages/artplayer-plugin-jassub/src'
|
||||
|
||||
declare const art: Artplayer
|
||||
const registration: RuntimeResult = factory()(art)
|
||||
const publicFactory: RuntimeFactory = factory
|
||||
export { publicFactory, registration }
|
||||
+4
-3
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 232 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 64 / doing 14 / blocked 0 / done 154 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 63 / doing 14 / blocked 0 / done 155 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -232,10 +232,10 @@
|
||||
| PKG-JASSUB-01 | artplayer-plugin-jassub<br>核对包契约与历史用法 | BASE-05 | 选项透传、result.instance、vendor 来源、worker/WASM/font 路径 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结;接续 VENDOR-04/05,核对 wrapper/worker/WASM/font 的独立来源与通知,保持资源路径和选项透传 | H | done |
|
||||
| PKG-JASSUB-02 | artplayer-plugin-jassub<br>建立特有行为与错误测试 | PKG-JASSUB-01, ENG-03, ENG-05 | ASS 字体、时钟/seek/倍率/resize、加载失败和销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
|
||||
| PKG-JASSUB-03 | artplayer-plugin-jassub<br>整理内部职责与资源 | PKG-JASSUB-02, CORE-15, CORE-16 | 仅整理自有 adapter/销毁;保留第三方文件及来源 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
|
||||
| PKG-JASSUB-04 | artplayer-plugin-jassub<br>迁移自有源码和公开类型 | PKG-JASSUB-03, ENG-04, ENG-06, CORE-07 | JASSUB option/instance 的兼容类型包装,vendor JS 例外记录 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
|
||||
| PKG-JASSUB-04 | artplayer-plugin-jassub<br>迁移自有源码和公开类型 | PKG-JASSUB-03, ENG-04, ENG-06, CORE-07 | JASSUB option/instance 的兼容类型包装,vendor JS 例外记录 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done |
|
||||
| PKG-JASSUB-05 | artplayer-plugin-jassub<br>验证新旧核心和组合 | PKG-JASSUB-04, CORE-22, PKG-JASSUB-07 | 真实 worker/WASM 字幕渲染与全屏、旧核心测试 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
| PKG-JASSUB-06 | artplayer-plugin-jassub<br>验证分发并同步文档 | PKG-JASSUB-05, ENG-07 | jassub.js、外部资源路径、许可和离线失败记录;接续 VENDOR-04/05 完整组件/字体通知、LGPL 源码分发核对及六份未明确字体的分发处置 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录;来源身份已由01冻结,完整通知和未明确字体的发布处理必须闭环,不能以来源相同替代许可结论 | H | todo |
|
||||
| PKG-JASSUB-07 | artplayer-plugin-jassub<br>修复 vendor 生命周期与字幕时钟 | PKG-JASSUB-03, PKG-JASSUB-02 | 独立记录 vendor 适配差异;幂等销毁、跨容器 setVideo、构造回滚、逐帧归属和倍率/能力边界 | 保持公开真实 instance、同步方法与资源路径;保留原 vendor 来源指纹并记录每项实际补丁;候选修复正例和真实字幕/失败/时钟验证通过,未证实设备不得冒充通过 | H | todo |
|
||||
| PKG-JASSUB-07 | artplayer-plugin-jassub<br>修复 vendor 生命周期与字幕时钟 | PKG-JASSUB-03, PKG-JASSUB-02 | 独立记录 vendor 适配差异;幂等销毁、跨容器 setVideo、构造回滚、逐帧归属和倍率/能力边界;查询 timeout/Worker error 的缺失数据回调及每请求监听器/定时器清理(JASSUB-QUERY-01) | 保持公开真实 instance、同步方法与资源路径;保留原 vendor 来源指纹并记录每项实际补丁;候选修复正例和真实字幕/失败/时钟验证通过,未证实设备不得冒充通过 | H | todo |
|
||||
|
||||
## 5 包迁移:artplayer-plugin-danmuku-mask
|
||||
|
||||
@@ -535,6 +535,7 @@
|
||||
- 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) [记录](baselines/jassub-provenance.json) [记录](baselines/jassub-provenance-validation.json) [记录](changes/2026-09-14-PKG-JASSUB-01-provenance.md)
|
||||
- PKG-JASSUB-02: [记录](changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md) [记录](baselines/jassub-failures-validation.json)
|
||||
- PKG-JASSUB-03: [记录](changes/2026-09-14-PKG-JASSUB-03-registration.md) [记录](baselines/jassub-registration-validation.json)
|
||||
- PKG-JASSUB-04: [记录](changes/2026-09-14-PKG-JASSUB-04-types.md) [记录](baselines/jassub-types-validation.json)
|
||||
- PKG-MASK-01: [记录](baselines/danmuku-mask-release.json) [记录](baselines/danmuku-mask-registry.json) [记录](baselines/danmuku-mask-contract.md) [记录](baselines/danmuku-mask-contract-validation.json) [记录](changes/2026-09-13-PKG-MASK-01-contract.md)
|
||||
- PKG-MASK-02: [记录](changes/2026-09-13-PKG-MASK-02-failures.md) [记录](baselines/danmuku-mask-failures-validation.json)
|
||||
- PKG-MASK-03: [记录](changes/2026-09-13-PKG-MASK-03-lifecycle.md) [记录](baselines/danmuku-mask-lifecycle-validation.json)
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-JASSUB-04 自有 TS 与准确类型入口完成
|
||||
|
||||
两个自有模块已迁为严格 TS,编译后的 JS 和三种正常构建产物保持上一任务行为。
|
||||
实际 npm 两版旧根声明完整保留,新增 /runtime 描述真实同步方法/可选参数,
|
||||
修复旧 Node10 legacy 解析与编辑器导出冲突;无 interop 的 CJS 类型引用也通过。
|
||||
联合 139 项、源码/main/legacy 各 15 项、406 生产 TS 全仓检查通过;实际两旧包及
|
||||
候选的仓库外安装 15 编译器组合通过,新增真实 WASM 查询/修改测试。
|
||||
[变更记录](changes/2026-09-14-PKG-JASSUB-04-types.md)和
|
||||
[安装与类型证据](baselines/jassub-types-validation.json)保留中间失败及最终候选。
|
||||
新增 JASSUB-QUERY-01 有冻结错误复现,交由下一项 07 修复;没有宣称新浏览器
|
||||
播放或发布验收。下一步继续 vendor 生命周期/时钟/查询清理,05/06 仍未完成。
|
||||
|
||||
|
||||
## PKG-JASSUB-03 自有注册/清理改造完成
|
||||
|
||||
入口与 registration 分离;自定义 canvas、直接实例销毁后的宿主清理、重入与
|
||||
|
||||
@@ -248,3 +248,4 @@
|
||||
| JASSUB-VIDEO-FRAME-01 | open / 已复现 | Old video frame callbacks can drive replacement subtitle time and duplicate chains | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-RATE-01 | open / 已复现 | Fallback ratechange sends an Event instead of numeric playbackRate | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-NATIVE-FRAME-01 | open / 已复现 | Windows WebKit default frame-clock path does not produce subtitle demands in actual playback | PKG-JASSUB-02, PKG-JASSUB-07, PKG-JASSUB-05 |
|
||||
| JASSUB-QUERY-01 | open / 已复现 | Subtitle query errors throw before calling the consumer and leave request listeners attached | PKG-JASSUB-07, PKG-JASSUB-05 |
|
||||
+18
-2
@@ -2,7 +2,7 @@
|
||||
"schemaVersion": 1,
|
||||
"task": "BASE-07",
|
||||
"sourceCommit": "e01f39ff",
|
||||
"updatedAt": "2026-09-13",
|
||||
"updatedAt": "2026-09-14",
|
||||
"note": "Open means closure evidence is outstanding; source-observed and unverified are not reproduced runtime bugs. A source issue already mitigated in the workspace still requires candidate regression evidence. No accepted-with-scope item is implied by baseline task completion.",
|
||||
"items": [
|
||||
{
|
||||
@@ -779,7 +779,7 @@
|
||||
"PKG-JASSUB-05"
|
||||
],
|
||||
"evidence": [
|
||||
"packages/artplayer-plugin-jassub/src/index.js",
|
||||
"packages/artplayer-plugin-jassub/src/index.ts",
|
||||
"docs/assets/example/jassub.js"
|
||||
],
|
||||
"compatibleResolution": "Preserve options pass-through, result.instance and worker/wasm/font URLs. Wrapper, worker and WASM must be a verified compatible set.",
|
||||
@@ -5535,6 +5535,22 @@
|
||||
],
|
||||
"compatibleResolution": "Investigate native frame delivery and establish a compatible capability response; explicit event-clock success is diagnostic only.",
|
||||
"closureCriteria": "Default native behavior is verified or an explicit reviewed environment limitation is recorded; no silent global fallback or substitution of diagnostic mode for default acceptance."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-QUERY-01",
|
||||
"title": "Subtitle query errors throw before calling the consumer and leave request listeners attached",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-JASSUB-07",
|
||||
"PKG-JASSUB-05"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-04-types.md",
|
||||
"test/jassub-runtime.test.js"
|
||||
],
|
||||
"compatibleResolution": "Handle absent worker error payloads, release per-request listeners and timers before invoking user callbacks, and preserve successful synchronous query registration and callback data.",
|
||||
"closureCriteria": "Actual vendor timeout and Worker-error paths invoke the consumer exactly once with the original error, leave no request listeners/timers, and successful real-WASM events/styles remain unchanged. Browser error/recovery acceptance remains PKG-JASSUB-05."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
export const jassubInvalidStatements = [
|
||||
'jassub()',
|
||||
'jassub(undefined)',
|
||||
'jassub({ workerUrl: "/worker.js", wasmUrl: "/worker.wasm" })',
|
||||
'jassub({ workerUrl: 1, wasmUrl: "/worker.wasm", modernWasmUrl: "/modern.wasm" })',
|
||||
'instance.resize(640)',
|
||||
'const synchronousDestroy: void = instance.destroy()',
|
||||
'const wrongName: "other" = result.name',
|
||||
]
|
||||
|
||||
export function jassubConsumerSource(mode, raw = false) {
|
||||
let source = fs.readFileSync(new URL('../../test/types/jassub-public.ts', import.meta.url), 'utf8')
|
||||
if (mode === 'nodenext-esm' && !raw) {
|
||||
source = source
|
||||
.replace('import jassub from \'artplayer-plugin-jassub\'', 'import jassubModule from \'artplayer-plugin-jassub\'\nconst jassub = jassubModule.default')
|
||||
.replace('import legacy from \'artplayer-plugin-jassub/legacy\'', 'import legacyModule from \'artplayer-plugin-jassub/legacy\'\nconst legacy = legacyModule.default')
|
||||
}
|
||||
return source
|
||||
}
|
||||
|
||||
export function jassubRuntimeSource(mode) {
|
||||
const source = fs.readFileSync(new URL('../../test/types/jassub-runtime.ts', import.meta.url), 'utf8')
|
||||
return mode.endsWith('-no-interop')
|
||||
? source
|
||||
.replace('import runtime from \'artplayer-plugin-jassub/runtime\'', 'import runtime = require(\'artplayer-plugin-jassub/runtime\')')
|
||||
.replace('import type Artplayer from \'artplayer\'', 'type Core = typeof import(\'artplayer\'); type Artplayer = Core extends { default: { prototype: infer I } } ? I : Core extends { prototype: infer I } ? I : never')
|
||||
: source
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
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 { jassubConsumerSource, jassubInvalidStatements, jassubRuntimeSource } from './jassub-consumer.mjs'
|
||||
import { verifyJassubContract } from './jassub-contract.mjs'
|
||||
import { hash, readMember } from './releases.mjs'
|
||||
|
||||
const name = 'artplayer-plugin-jassub'
|
||||
const declaration = `package/types/${name}.d.ts`
|
||||
const shape = diagnostics => diagnostics.map(({ code, file, line }) => ({ code, file, line }))
|
||||
let evidenceOutput
|
||||
|
||||
function installedBytes(consumer, packages) {
|
||||
for (const pkg of packages) {
|
||||
const root = path.join(consumer, 'node_modules', pkg.name)
|
||||
assert.equal(fs.realpathSync(root), root, 'Installed package must not be a workspace link')
|
||||
assert(!fs.lstatSync(root).isSymbolicLink(), 'Installed package root is a link')
|
||||
for (const [member, expected] of Object.entries(pkg.files)) {
|
||||
const file = path.join(root, member.slice(8))
|
||||
assert.equal(fs.realpathSync(file), file, `Installed member resolves through a link: ${member}`)
|
||||
assert(fs.lstatSync(file).isFile(), `Installed member is not a regular file: ${member}`)
|
||||
assert.equal(hash(fs.readFileSync(file)), expected, `Installed bytes differ: ${member}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function runtimeProbe(candidate, old) {
|
||||
return `const assert = require('node:assert/strict');
|
||||
global.self = global;
|
||||
const names = ${JSON.stringify([name, `${name}/legacy`, ...(candidate ? [`${name}/runtime`] : [])])};
|
||||
const old = ${old};
|
||||
const commonjs = names.map(name => require(name));
|
||||
for (const value of commonjs) {
|
||||
assert.equal(typeof value, old ? 'object' : 'function');
|
||||
assert.equal(typeof value.default, old ? 'function' : 'undefined');
|
||||
const factory = old ? value.default : value;
|
||||
assert.equal(typeof factory(), 'function');
|
||||
assert.equal(typeof factory({workerUrl: '/worker.js', wasmUrl: '/wasm.wasm', modernWasmUrl: '/modern.wasm'}), 'function');
|
||||
}
|
||||
${candidate ? 'assert.equal(commonjs[2], commonjs[0]); assert.equal(require.resolve(names[2]), require.resolve(names[0]));' : ''}
|
||||
Promise.all(names.map(name => import(name))).then(modules => {
|
||||
assert.equal(typeof modules[0].default, 'function');
|
||||
assert.equal(typeof modules[0].default(), 'function');
|
||||
assert.equal(modules[1].default, commonjs[1]);
|
||||
const legacy = old ? modules[1].default.default : modules[1].default;
|
||||
assert.equal(typeof legacy(), 'function');
|
||||
${candidate ? 'assert.equal(modules[2].default, modules[0].default); assert.equal(typeof modules[2].default(), \'function\');' : ''}
|
||||
console.log('Installed CJS/ESM root/legacy lazy factories and conditional runtime identity passed. Registrars were not invoked; no vendor initialization.');
|
||||
}).catch(error => { console.error(error); process.exitCode = 1; });
|
||||
`
|
||||
}
|
||||
|
||||
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:jassub-types-package')
|
||||
const yarn = process.env.npm_execpath
|
||||
assert(yarn && fs.existsSync(yarn))
|
||||
const output = fs.mkdtempSync(path.join(workspace, 'refactor/.cache/jassub-package-types-'))
|
||||
evidenceOutput = output
|
||||
console.log(`JASSUB isolated installed consumer evidence: ${output}`)
|
||||
const fixtureHashes = () => Object.fromEntries(['refactor/scripts/jassub-consumer.mjs', 'test/types/jassub-public.ts', 'test/types/jassub-runtime.ts'].map(file => [file, hash(fs.readFileSync(path.join(workspace, file)))]))
|
||||
const fixtures = fixtureHashes()
|
||||
const contract = await verifyJassubContract()
|
||||
const releases = [...contract.baseline.previous, contract.baseline.release]
|
||||
assert.deepEqual(releases.map(release => release.version), ['1.0.0', '1.1.0'])
|
||||
const packages = []
|
||||
for (const packageName of ['artplayer', name]) {
|
||||
const archive = path.join(output, `${packageName}.tgz`)
|
||||
fs.writeFileSync(path.join(output, `${packageName}-pack.log`), run([yarn, 'pack', '--filename', archive], path.join(workspace, 'packages', packageName)))
|
||||
const manifest = JSON.parse(readMember(archive, 'package/package.json'))
|
||||
const files = packedFiles(archive)
|
||||
checkFiles(manifest, files, packageName === name ? Object.keys(contract.baseline.release.files) : [])
|
||||
packages.push({ name: packageName, archive, manifest, sha256: hash(fs.readFileSync(archive)), files: Object.fromEntries(files.map(member => [member, hash(readMember(archive, member))])) })
|
||||
}
|
||||
const [core, candidate] = packages
|
||||
const rootDeclarations = releases.map((release) => {
|
||||
const current = readMember(candidate.archive, declaration)
|
||||
const previous = readMember(contract.archives.get(release.version), declaration)
|
||||
const normalize = bytes => bytes.toString('utf8').replaceAll('\r\n', '\n')
|
||||
assert.equal(normalize(current), normalize(previous), `Preserve npm ${release.version} root declaration; only checkout CRLF may differ`)
|
||||
return { version: release.version, candidateSha256: hash(current), publishedSha256: hash(previous), bytesEqual: current.equals(previous), normalizedLFSha256: hash(normalize(current)) }
|
||||
})
|
||||
const exports = candidate.manifest.exports
|
||||
assert.equal(exports['./runtime'].import.types, './types/runtime.d.mts')
|
||||
assert.equal(exports['./runtime'].require.types, './types/runtime.d.cts')
|
||||
assert.equal(exports['./runtime'].import.default, exports['.'].import)
|
||||
assert.equal(exports['./runtime'].require.default, exports['.'].require)
|
||||
assert.deepEqual(candidate.manifest.typesVersions['*'].runtime, ['types/runtime.d.ts'])
|
||||
assert.deepEqual(candidate.manifest.typesVersions['*'].legacy, [`types/${name}.d.ts`])
|
||||
const plugins = [
|
||||
...releases.map(release => ({ ...release, archive: contract.archives.get(release.version), label: `published-${release.version}` })),
|
||||
{ ...candidate, label: 'candidate' },
|
||||
]
|
||||
const matrix = []
|
||||
const historical = new Map()
|
||||
for (const plugin of plugins) {
|
||||
const consumer = consumerDirectory()
|
||||
try {
|
||||
writeJson(path.join(consumer, 'package.json'), { name: 'jassub-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))
|
||||
installedBytes(consumer, [core, plugin])
|
||||
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', '--force', '--ignore-scripts', '--non-interactive'], consumer))
|
||||
assert.deepEqual(fs.readFileSync(path.join(consumer, 'yarn.lock')), lock, 'Frozen reinstall changed consumer lock')
|
||||
installedBytes(consumer, [core, plugin])
|
||||
fs.writeFileSync(path.join(output, `${plugin.label}-yarn.lock`), lock)
|
||||
const runtimeFile = path.join(consumer, 'exports.cjs')
|
||||
fs.writeFileSync(runtimeFile, runtimeProbe(plugin.label === 'candidate', plugin.version === '1.0.0'))
|
||||
fs.writeFileSync(path.join(output, `${plugin.label}-runtime.log`), run([runtimeFile], consumer))
|
||||
for (const [compiler, mode] of [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]) {
|
||||
const next = mode.startsWith('nodenext')
|
||||
const filename = path.join(consumer, `consumer.${next ? mode.endsWith('-cjs') ? 'cts' : 'mts' : 'ts'}`)
|
||||
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: true, target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], module: next ? compiler.ModuleKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleKind.ESNext : compiler.ModuleKind.CommonJS, moduleResolution: next ? compiler.ModuleResolutionKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleResolutionKind.Bundler : compiler.ModuleResolutionKind.NodeJs }
|
||||
let declarations = []
|
||||
const compile = (source, esModuleInterop = options.esModuleInterop) => {
|
||||
fs.writeFileSync(filename, source)
|
||||
const program = compiler.createProgram([filename], { ...options, esModuleInterop })
|
||||
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()))), `Type escaped consumer: ${actual}`)
|
||||
}
|
||||
declarations = program.getSourceFiles().filter(file => !program.isSourceFileDefaultLibrary(file)).map(file => path.relative(consumer, file.fileName).replaceAll('\\', '/')).sort()
|
||||
return compiler.getPreEmitDiagnostics(program).map(diagnostic => ({ code: diagnostic.code, file: diagnostic.file ? path.relative(consumer, diagnostic.file.fileName).replaceAll('\\', '/') : null, line: diagnostic.file && diagnostic.start !== undefined ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start).line + 1 : null, message: compiler.flattenDiagnosticMessageText(diagnostic.messageText, '\n').replaceAll(consumer.replaceAll('\\', '/'), '<consumer>').replaceAll(consumer, '<consumer>') }))
|
||||
}
|
||||
const rawSource = jassubConsumerSource(mode, true)
|
||||
const raw = compile(rawSource)
|
||||
const key = `${compiler.version}:${mode}`
|
||||
const entry = { plugin: plugin.label, compiler: compiler.version, mode, raw, diagnostics: null, invalid: null, runtime: null }
|
||||
matrix.push(entry)
|
||||
writeJson(path.join(output, 'matrix-progress.json'), matrix)
|
||||
const legacyLines = rawSource.split('\n').flatMap((line, index) => /from ['"]artplayer-plugin-jassub\/legacy['"]/.test(line) ? [index + 1] : [])
|
||||
assert.equal(legacyLines.length, 1, 'The raw consumer must contain one identifiable legacy import')
|
||||
const legacyFailure = { code: 2307, file: path.basename(filename), line: legacyLines[0] }
|
||||
if (plugin.label === 'published-1.0.0') {
|
||||
if (mode === 'node10-commonjs')
|
||||
assert.deepEqual(shape(raw), [legacyFailure], 'Preserve the exact old Node10 legacy resolution error')
|
||||
if (mode === 'nodenext-esm')
|
||||
assert(raw.length > 0, 'Keep raw historical NodeNext namespace failures visible')
|
||||
historical.set(key, { raw: shape(raw) })
|
||||
}
|
||||
else {
|
||||
const expected = historical.get(key).raw
|
||||
assert.deepEqual(shape(raw), plugin.label === 'candidate' && mode === 'node10-commonjs' ? expected.filter(item => item.code !== legacyFailure.code || item.file !== legacyFailure.file || item.line !== legacyFailure.line) : expected, 'Only the exact recorded old Node10 legacy resolution failure may be corrected')
|
||||
}
|
||||
let source = jassubConsumerSource(mode)
|
||||
if (plugin.label !== 'candidate' && mode === 'node10-commonjs')
|
||||
source = source.replace(/(['"])artplayer-plugin-jassub\/legacy\1/g, `'artplayer-plugin-jassub/types/artplayer-plugin-jassub'`)
|
||||
entry.diagnostics = compile(source)
|
||||
assert.deepEqual(entry.diagnostics, [], `${plugin.label} ${compiler.version} ${mode}: explicit historical namespace consumer`)
|
||||
entry.invalid = compile(`${source}\n${jassubInvalidStatements.join('\n')}\n`)
|
||||
assert.equal(jassubInvalidStatements.length, 7, 'Retain all seven historical negative consumers')
|
||||
assert.equal(entry.invalid.length, jassubInvalidStatements.length)
|
||||
assert.deepEqual(entry.invalid.map(item => item.line), jassubInvalidStatements.map((_, index) => source.split('\n').length + 1 + index), 'Every old-entry negative statement must fail at its exact line')
|
||||
if (plugin.label === 'published-1.0.0')
|
||||
historical.get(key).invalid = shape(entry.invalid)
|
||||
else assert.deepEqual(shape(entry.invalid), historical.get(key).invalid, 'Both published packages and candidate preserve exact invalid diagnostic codes and lines')
|
||||
if (plugin.label === 'candidate') {
|
||||
const runtimeSource = jassubRuntimeSource(mode)
|
||||
const diagnostics = compile(runtimeSource)
|
||||
const runtimeDeclarations = declarations
|
||||
assert.deepEqual(diagnostics, [], `Accurate runtime consumer: ${compiler.version} ${mode}`)
|
||||
const lines = runtimeSource.split('\n')
|
||||
const negative = []
|
||||
for (let index = 0; index < lines.length; index++) {
|
||||
if (!/^[ \t]*\/\/ @ts-expect-error/.test(lines[index]))
|
||||
continue
|
||||
assert(lines[index + 1]?.trim() && !lines[index + 1].trim().startsWith('//'), 'Each runtime directive must precede one negative statement')
|
||||
const invalid = compile(lines.map((line, offset) => offset === index ? '' : line).join('\n'))
|
||||
assert(invalid.length > 0 && invalid.every(item => item.file === path.basename(filename) && item.line === index + 2), `Runtime negative statement ${index + 2} must independently fail on its own line`)
|
||||
negative.push({ line: index + 2, diagnostics: invalid })
|
||||
}
|
||||
assert.equal(negative.length, 14, 'Retain all fourteen runtime negative consumers')
|
||||
entry.runtime = { diagnostics, negative, declarations: runtimeDeclarations, sourceSha256: hash(runtimeSource) }
|
||||
if ((compiler === ts && mode === 'nodenext-cjs') || (compiler === compat && mode === 'node10-commonjs')) {
|
||||
const commonjsSource = jassubRuntimeSource(`${mode}-no-interop`)
|
||||
assert(commonjsSource.includes('import runtime = require(\'artplayer-plugin-jassub/runtime\')'), 'Exercise the promised CommonJS import assignment')
|
||||
const commonjsDiagnostics = compile(commonjsSource, false)
|
||||
entry.runtime.commonjs = { esModuleInterop: false, diagnostics: commonjsDiagnostics, declarations, sourceSha256: hash(commonjsSource) }
|
||||
writeJson(path.join(output, 'matrix-progress.json'), matrix)
|
||||
assert.deepEqual(commonjsDiagnostics, [], `Runtime import=require without interop: ${compiler.version} ${mode}`)
|
||||
}
|
||||
}
|
||||
writeJson(path.join(output, 'matrix-progress.json'), matrix)
|
||||
}
|
||||
}
|
||||
finally { removeConsumer(consumer) }
|
||||
}
|
||||
assert.deepEqual(fixtureHashes(), fixtures, 'Consumer fixtures changed during the installed matrix; rerun with final inputs')
|
||||
writeJson(path.join(output, 'report.json'), { task: 'PKG-JASSUB-04', node: process.version, fixtures, rootDeclarations, packages, published: plugins.slice(0, 2).map(plugin => ({ label: plugin.label, archive: plugin.archive, sha256: hash(fs.readFileSync(plugin.archive)) })), matrix, scope: 'Actual npm 1.0.0/1.1.0 and Yarn-packed candidate/core installed outside workspace; offline installation and forced frozen reinstall with unchanged lock, strictly verified installed member hashes and non-link checks; root declaration text unchanged after CRLF normalization with both raw hashes recorded, exact old diagnostics, conditional accurate runtime declarations and Node lazy factory/legacy identity. Registrars are never invoked in Node; this is not browser Worker/WASM/font rendering or publication acceptance.' })
|
||||
console.log(`JASSUB isolated installed consumers passed ${matrix.length} compiler modes: ${output}`)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
if (evidenceOutput)
|
||||
writeJson(path.join(evidenceOutput, 'failure.json'), { message: error.message, stack: error.stack })
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Repository baseline 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 { jassubConsumerSource, jassubInvalidStatements, jassubRuntimeSource } from './jassub-consumer.mjs'
|
||||
import { verifyJassubContract } from './jassub-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('JASSUB keeps both actual published root declarations without changing historical extraction or replacement types', async () => {
|
||||
const contract = await verifyJassubContract()
|
||||
const candidate = fs.readFileSync('packages/artplayer-plugin-jassub/types/artplayer-plugin-jassub.d.ts', 'utf8').replaceAll('\r\n', '\n')
|
||||
for (const archive of contract.archives.values())
|
||||
assert.equal(candidate, readMember(archive, 'package/types/artplayer-plugin-jassub.d.ts').toString().replaceAll('\r\n', '\n'))
|
||||
for (const [compiler, mode] of modes) {
|
||||
const source = jassubConsumerSource(mode)
|
||||
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${compiler.version} ${mode}`)
|
||||
const errors = checkConsumer(compiler, mode, `${source}\n${jassubInvalidStatements.join('\n')}\n`)
|
||||
assert.deepEqual(errors.map(item => item.line), jassubInvalidStatements.map((_, index) => source.split('\n').length + 1 + index))
|
||||
}
|
||||
})
|
||||
|
||||
test('JASSUB runtime entry checks optional options, sync instances, worker messages, callbacks and invalid consumers', () => {
|
||||
for (const [compiler, mode] of modes) {
|
||||
const source = jassubRuntimeSource(mode)
|
||||
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${compiler.version} ${mode}`)
|
||||
const invalid = checkConsumer(compiler, mode, source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, ''))
|
||||
const expected = []
|
||||
let line = 1
|
||||
for (const text of source.split('\n')) {
|
||||
if (text.startsWith('// @ts-expect-error'))
|
||||
expected.push(line)
|
||||
else line++
|
||||
}
|
||||
assert.equal(expected.length, 14)
|
||||
assert.deepEqual(invalid.map(item => item.line), expected, `${compiler.version} ${mode}: each negative statement must fail independently`)
|
||||
}
|
||||
assert.deepEqual(checkConsumer(ts, 'nodenext-cjs', `import root = require('artplayer-plugin-jassub'); import legacy = require('artplayer-plugin-jassub/legacy'); import runtime = require('artplayer-plugin-jassub/runtime');
|
||||
const option: root.JassubOption = {workerUrl: '/worker.js', wasmUrl: '/worker.wasm', modernWasmUrl: '/modern.wasm'};
|
||||
root.default(option); legacy.default(option); runtime(); const value: runtime.RuntimeOption = {}; runtime(value);`), [])
|
||||
})
|
||||
|
||||
test('JASSUB generated editor declaration preserves the old global API and is valid in both compilers', async () => {
|
||||
const source = fs.readFileSync('packages/artplayer-plugin-jassub/types/artplayer-plugin-jassub.d.ts', 'utf8')
|
||||
const generated = generatePluginEditorDeclaration(source, 'artplayerPluginJassub')
|
||||
const file = 'docs/assets/ts/artplayer-plugin-jassub.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: artplayerPluginJassub.JassubOption = {workerUrl: '/worker.js', wasmUrl: '/worker.wasm', modernWasmUrl: '/modern.wasm'};
|
||||
const result = artplayerPluginJassub(option)(art);
|
||||
const promise: Promise<void> = result.instance.resize(true, 640, 360);
|
||||
const replacement: typeof artplayerPluginJassub = (_option) => (_art) => result;
|
||||
// @ts-expect-error Keep the historical required options argument.
|
||||
artplayerPluginJassub();
|
||||
// @ts-expect-error Keep the historical required resource URLs.
|
||||
artplayerPluginJassub({});
|
||||
// @ts-expect-error Global editor uses the historical force-first resize signature.
|
||||
result.instance.resize(640);`
|
||||
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, 3)
|
||||
const oldEditor = `${source.replace(/^import.*$/gim, '')}\nexport = artplayerPluginJassub;\nexport as namespace artplayerPluginJassub;\n`
|
||||
assert(checkPluginEditorDeclaration(oldEditor, core, '', compiler).some(item => item.code === 2309), 'Reproduce the former conflicting export assignment before claiming the editor fix')
|
||||
}
|
||||
})
|
||||
+6
-3
@@ -2477,11 +2477,14 @@
|
||||
"ENG-06",
|
||||
"CORE-07"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "JASSUB option/instance 的兼容类型包装,vendor JS 例外记录",
|
||||
"acceptance": "严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-14-PKG-JASSUB-04-types.md",
|
||||
"baselines/jassub-types-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-JASSUB-05",
|
||||
@@ -2531,7 +2534,7 @@
|
||||
],
|
||||
"status": "todo",
|
||||
"risk": "H",
|
||||
"deliverable": "独立记录 vendor 适配差异;幂等销毁、跨容器 setVideo、构造回滚、逐帧归属和倍率/能力边界",
|
||||
"deliverable": "独立记录 vendor 适配差异;幂等销毁、跨容器 setVideo、构造回滚、逐帧归属和倍率/能力边界;查询 timeout/Worker error 的缺失数据回调及每请求监听器/定时器清理(JASSUB-QUERY-01)",
|
||||
"acceptance": "保持公开真实 instance、同步方法与资源路径;保留原 vendor 来源指纹并记录每项实际补丁;候选修复正例和真实字幕/失败/时钟验证通过,未证实设备不得冒充通过",
|
||||
"evidence": []
|
||||
},
|
||||
|
||||
@@ -1258,7 +1258,7 @@
|
||||
"PKG-JASSUB-05"
|
||||
],
|
||||
"sourceFiles": [
|
||||
"packages/artplayer-plugin-jassub/src/index.js",
|
||||
"packages/artplayer-plugin-jassub/src/index.ts",
|
||||
"docs/assets/example/jassub.js"
|
||||
],
|
||||
"contract": "Preserve options pass-through, result.instance and worker/wasm/font URLs. Wrapper, worker and WASM must be a verified compatible set.",
|
||||
|
||||
@@ -35,3 +35,8 @@ Danmuku的PKG-DANMUKU-06保留实际npm 5.3.0根声明的完整类型形状与
|
||||
points、静态icons和回调receiver;根/legacy/runtime复用原分发实现,不添加不存在
|
||||
的factory.default。旧入口用户不需要因本次类型迁移修改代码。切换/runtime是可选
|
||||
的准确类型入口,历史声明与运行时的区别见包README;这里没有新增旧类型破坏授权。
|
||||
|
||||
JASSUB 的 PKG-JASSUB-04 保留实际 npm 1.0.0/1.1.0 相同的根声明(忽略换行),
|
||||
不改变必填资源 URL、旧 resize 参数顺序、Promise 返回声明和扩展索引。新增
|
||||
/runtime 描述真实可选参数、同步方法及 Worker 数据,并复用同一运行产物。
|
||||
这项实施没有新增旧类型破坏,不声称用户另行批准过 JASSUB 专属类型冲突。
|
||||
Reference in new issue
Block a user