refactor(danmuku): [PKG-DANMUKU-06] migrate runtime and preserve public types

This commit is contained in:
Harvey Zhao committed 2026-09-13 22:06:00 +08:00
1 parent abc8008da7
commit 24b883d862
64 files changed
+14020 -633

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,99 @@
# PKG-DANMUKU-06 自有 TypeScript 与兼容声明
本步将前面已拆分的20个自有运行模块迁移为严格TypeScript,明确输入、队列、
Worker协议、UI与生命周期边界。任务完成状态以tasks.json为准。
## 实现与兼容边界
- 入口保持同步插件门面;Danmuku拥有配置与队列,scheduler负责时钟与有效操作,
renderer负责节点,WorkerClient负责请求身份与终止,worker只计算轨道。
input、bilibili及parser保持请求/解析/取消分层;设置和热图继续使用05/10的模块边界。
- 新增内部types、worker-types、parser-types、setting-types及Worker资源声明。
公开运行数据由types/runtime-shared.d.ts单一维护,内部通过type-only转发使用;
DOM宿主与Worker协议留在src。不存在新的运行时模块环或消费者依赖。
- 类字段使用declare,不改变原初始化顺序或新增undefined自有属性。保留同步数组
加载前缀、Promise/owner返回身份、回调receiver、默认参数、事件与原DOM属性写入。
取消符号明确区分异步结果;没有为了通过类型检查改变轨道算法。
- 受控断言用于旧核心constructor声明、已初始化的设置节点、已分配的活动弹幕节点、
DOM WebIDL的原始数值/null赋值,以及历史无效margin字符串回退。它们不替代输入
验证,不引入全局any。旧postMessage空对象默认值仍保留,未知协议不产生回复。
- 分包tsconfig禁用allowJs。ES2021.String仅描述既有parser.replaceAll,不新增该
运行时要求;现代es2020与legacy es2015构建目标不变。本包没有自有遗留JS或vendor。
模块地图、生命周期与维护指引见包内ARCHITECTURE.md。
## 公开类型与消费者
实际npm 5.3.0根声明完整保留,换行归一后SHA256为
`006c3e2d22425b936d6d09bf5138b0ae13c4cfffd0a571d03722c17137dbe99c`。
旧根/legacy的参数、返回提取和普通替代工厂关系不变;没有给工厂增加必填default。
新增可选/runtime路径指向相同分发文件,只提供准确声明:
- 工厂参数对象仍必传,但对象内字段可选。保留弹幕扩展字段;输入可为数组、URL、
Promise或返回数组/Promise的函数,config可以传部分字段。
- emit/load返回Promise<Owner>,同步命令返回Owner,Owner与注册门面分别描述;
门面的option/isHide/isStop只读getter不会使内部配置对象变为深只读。
- filter接收填默认值后的数据,beforeVisible接收队列项;两者按truthy判断,
beforeEmit严格比较true。回调this为实际NormalizedOption。
- mount需要有效目标。icons描述真实可写属性;Slider与margin元组、heatmap坐标和
配置保持实际格式。points配置目前只保存,自定义绘图由points事件触发,保留内层
元组mutation。EventMap提供明确payload,不全局改写旧核心事件声明。
- 旧根的同步emit结果和对象points属于历史声明错误,切换/runtime后需按真实行为
使用await及元组;继续使用旧入口无需因本次迁移修改代码。实际运行并未被旧声明
所描述的行为替换,具体示例见README及type-compatibility-policy.md。
四项类型测试覆盖真实声明归档、正反例、实现赋值和语义编辑器生成。安装测试在
仓库外实际Yarn pack/install并离线冻结重装,逐成员核对字节,检查CJS/ESM身份。
12个编译器/解析模式组合包括TS5.9.3与4.3.5;候选每个适用模式有4个root和14个
runtime负例,逐个去掉expect-error确认相应错误实际出现。
旧根在NodeNext ESM默认namespace方式的7个历史诊断与候选逐代码/行号比较,
显式namespace适配通过;保留失败事实,不把它包装成全部旧TS用法均无错误。
build:ts把Danmuku纳入语义声明生成,更新docs编辑器资产;不手改生成声明。
.npmignore排除实现tsconfig,公开类型仍随包分发。新增test:danmuku-types及
test:danmuku-types-package脚本,前者被既有test:baseline自动发现。本步无新依赖,
唯一yarn.lock不变,版本递增仍由统一版本任务执行。
## 测试与证据
最终机器证据见[验收报告](../baselines/danmuku-types-validation.json)。
完整源码250项单测通过,正式main和legacy各164项联合测试通过;严格类型检查覆盖
全仓390个生产TS文件。源码/main/legacy最终各156项真实浏览器通过,共468项,
每轮2并发、零重试、零跳过;覆盖实际发布5.4.0与候选5.4.1核心以及Chromium、
Firefox和Windows WebKit。正常构建、类型编辑器生成、目标lint、冻结安装及严格
Node24.21.0/Yarn1.22.22工具链检查通过;构建产物与docs复制逐字节相同。
源码单测、正式main/legacy联合测试包含共同内部模块与历史对照,不能把每一项都
计为独立产物测试。Worker加载器会执行选中产物内嵌Blob或data URL的实际字节;
只有源码受控模式才单独编译worker.ts。冻结的历史helper没有修改。
内置浏览器用正常yarn dev打开8082现有danmuku示例,本地视频和XML实际加载。
Chrome/152.0.0.0,视频播放至11.711807秒且解码宽640;发送
`TS migration browser check`后输入清空;暂停在43.744183秒,再次读取保持同值。
真实弹幕可见,页面warn/error记录为空。原生Browser.getVersion不支持,改用受支持
CDP只读navigator.userAgent取版本;之前失效tab的CDP焦点超时不作为页面失败。
这只是开发示例smoke,不替代发布包、Safari真机或07的持续负载验收。
## 失败处置与未完成范围
- 第一版实际pack发现实现tsconfig泄漏,修正.npmignore后重做完整隔离安装。
- 实现赋值fixture最初放在根test/types,导致根配置缺少分包Worker资源和既有
ES2021.String声明;移至refactor/fixtures/implementation,并用分包配置检查。
负例诊断行计算也修正了跨空白行匹配;保留失败日志,不弱化类型断言。
- 最终独立复查更正架构文档仍指向旧位置的实现fixture,并对最终包内容重新执行
隔离打包安装;不使用修正文档前的归档作为最终候选证据。
- 产物Worker测试首次只识别Blob,实际正式构建使用百分号编码data URL。增加精确
data URL解码后执行内嵌算法,保持未知来源断言,不用当前源码冒充产物。
- 一轮浏览器复测提前启动下一轮,共享结果目录被覆盖,导致最后两项trace清理失败;
下一轮同时遇到端口占用。保留日志,改为一个串行命令,上一进程结束后归档再继续。
- 随后source矩阵154通过、2个WebKit uniform场景超时。trace显示published首次
Play客户端13.17秒而浏览器动作仅0.39秒;candidate启动/建页/导航合计16.23秒,
都在插件安装前。16k加载分别0.995秒与2.96秒;published超时后几何断言实际通过,
但整项仍记失败。证据支持宿主或自动化延迟,不能确认更具体的系统原因。
其他重型检查结束后,原断言/20秒超时不变,单worker WebKit诊断6项通过;完整
最终矩阵单独归档,不关闭历史负载疑点,也不以重跑抹去首次失败。
07继续承担持续负载、渲染/资源对比及Mask稳定依赖,08承担完整组合,09承担分发
与文档最终验收。DANMUKU-LOAD-01仍保持待验证;本步不声称npm候选或全部重构完成。
三轮发布复盘、设备/外部SDK证据、全包独立major以及远端CI仍按原计划。
回退:单独revert本任务提交,恢复05/10的JS实现及原类型分发;#958独立修复不回退。
只提交本任务实现、测试、文档和状态,不推送或发布。
@@ -0,0 +1,27 @@
import type Artplayer from 'artplayer'
import type { Danmu, Option, Result } from 'artplayer-plugin-danmuku'
import danmuku from 'artplayer-plugin-danmuku'
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false
type Assert<T extends true> = T
type Factory = (option: Option) => (art: Artplayer) => Result
type SameArguments = Assert<Equal<Parameters<typeof danmuku>, [option: Option]>>
type SameResult = Assert<Equal<ReturnType<ReturnType<typeof danmuku>>, Result>>
type SameEmit = Assert<Equal<ReturnType<Result['emit']>, Result>>
type SameLoad = Assert<Equal<ReturnType<Result['load']>, Promise<Result>>>
type SamePoints = Assert<Equal<Option['points'], { time: number, value: number }[] | undefined>>
type SameDanmuKeys = Assert<Equal<keyof Danmu, 'text' | 'mode' | 'color' | 'time' | 'border' | 'style'>>
declare const art: Artplayer
declare const result: Result
const replacement: Factory = (_option: Option) => (_art: Artplayer) => result
const toOld: Factory = danmuku
const fromOld: typeof danmuku = replacement
const option: Option = { danmuku: [], points: [{ time: 1, value: 2 }], margin: [0, '25%'], beforeEmit: async () => true }
const plugin: Result = danmuku(option)(art)
const synchronousOldEmit: Result = plugin.emit({ text: 'old declaration', mode: 1 })
const loaded: Promise<Result> = plugin.load(Promise.resolve([{ text: 'reload' }]))
const configured: Result = plugin.config(option)
const mounted: void = plugin.mount('#external')
void [toOld, fromOld, synchronousOldEmit, loaded, configured, mounted]
export type { SameArguments, SameDanmuKeys, SameEmit, SameLoad, SamePoints, SameResult }
@@ -0,0 +1,6 @@
import type { RuntimeFactory } from 'artplayer-plugin-danmuku/runtime'
import factory from '../../../packages/artplayer-plugin-danmuku/src'
// Compile with the package configuration: the implementation imports Worker assets.
const implementation: RuntimeFactory = factory
export { implementation }
+4 -3
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 229 项,范围 22 个包及工作区/示例。
状态:todo 69 / doing 15 / blocked 0 / done 145 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 68 / doing 15 / blocked 0 / done 146 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -314,8 +314,8 @@
| PKG-DANMUKU-03 | artplayer-plugin-danmuku<br>整理加载、解析与配置 | PKG-DANMUKU-02, CORE-08 | bilibili/input/parser/config 的分层与取消 | 旧格式、回调、过滤、追加/替换语义保持 | M | done |
| PKG-DANMUKU-04 | artplayer-plugin-danmuku<br>整理时钟、队列与轨道调度 | PKG-DANMUKU-03, CORE-10 | danmuku 调度器和确定性时钟测试 | seek/暂停/倍率/长时间运行无顺序和碰撞回归 | H | done |
| PKG-DANMUKU-05 | artplayer-plugin-danmuku<br>整理 DOM 渲染、设置、热力图与 worker | PKG-DANMUKU-04, CORE-14, CORE-18 | renderer/setting/heatmap/worker 职责及资源归属 | mount/icons/设置和 worker 协议保持,销毁无后台工作 | H | done |
| PKG-DANMUKU-06 | artplayer-plugin-danmuku<br>迁移 TS 与公开声明 | PKG-DANMUKU-05, ENG-04, ENG-06 | 自有模块、worker 消息、option/item/result 类型 | 旧参数、扩展字段和事件消费通过,vendored 边界清楚 | H | doing |
| PKG-DANMUKU-07 | artplayer-plugin-danmuku<br>弹幕能力稳定性验收 | PKG-DANMUKU-06, PKG-DANMUKU-10 | 负载/渲染/资源对比及 mask 可依赖的稳定边界 | 在试点核心中旧插件 API 与关键帧路径通过 | H | todo |
| PKG-DANMUKU-06 | artplayer-plugin-danmuku<br>迁移 TS 与公开声明 | PKG-DANMUKU-05, ENG-04, ENG-06 | 自有模块、worker 消息、option/item/result 类型 | 旧参数、扩展字段和事件消费通过,vendored 边界清楚 | H | done |
| PKG-DANMUKU-07 | artplayer-plugin-danmuku<br>弹幕能力稳定性验收 | PKG-DANMUKU-06, PKG-DANMUKU-10 | 负载/渲染/资源对比及 mask 可依赖的稳定边界 | 在试点核心中旧插件 API 与关键帧路径通过 | H | doing |
| PKG-DANMUKU-08 | artplayer-plugin-danmuku<br>完成新旧核心与组合验收 | PKG-DANMUKU-07, CORE-22 | 最终核心/旧核心、mask/fullscreen/PiP 组合报告 | 性能无未解释退化,反复装卸无累计资源 | H | todo |
| PKG-DANMUKU-09 | artplayer-plugin-danmuku<br>完成分发、示例与文档 | PKG-DANMUKU-08, ENG-07 | danmuku.js、README、声明、worker 与产物 | tarball 和静态 icons 等旧调用通过,有独立回退版本 | H | todo |
| PKG-DANMUKU-10 | artplayer-plugin-danmuku<br>修复 issue958 密集热力图过高与曲线裁平 | PKG-DANMUKU-05 | 自动密度缩放与曲线边界、历史自定义坐标兼容及真实浏览器红绿证据 | 16000条三种分布在两核心三引擎和source/main/legacy不遮挡;显式轴和自定义points保持旧解释,有单独commit | M | done |
@@ -559,6 +559,7 @@
- PKG-DANMUKU-03: [记录](changes/2026-09-13-PKG-DANMUKU-03-input.md) [记录](baselines/danmuku-input-validation.json)
- PKG-DANMUKU-04: [记录](changes/2026-09-13-PKG-DANMUKU-04-scheduler.md) [记录](baselines/danmuku-scheduler-validation.json)
- PKG-DANMUKU-05: [记录](changes/2026-09-13-PKG-DANMUKU-05-resources.md) [记录](baselines/danmuku-resources-validation.json)
- PKG-DANMUKU-06: [记录](changes/2026-09-13-PKG-DANMUKU-06-types.md) [记录](baselines/danmuku-types-validation.json)
- PKG-CANVAS-01: [记录](changes/2026-09-12-PKG-CANVAS-01-contract.md) [记录](baselines/canvas-contract.md) [记录](baselines/canvas-release.json) [记录](canvas-validation.md) [记录](baselines/canvas-contract-validation.json)
- PKG-CANVAS-02: [记录](changes/2026-09-12-PKG-CANVAS-02-tests.md) [记录](baselines/canvas-behavior-validation.json) [记录](canvas-validation.md)
- PKG-CANVAS-03: [记录](changes/2026-09-12-PKG-CANVAS-03-lifecycle.md) [记录](baselines/canvas-lifecycle-validation.json) [记录](canvas-validation.md)
+15
View File
@@ -1,5 +1,20 @@
# 进度与证据
## PKG-DANMUKU-06 自有 TS 与公开声明完成
20个自有运行模块完成严格TS,新增内部状态/Worker/parser/UI类型边界;公开运行
数据单一维护。实际npm5.3.0根声明完整保留,新增同实现/runtime准确描述Promise、
owner、getter、回调和数据;语义docs编辑器声明同步生成。包架构和维护命令已更新。
完整源码250项、main/legacy各164项联合单测、4项类型检查、12组仓库外安装消费者
通过;全仓390生产TS严格检查、冻结Yarn安装、工具链、构建和目标lint通过。
三格式/两核心/三引擎最终468项浏览器通过,原断言/超时不变、无重试/跳过;内置
Chrome152完成现有docs示例实际播放、发送与暂停。Worker data URL测试加载器、
先前报告目录冲突和WebKit超时均保留原因与失败证据,未把重跑等同历史从未失败。
关闭DANMUKU-TYPE-01/02;LOAD-01与稳定性/组合/分发及发布复盘继续保留。
见[变更](changes/2026-09-13-PKG-DANMUKU-06-types.md)和[机器证据](baselines/danmuku-types-validation.json)。
229项:146 done、15 doing、68 todo;07进入负载、渲染与资源稳定性验收。
本地独立任务提交,不推送或发布。
## PKG-DANMUKU-10:issue #958 密集热力图修复
用户新增的16000条弹幕热图过高/裁平已独立复现并修复。自动密度拟合到底部四分之一,
+2 -2
View File
@@ -227,11 +227,11 @@
| MASK-BACKEND-01 | open / 已复现 | Mask TensorFlow backend fallback does not establish MediaPipe fallback and failed initialization keeps scheduling | PKG-MASK-02, PKG-MASK-03, PKG-MASK-05 |
| MASK-DOM-01 | open / 已复现 | Mask registration and frame processing assume Danmuku DOM, readable frames and a 2D context | PKG-MASK-02, PKG-MASK-03, PKG-MASK-05 |
| MASK-NOTICE-01 | open / 源码/产物事实 | Published Mask bundles lack a separate third-party notice despite bundled SDK code | PKG-MASK-06 |
| DANMUKU-TYPE-01 | open / 已复现 | Danmuku historical declared results, optional mount and initial Promise input disagree with runtime | PKG-DANMUKU-03, PKG-DANMUKU-06 |
| DANMUKU-TYPE-01 | resolved / 已复现 | Danmuku historical declared results, optional mount and initial Promise input disagree with runtime | PKG-DANMUKU-03, PKG-DANMUKU-06 |
| DANMUKU-INPUT-01 | resolved / 已复现 | Danmuku replaces time zero with current time plus 0.5 and mutates supplied input objects | PKG-DANMUKU-02, PKG-DANMUKU-03 |
| DANMUKU-CONFIG-01 | resolved / 已复现 | Danmuku JSON configuration comparison ignores a replacement filter function | PKG-DANMUKU-02, PKG-DANMUKU-03 |
| DANMUKU-CLEANUP-01 | resolved / 已复现 | Danmuku removes a different resize callback and retains the settings lock timer on destroy | PKG-DANMUKU-02, PKG-DANMUKU-05 |
| DANMUKU-TYPE-02 | open / 源码/产物事实 | Danmuku id, icons, Slider steps, heatmap points and partial configuration drift from declarations | PKG-DANMUKU-05, PKG-DANMUKU-06 |
| DANMUKU-TYPE-02 | resolved / 源码/产物事实 | Danmuku id, icons, Slider steps, heatmap points and partial configuration drift from declarations | PKG-DANMUKU-05, PKG-DANMUKU-06 |
| DANMUKU-ASYNC-01 | resolved / 已复现 | Danmuku asynchronous input, worker replies and pending visibility have unverified failure and cancellation paths | PKG-DANMUKU-02, PKG-DANMUKU-03, PKG-DANMUKU-04, PKG-DANMUKU-05 |
| DANMUKU-HEATMAP-01 | resolved / 已复现 | Danmuku heatmap sampling can become zero and points/instance ownership need browser validation | PKG-DANMUKU-02, PKG-DANMUKU-05 |
| DANMUKU-SOURCE-01 | open / 已复现 | Danmuku registry Git associations differ from releases and static icon provenance remains incomplete | PKG-DANMUKU-09 |
+20 -6
View File
@@ -5023,7 +5023,7 @@
"id": "DANMUKU-TYPE-01",
"title": "Danmuku historical declared results, optional mount and initial Promise input disagree with runtime",
"confirmation": "reproduced",
"status": "open",
"status": "resolved",
"owners": [
"PKG-DANMUKU-03",
"PKG-DANMUKU-06"
@@ -5033,10 +5033,17 @@
"refactor/baselines/danmuku-contract-validation.json",
"refactor/baselines/danmuku-release.json",
"refactor/changes/2026-09-13-PKG-DANMUKU-03-input.md",
"refactor/baselines/danmuku-input-validation.json"
"refactor/baselines/danmuku-input-validation.json",
"refactor/changes/2026-09-13-PKG-DANMUKU-06-types.md",
"refactor/baselines/danmuku-types-validation.json"
],
"compatibleResolution": "Preserve latest published complete root type contracts and actual legal JS return identities; expose accurate types separately.",
"closureCriteria": "Installed consumers verify latest root assignments and precise async/getter/mount contracts; initial Promise mismatch has an explicit tested resolution."
"closureCriteria": "Installed consumers verify latest root assignments and precise async/getter/mount contracts; initial Promise mismatch has an explicit tested resolution.",
"resolutionEvidence": [
"refactor/changes/2026-09-13-PKG-DANMUKU-06-types.md",
"refactor/baselines/danmuku-types-validation.json"
],
"resolutionRationale": "Preserved actual npm 5.3.0 complete root/legacy declaration and installed consumer behavior; optional runtime entry describes tested async owner, getter, mount, data, icon and callback contracts. Verified actual implementation assignability and packed/offline-installed current and historical compiler consumers."
},
{
"id": "DANMUKU-INPUT-01",
@@ -5127,7 +5134,7 @@
"id": "DANMUKU-TYPE-02",
"title": "Danmuku id, icons, Slider steps, heatmap points and partial configuration drift from declarations",
"confirmation": "source-observed",
"status": "open",
"status": "resolved",
"owners": [
"PKG-DANMUKU-05",
"PKG-DANMUKU-06"
@@ -5135,10 +5142,17 @@
"evidence": [
"refactor/baselines/danmuku-contract.md",
"refactor/baselines/danmuku-contract-validation.json",
"refactor/baselines/danmuku-release.json"
"refactor/baselines/danmuku-release.json",
"refactor/changes/2026-09-13-PKG-DANMUKU-06-types.md",
"refactor/baselines/danmuku-types-validation.json"
],
"compatibleResolution": "Validate actual accepted data and static properties, retain root compatibility and document effective fields.",
"closureCriteria": "Positive and negative installed consumers and source behavior tests cover these fields without widening away old factory contracts."
"closureCriteria": "Positive and negative installed consumers and source behavior tests cover these fields without widening away old factory contracts.",
"resolutionEvidence": [
"refactor/changes/2026-09-13-PKG-DANMUKU-06-types.md",
"refactor/baselines/danmuku-types-validation.json"
],
"resolutionRationale": "Preserved actual npm 5.3.0 complete root/legacy declaration and installed consumer behavior; optional runtime entry describes tested async owner, getter, mount, data, icon and callback contracts. Verified actual implementation assignability and packed/offline-installed current and historical compiler consumers."
},
{
"id": "DANMUKU-ASYNC-01",
+140
View File
@@ -0,0 +1,140 @@
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 { verifyDanmukuContract } from './danmuku-contract.mjs'
import { hash, readMember } from './releases.mjs'
const publishedFixture = 'refactor/fixtures/consumers/danmuku-published.ts'
const rootFixture = 'test/types/danmuku-root.ts'
const runtimeFixture = 'test/types/danmuku-runtime.ts'
async function main() {
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use yarn test:danmuku-types-package')
const yarn = process.env.npm_execpath
assert(yarn && fs.existsSync(yarn))
const output = fs.mkdtempSync(path.join(workspace, 'refactor/.cache/danmuku-package-types-'))
console.log(`Danmuku isolated package evidence: ${output}`)
const contract = await verifyDanmukuContract()
assert.equal(contract.baseline.release.version, '5.3.0')
const fixtures = Object.fromEntries([publishedFixture, rootFixture, runtimeFixture].map(file => [file, fs.readFileSync(path.join(workspace, file), 'utf8')]))
const packages = []
for (const name of ['artplayer', 'artplayer-plugin-danmuku']) {
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 published = { ...contract.baseline.release, archive: contract.archives.get('5.3.0'), label: 'published-5.3.0' }
const declaration = 'package/types/artplayer-plugin-danmuku.d.ts'
assert.equal(readMember(packages[1].archive, declaration).toString().replaceAll('\r\n', '\n'), readMember(published.archive, declaration).toString().replaceAll('\r\n', '\n'), 'Preserve the actual npm 5.3.0 root declaration')
const matrix = []
const historical = new Map()
for (const plugin of [published, { ...packages[1], label: 'candidate' }]) {
const consumer = consumerDirectory()
try {
writeJson(path.join(consumer, 'package.json'), { name: 'danmuku-isolated-consumer', private: true, dependencies: Object.fromEntries([packages[0], 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 [packages[0], 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, `const assert = require('node:assert/strict');
const names = ${JSON.stringify(plugin.label === 'candidate' ? ['artplayer-plugin-danmuku', 'artplayer-plugin-danmuku/legacy', 'artplayer-plugin-danmuku/runtime'] : ['artplayer-plugin-danmuku', 'artplayer-plugin-danmuku/legacy'])};
for (const name of names) { const factory = require(name); assert.equal(typeof factory, 'function'); assert.equal(typeof factory({danmuku: []}), 'function'); assert.equal(typeof factory.icons, 'object'); }
${plugin.label === 'candidate' ? 'assert.equal(require(\'artplayer-plugin-danmuku/runtime\'), require(\'artplayer-plugin-danmuku\'));' : ''}
Promise.all(names.map(name => import(name))).then(modules => { for (const value of modules) assert.equal(typeof value.default, 'function'); assert.equal(modules[1].default, require(names[1])); ${plugin.label === 'candidate' ? 'assert.equal(modules[2].default, modules[0].default);' : ''} }).catch(error => { console.error(error); process.exitCode = 1; });`)
fs.writeFileSync(path.join(output, `${plugin.label}-runtime.log`), run([runtimeFile], consumer))
const modes = [[ts, 'node10-commonjs'], [compat, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm']]
if (plugin.label === 'candidate')
modes.push([ts, 'node10-commonjs-no-interop'], [compat, 'node10-commonjs-no-interop'])
for (const [compiler, mode] of modes) {
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: !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(source) {
fs.writeFileSync(filename, source)
const program = compiler.createProgram([filename], 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, line: item.file && item.start !== undefined ? item.file.getLineAndCharacterOfPosition(item.start).line + 1 : null, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n').replaceAll(consumer.replaceAll('\\', '/'), '<consumer>') }))
}
function checkedFixture(source, label) {
const diagnostics = compile(source)
assert.deepEqual(diagnostics, [], `${label}: ${compiler.version} ${mode}`)
const lines = []
let line = 1
for (const text of source.split('\n')) {
if (/^[ \t]*\/\/ @ts-expect-error/.test(text))
lines.push(line)
else line++
}
assert.equal(lines.length, label === 'root/legacy' ? 4 : 14, `Keep all negative ${label} fixtures`)
const invalid = compile(source.replaceAll(/^[ \t]*\/\/ @ts-expect-error[^\n]*\n/gm, ''))
assert.deepEqual([...new Set(invalid.map(item => item.line))].sort((a, b) => a - b), lines, `Every ${label} negative statement must fail at its own line`)
return { diagnostics, invalid, negativeStatements: lines.length }
}
const direct = compile(fixtures[publishedFixture])
const key = `${compiler.version}:${mode}`
if (plugin.label === 'published-5.3.0') {
if (mode === 'nodenext-esm') {
assert(direct.length > 0, 'Preserve evidence of the existing NodeNext default namespace mismatch')
assert(direct.every(item => [2322, 2344, 2349].includes(item.code)), 'Only the established callable namespace mismatch is expected')
}
else {
assert.deepEqual(direct, [])
}
historical.set(key, direct.map(({ code, line }) => ({ code, line })))
}
else {
assert.deepEqual(direct.map(({ code, line }) => ({ code, line })), historical.get(key) ?? [], 'Candidate must preserve exact npm root diagnostic codes and positions')
}
const namespace = mode === 'nodenext-esm' ? compile(fixtures[publishedFixture].replace('import danmuku from \'artplayer-plugin-danmuku\'', 'import danmukuModule from \'artplayer-plugin-danmuku\'\nconst danmuku = danmukuModule.default')) : []
assert.deepEqual(namespace, [], 'Explicit published NodeNext namespace remains valid')
let root = null
let runtime = null
let commonjs = []
if (plugin.label === 'candidate') {
const rootSource = mode === 'nodenext-esm'
? fixtures[rootFixture].replace('import danmuku from \'artplayer-plugin-danmuku\'', 'import danmukuModule from \'artplayer-plugin-danmuku\'\nconst danmuku = danmukuModule.default').replace('import legacy from \'artplayer-plugin-danmuku/legacy\'', 'import legacyModule from \'artplayer-plugin-danmuku/legacy\'\nconst legacy = legacyModule.default')
: fixtures[rootFixture]
root = checkedFixture(rootSource, 'root/legacy')
const runtimeSource = mode.endsWith('-no-interop') ? fixtures[runtimeFixture].replace('import runtime from \'artplayer-plugin-danmuku/runtime\'', 'import runtime = require(\'artplayer-plugin-danmuku/runtime\')') : fixtures[runtimeFixture]
runtime = checkedFixture(runtimeSource, 'runtime')
if (mode !== 'nodenext-esm' && mode !== 'bundler-esm') {
commonjs = compile(`import root = require('artplayer-plugin-danmuku'); import legacy = require('artplayer-plugin-danmuku/legacy'); import runtime = require('artplayer-plugin-danmuku/runtime');\nconst replacement: typeof root.default = (_option) => (_art) => { throw new Error('consumer'); }; const other: typeof legacy.default = replacement; root.default({danmuku: []}); legacy.default({danmuku: []}); runtime({danmuku: []}); void other;`)
assert.deepEqual(commonjs, [], 'Installed require declarations preserve old factories and callable runtime')
}
}
matrix.push({ plugin: plugin.label, compiler: compiler.version, mode, direct, namespace, root, runtime, commonjs })
writeJson(path.join(output, 'matrix-progress.json'), matrix)
}
}
finally {
removeConsumer(consumer)
}
}
writeJson(path.join(output, 'report.json'), { suite: 'danmuku-isolated-package-types', introducedBy: 'PKG-DANMUKU-06', scope: 'Actual npm 5.3.0 and packed candidate outside workspace; offline and frozen reinstall, exact installed bytes, unchanged root declaration, full factory replacement and extraction. Old synchronous emit and object points remain root typing only. Runtime types are checked separately. NodeNext root namespace failures are compared by exact codes and line positions. This is not real playback or complete publication acceptance.', fixtures: Object.fromEntries(Object.entries(fixtures).map(([file, source]) => [file, hash(source)])), packages, published: { archive: published.archive, sha256: published.sha256 }, matrix })
console.log(`Danmuku installed types passed: ${matrix.length} cases; ${output}`)
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
+64
View File
@@ -0,0 +1,64 @@
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 uses Node.
import test from 'node:test'
import ts from 'typescript'
import compat from 'typescript-compat'
import { generateCoreEditorDeclaration } from '../../scripts/editor-types.mjs'
import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from '../../scripts/plugin-editor-types.mjs'
import { checkConsumer } from '../../scripts/typecheck.mjs'
import { ensureArchive, readMember } from './releases.mjs'
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
const read = file => fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n')
const declaration = 'packages/artplayer-plugin-danmuku/types/artplayer-plugin-danmuku.d.ts'
test('Danmuku root declaration retains the exact latest npm 5.3.0 type source', async () => {
const { release } = JSON.parse(read('refactor/baselines/danmuku-release.json'))
const archive = await ensureArchive(release)
assert.equal(read(declaration), readMember(archive, 'package/types/artplayer-plugin-danmuku.d.ts').toString().replaceAll('\r\n', '\n'))
})
test('Danmuku root and runtime consumers retain extraction, ownership and meaningful negative checks', () => {
for (const [compiler, mode] of modes) {
for (const fixture of ['danmuku-root', 'danmuku-runtime']) {
let source = read(`test/types/${fixture}.ts`)
if (fixture === 'danmuku-root' && mode === 'nodenext-esm') {
source = source.replace('import danmuku from \'artplayer-plugin-danmuku\'', 'import danmukuModule from \'artplayer-plugin-danmuku\'\nconst danmuku = danmukuModule.default')
.replace('import legacy from \'artplayer-plugin-danmuku/legacy\'', 'import legacyModule from \'artplayer-plugin-danmuku/legacy\'\nconst legacy = legacyModule.default')
}
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${fixture}: ${compiler.version}/${mode}`)
const expected = []
let line = 1
for (const text of source.split('\n')) {
if (/^\s*\/\/ @ts-expect-error/u.test(text))
expected.push(line)
else line++
}
assert.equal(expected.length, fixture === 'danmuku-root' ? 4 : 14)
const invalid = checkConsumer(compiler, mode, source.replaceAll(/^[\t ]*\/\/ @ts-expect-error[^\n]*\n/gmu, ''))
assert.deepEqual(invalid.map(item => item.line).sort((a, b) => a - b), expected, 'Every invalid consumer statement must fail at its own line')
}
}
})
test('Danmuku runtime declarations are assignable from the real strict TypeScript implementation', () => {
const filename = path.resolve('packages/artplayer-plugin-danmuku/tsconfig.json')
const config = ts.readConfigFile(filename, ts.sys.readFile)
assert.equal(config.error, undefined)
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(filename))
const program = ts.createProgram([...parsed.fileNames, path.resolve('refactor/fixtures/implementation/danmuku.ts')], parsed.options)
assert.deepEqual(ts.getPreEmitDiagnostics(program).map(item => ts.flattenDiagnosticMessageText(item.messageText, '\n')), [])
})
test('Danmuku editor declarations retain the callable global and exported historical names', () => {
const generated = generatePluginEditorDeclaration(read(declaration), 'artplayerPluginDanmuku')
const core = generateCoreEditorDeclaration()
const consumer = `const option: artplayerPluginDanmuku.Option = {danmuku: [], points: [{time: 1, value: 2}]};
const art = new Artplayer({container: '#player', url: '/video.mp4'});
const result: artplayerPluginDanmuku.Result = artplayerPluginDanmuku(option)(art);
const oldReturn: artplayerPluginDanmuku.Result = result.emit({text: 'legacy editor'}); void oldReturn;`
for (const compiler of [ts, compat])
assert.deepEqual(checkPluginEditorDeclaration(generated, core, consumer, compiler), [])
})
+6 -3
View File
@@ -3362,11 +3362,14 @@
"ENG-04",
"ENG-06"
],
"status": "doing",
"status": "done",
"risk": "H",
"deliverable": "自有模块、worker 消息、option/item/result 类型",
"acceptance": "旧参数、扩展字段和事件消费通过,vendored 边界清楚",
"evidence": []
"evidence": [
"changes/2026-09-13-PKG-DANMUKU-06-types.md",
"baselines/danmuku-types-validation.json"
]
},
{
"id": "PKG-DANMUKU-07",
@@ -3379,7 +3382,7 @@
"PKG-DANMUKU-06",
"PKG-DANMUKU-10"
],
"status": "todo",
"status": "doing",
"risk": "H",
"deliverable": "负载/渲染/资源对比及 mask 可依赖的稳定边界",
"acceptance": "在试点核心中旧插件 API 与关键帧路径通过",
+7
View File
@@ -28,3 +28,10 @@ Chromecast的PKG-CAST-04沿用此规则保留最新npm 1.1.0根声明,不引
破坏。1.0.0的export=与1.1.0的default export已有历史差异,分别保留测试证据;
准确异步注册、回调和查询类型通过新增/runtime提供。此处记录规则的应用,
不声称用户另行确认过Cast专属问题,也不扩大到未验证的运行时变化。
Danmuku的PKG-DANMUKU-06保留实际npm 5.3.0根声明的完整类型形状与字节(忽略
换行格式),包括历史同步emit结果、必填danmuku、对象形points和NodeNext ESM的
既有namespace限制。新增/runtime描述真实Promise/owner结果、可选配置字段、tuple
points、静态icons和回调receiver;根/legacy/runtime复用原分发实现,不添加不存在
的factory.default。旧入口用户不需要因本次类型迁移修改代码。切换/runtime是可选
的准确类型入口,历史声明与运行时的区别见包README;这里没有新增旧类型破坏授权。