feat(auto-thumbnail): [PKG-AUTO-THUMB-08] add compatible runtime type entry

This commit is contained in:
Harvey Zhao committed 2026-09-14 04:07:59 +08:00
1 parent 7be628a983
commit c4f5dcf17f
27 files changed
+2005 -40

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,59 @@
# PKG-AUTO-THUMB-08 兼容公开类型与异步入口
从尚未开始的 04 中拆出独立声明交付,依赖已完成的契约、测试与类型基础设施。
04 仍依赖 03 和本任务;03 的 WebKit 首两格像素等门槛没有移走或降低。
本项不等于整个 Auto Thumbnail 包完成。
## 实现与兼容边界
实际 npm 1.1.0 的根声明保持原字节(仅允许 checkout 换行格式不同),包括必填
options、同步 Result、Parameters/ReturnType、普通替代函数和模块 default 形状。
没有为修正 Promise 返回值给旧工厂追加重载、属性或改变类型提取。
新增可选 `/runtime`,通过 package exports 复用根入口的 main/ESM 文件,没有
另一套运行实现。runtime-api.d.ts 定义 Option/Result/Factory,三个薄声明入口
分别处理 NodeNext ESM、CommonJS 和旧 Node10 解析。Promise 只代表注册完成,
不代表抽帧完成。工厂实际没有 `.default` 自引用,因此声明也不提供;历史忽略
的 height 在准确选项类型中保留并注明,number/scale/width 仍有明确数字类型。
内部 option 与这份声明共用,入口显式返回 Promise<Result>,实施 fixture 校验
真实源码函数可赋给公开 Factory,避免声明与自有源码分离漂移。
新增 typesVersions 也补齐 Node10 对旧 `/legacy` 子路径的声明解析。真实 npm
1.1.0 在该路径存在 TS2307,TS4.3.5 另有依赖此 unresolved type 的 TS2344;
保留精确历史诊断,并另用根入口验证旧消费者。未把历史失败吞掉或称为旧包通过。
编辑器声明现在走已有语义生成器,避免同时出现 export default 与 export =。
旧生成方式的 TS2309 有回归;新全局 callable 保留旧同步形状。没有往原根模块
添加 Option/Result 命名导出;旧类型仍通过 Parameters/ReturnType 提取。
README、ARCHITECTURE 与 tests README 同步维护实现地图、使用方式和验证范围。
没有新依赖、锁文件变更、包版本提升、推送或发布。
## 验证与中间失败
结果及实际包/文件指纹见 [验证记录](../baselines/auto-thumbnail-types-validation.json)。
最新 npm 1.1.0 和候选实际 tarball 在仓库外安装,以固定 Yarn 离线强制 frozen
重装并核对每个归档文件字节。TS5.9.3 的 Node10/CJS/ESM/bundler 与 TS4.3.5
Node10 共十组;候选另验证三组关闭 interop 的 CommonJS 消费。
正例检查完整工厂、参数元组、结果和普通模块替换;负例逐行匹配,避免 any 或
未解析模块导致假绿。实际安装 JS 测试 root/legacy/runtime 的 require/import
形状、共享函数身份、Promise 注册、结果及候选订阅清理,不触发视频提取。
初次编辑器测试误用未从根模块导出的命名类型,已改为原有类型提取;没有扩大
根 API 来迁就测试。初次历史诊断断言未区分 TS4.3.5 与 TS5.9.3 的二级错误,
已按实际版本精确记录。真实 no-interop 安装测试发现 runtime-api 的 default
core import 在 NodeNext CJS 下失败,改用核心模块的实际 constructor prototype
类型提取后通过。原失败报告在缓存中保留。最初 Yarn exec 未找到 eslint binary,
改用固定 Node 执行已安装的 ESLint;工具链 frozen 安装和严格检查另行通过。
本项类型工作不是首帧修复,也不代表三浏览器重跑。历史 1.0.x 的 export= 与
1.1.0 的 default、height/number 声明差异已是发布历史的一部分;根保持 1.1.0,
本次没有新增旧类型破坏授权。1.0.x 的完整隔离消费/导出门面仍留在 04/06 与
AUTO-THUMB-TYPE-01、AUTO-THUMB-EXPORT-01,不能仅凭本项关闭整个历史范围。
## 回退与接续
本提交可独立 revert:移除新增类型入口、选项共享和语义编辑器生成接入,恢复
原 manifest、生成文件与测试脚本;原运行算法不变。保留 07 的画布清理修复。
接续优先处理 03 的抽帧真实像素与资源边界;04 汇总自有源码、全部旧消费范围,
05/06 再执行组合、设备、分发及文档完整验收。
@@ -1,6 +1,8 @@
import type Artplayer from 'artplayer'
import type { Factory, Result } from '../../../packages/artplayer-plugin-auto-thumbnail/types/runtime-api'
import factory from '../../../packages/artplayer-plugin-auto-thumbnail/src'
declare const art: Artplayer
const registration: Promise<{ name: string }> = factory({ width: 80, number: 5 })(art)
export { registration }
const registration: Promise<Result> = factory({ width: 80, number: 5 })(art)
const publicFactory: Factory = factory
export { publicFactory, registration }
+6 -4
View File
@@ -2,9 +2,9 @@
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 235 项,范围 22 个包及工作区/示例。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 236 项,范围 22 个包及工作区/示例。
状态:todo 62 / doing 15 / blocked 0 / done 158 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 62 / doing 15 / blocked 0 / done 159 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -19,7 +19,7 @@
| artplayer-plugin-ambilight | 1.1.0 | PKG-AMBILIGHT-01, PKG-AMBILIGHT-02, PKG-AMBILIGHT-03, PKG-AMBILIGHT-04, PKG-AMBILIGHT-PROXY-01, PKG-AMBILIGHT-05, PKG-AMBILIGHT-06, PKG-FACTORY-01 |
| artplayer-plugin-asr | 2.1.0 | PKG-ASR-01, PKG-ASR-02, PKG-ASR-03, PKG-ASR-04, PKG-ASR-05, PKG-ASR-06, PKG-ASR-07, PKG-ASR-08, PKG-ASR-09 |
| artplayer-plugin-audio-track | 1.1.0 | PKG-AUDIO-01, PKG-AUDIO-02, PKG-AUDIO-03, PKG-AUDIO-04, PKG-AUDIO-05, PKG-AUDIO-06 |
| artplayer-plugin-auto-thumbnail | 1.1.0 | PKG-AUTO-THUMB-01, PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-04, PKG-AUTO-THUMB-05, PKG-AUTO-THUMB-06, PKG-AUTO-THUMB-07 |
| artplayer-plugin-auto-thumbnail | 1.1.0 | PKG-AUTO-THUMB-01, PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-04, PKG-AUTO-THUMB-05, PKG-AUTO-THUMB-06, PKG-AUTO-THUMB-07, PKG-AUTO-THUMB-08 |
| artplayer-plugin-chapter | 1.1.0 | PILOT-01, PKG-CHAPTER-01, PKG-CHAPTER-02, PKG-CHAPTER-03, PKG-CHAPTER-04, PKG-CHAPTER-05, PKG-CHAPTER-06 |
| artplayer-plugin-chromecast | 1.1.0 | PKG-CAST-01, PKG-CAST-02, PKG-CAST-03, PKG-CAST-04, PKG-CAST-05, PKG-CAST-06 |
| artplayer-plugin-danmuku | 5.3.0 | PKG-DANMUKU-01, PKG-DANMUKU-02, PKG-DANMUKU-03, PKG-DANMUKU-04, PKG-DANMUKU-05, PKG-DANMUKU-06, PKG-DANMUKU-07, PKG-DANMUKU-08, PKG-DANMUKU-09, PKG-DANMUKU-10, PKG-DANMUKU-11, PKG-DANMUKU-12 |
@@ -175,10 +175,11 @@
| PKG-AUTO-THUMB-01 | artplayer-plugin-auto-thumbnail<br>核对包契约与历史用法 | BASE-05 | url/width/number/scale、异步工厂和渐进缩略图更新 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-AUTO-THUMB-02 | artplayer-plugin-auto-thumbnail<br>建立特有行为与错误测试 | PKG-AUTO-THUMB-01, ENG-03, ENG-05 | 抽帧边界、短视频、失败、连续来源和销毁中的抽帧 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-AUTO-THUMB-03 | artplayer-plugin-auto-thumbnail<br>整理内部职责与资源 | PKG-AUTO-THUMB-02, CORE-09, CORE-19, PKG-AUTO-THUMB-07 | 隐藏 video、seek 队列、canvas 编码与 Blob URL 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | doing |
| PKG-AUTO-THUMB-04 | artplayer-plugin-auto-thumbnail<br>迁移自有源码和公开类型 | PKG-AUTO-THUMB-03, ENG-04, ENG-06, CORE-07 | 抽帧结果和真实异步插件返回类型兼容 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
| PKG-AUTO-THUMB-04 | artplayer-plugin-auto-thumbnail<br>迁移自有源码和公开类型 | PKG-AUTO-THUMB-03, ENG-04, ENG-06, CORE-07, PKG-AUTO-THUMB-08 | 抽帧结果和真实异步插件返回类型兼容 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
| PKG-AUTO-THUMB-05 | artplayer-plugin-auto-thumbnail<br>验证新旧核心和组合 | PKG-AUTO-THUMB-04, CORE-22 | 旧核心/候选核心预览、多次加载无过期缩略图覆盖 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-AUTO-THUMB-06 | artplayer-plugin-auto-thumbnail<br>验证分发并同步文档 | PKG-AUTO-THUMB-05, ENG-07 | auto.thumbnail.js、三种产物和内存/URL 证据 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
| PKG-AUTO-THUMB-07 | artplayer-plugin-auto-thumbnail<br>终止抽帧时清空私有画布像素尺寸 | PKG-AUTO-THUMB-02, CORE-09, CORE-19 | 将私有 canvas 尺寸归入 job 清理;完成、取消、失败及分配重入均有回归,保留已编码 JPEG | 旧红新绿证明保留回调时画布归零,单项清理失败不阻断其他资源;原生取消和完成后 JPEG 仍可解码,原 API/时间公式/类型不变 | M | done |
| PKG-AUTO-THUMB-08 | artplayer-plugin-auto-thumbnail<br>保留旧声明并提供准确异步类型入口 | PKG-AUTO-THUMB-02, ENG-04, ENG-06, CORE-07 | 保留实际 npm 1.1.0 根声明;新增同实现 /runtime 类型、编辑器声明及隔离安装消费者 | 原 Parameters/ReturnType/替代函数不变;准确 Promise 类型正反例、CJS/ESM 条件入口和声明解析通过;不把像素与设备缺口计为通过 | M | done |
## 5 包迁移:artplayer-plugin-vtt-thumbnail
@@ -608,3 +609,4 @@
- PKG-JASSUB-08: [记录](changes/2026-09-14-PKG-JASSUB-08-offscreen.md) [记录](baselines/jassub-offscreen-validation.json) [记录](baselines/jassub-offscreen-patch.json)
- PKG-JASSUB-09: [记录](changes/2026-09-14-PKG-JASSUB-09-hybrid.md) [记录](baselines/jassub-hybrid-validation.json) [记录](baselines/jassub-hybrid-patch.json) [记录](changes/2026-09-14-PKG-JASSUB-09-firefox-diagnostics.md) [记录](changes/2026-09-14-PKG-JASSUB-09-display.md) [记录](baselines/jassub-display-validation.json)
- PKG-AUTO-THUMB-07: [记录](changes/2026-09-14-PKG-AUTO-THUMB-07-canvas.md) [记录](baselines/auto-thumbnail-canvas-validation.json)
- PKG-AUTO-THUMB-08: [记录](changes/2026-09-14-PKG-AUTO-THUMB-08-types.md) [记录](baselines/auto-thumbnail-types-validation.json)
+16
View File
@@ -1,5 +1,21 @@
# 进度与证据
## PKG-AUTO-THUMB-08 公开类型入口完成
保留实际 npm 1.1.0 根声明字节与旧工厂提取/替换形状,新增复用原运行文件的
`/runtime`,准确声明 Promise 注册、选项和结果;未添加不存在的 default 自引用。
内部选项与运行类型共用,源码返回值通过公开 Factory 验证。编辑器改用语义
生成器,修复混合导出声明;Node10 legacy 类型路径已补齐。最新与候选实际安装
十组编译配置和三组无 interop 验证通过,安装产物 Promise 注册与清理通过。
联合 163 项、最终三项类型回归、全仓严格类型检查(406 个生产 TS 文件)、lint、
正常构建、frozen Yarn 安装和严格工具链检查通过;构建运行代码与起点归一化
换行后完全一致。中间 no-interop 声明失败已修复并留证。没有新增依赖或发布。
见[变更](changes/2026-09-14-PKG-AUTO-THUMB-08-types.md)与
[验证](baselines/auto-thumbnail-types-validation.json)。本项从 04 独立拆出,04
仍依赖 03 与 08;03 首帧、1.0.x 完整历史隔离消费及 05/06 仍未完成,不关闭
AUTO-THUMB-PIXEL-01/TYPE-01/EXPORT-01。源码和公开类型阶段完全完成的插件仍
为 14/16;全计划 236 项中 159 done、15 doing、62 todo。
## PKG-AUTO-THUMB-07 私有画布清理完成
抽帧 canvas 现由 job 清理宽高;取消、失败和正常完成后归零,单个 reset 抛错
+1 -1
View File
@@ -183,7 +183,7 @@
| THUMB-MEDIA-01 | open / 已复现 | Windows WebKit native Blob URLs cannot load tested MP4 media despite successful HTTP controls | PKG-TOOL-THUMB-05, PKG-TOOL-THUMB-06 |
| VENDOR-09 | resolved / 源码/产物事实 | Thumbnail tiny-emitter 改写来源与最终分发许可 | PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-06 |
| THUMB-EVENT-01 | resolved / 已复现 | Thumbnail 原型事件名与嵌套 once 派发缺陷 | PKG-TOOL-THUMB-04 |
| AUTO-THUMB-TYPE-01 | open / 已复现 | Published declarations misdescribe async registration and switch height/number option fields across generations | PKG-AUTO-THUMB-04 |
| AUTO-THUMB-TYPE-01 | open / 已复现 | Published declarations misdescribe async registration and switch height/number option fields across generations | PKG-AUTO-THUMB-04, PKG-AUTO-THUMB-08 |
| AUTO-THUMB-EXPORT-01 | open / 已复现 | Published 1.0.1 CommonJS default namespace differs from the direct function exported by 1.1.0 | PKG-AUTO-THUMB-04, PKG-AUTO-THUMB-06 |
| AUTO-THUMB-DIST-01 | open / 已复现 | Published 1.0.0 archive lacks declared main and legacy runtimes | PKG-AUTO-THUMB-06 |
| AUTO-THUMB-LIFE-01 | open / 已复现 | Extraction tasks, decoder handlers and final object URLs lack source-switch and destroy ownership | PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-05 |
+6 -3
View File
@@ -4050,14 +4050,17 @@
"confirmation": "reproduced",
"status": "open",
"owners": [
"PKG-AUTO-THUMB-04"
"PKG-AUTO-THUMB-04",
"PKG-AUTO-THUMB-08"
],
"evidence": [
"refactor/baselines/auto-thumbnail-release.json",
"refactor/baselines/auto-thumbnail-contract.md",
"refactor/scripts/auto-thumbnail-contract.test.mjs"
"refactor/scripts/auto-thumbnail-contract.test.mjs",
"refactor/changes/2026-09-14-PKG-AUTO-THUMB-08-types.md",
"refactor/baselines/auto-thumbnail-types-validation.json"
],
"compatibleResolution": "Preserve actual Promise runtime and old valid consumers; explicitly retain the historically ignored height field and verify paired CJS/ESM declarations.",
"compatibleResolution": "Preserve actual Promise runtime and old valid consumers; explicitly retain the historically ignored height field and verify paired CJS/ESM declarations. PKG-AUTO-THUMB-08 retains npm 1.1.0 root bytes and adds tested /runtime types and editor generation; older 1.0.x isolated consumer/export evidence is still pending in 04/06.",
"closureCriteria": "Old and current isolated consumers, negative type cases and matching runtime registration pass; any irreconcilable inference change has an explicit user decision."
},
{
@@ -0,0 +1,43 @@
import fs from 'node:fs'
export const publicInvalid = [
'thumbnail()',
'thumbnail(undefined)',
'thumbnail({ width: "80" })',
'thumbnail({ height: 90 })',
'const asynchronous: Promise<{name: "artplayerPluginAutoThumbnail"}> = result',
'thumbnail.default({})',
]
export const runtimeInvalid = [
'runtime()',
'runtime(undefined)',
'runtime({ url: 1 })',
'runtime({ width: "80" })',
'runtime({ number: false })',
'runtime({ scale: "1" })',
'runtime({ height: "90" })',
'runtime({ unknown: true })',
'runtime({})()',
'pending.name',
'runtime.default({})',
'const synchronous: Result = pending',
'const wrong: Factory = (_option) => (_art) => ({name: "artplayerPluginAutoThumbnail"})',
]
export function publicSource(mode, raw = false) {
let source = fs.readFileSync(new URL('../../test/types/auto-thumbnail-public.ts', import.meta.url), 'utf8')
if (mode === 'nodenext-esm' && !raw) {
source = source.replace('import thumbnail from \'artplayer-plugin-auto-thumbnail\'', 'import thumbnailModule from \'artplayer-plugin-auto-thumbnail\'\nconst thumbnail = thumbnailModule.default')
.replace('import legacy from \'artplayer-plugin-auto-thumbnail/legacy\'', 'import legacyModule from \'artplayer-plugin-auto-thumbnail/legacy\'\nconst legacy = legacyModule.default')
}
return source
}
export function runtimeSource(mode) {
const source = fs.readFileSync(new URL('../../test/types/auto-thumbnail-runtime.ts', import.meta.url), 'utf8')
return mode.endsWith('-no-interop')
? source.replace('import runtime from \'artplayer-plugin-auto-thumbnail/runtime\'', 'import runtime = require(\'artplayer-plugin-auto-thumbnail/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,150 @@
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 { publicInvalid, publicSource, runtimeInvalid, runtimeSource } from './auto-thumbnail-consumer.mjs'
import { verifyAutoThumbnailContract } from './auto-thumbnail-contract.mjs'
import { hash, readMember } from './releases.mjs'
const name = 'artplayer-plugin-auto-thumbnail'
assert.equal(process.version.slice(1), fs.readFileSync(path.join(workspace, '.node-version'), 'utf8').trim())
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22')
const yarn = process.env.npm_execpath
assert(yarn && fs.existsSync(yarn))
const output = fs.mkdtempSync(path.join(workspace, 'refactor/.cache/auto-thumbnail-package-types-'))
console.log(`Auto Thumbnail installed consumer evidence: ${output}`)
const contract = await verifyAutoThumbnailContract()
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, manifest, archive, sha256: hash(fs.readFileSync(archive)), files: Object.fromEntries(files.map(member => [member, hash(readMember(archive, member))])) })
}
const [core, candidate] = packages
const declaration = `package/types/${name}.d.ts`
const normalize = value => value.toString().replaceAll('\r\n', '\n')
assert.equal(normalize(readMember(candidate.archive, declaration)), normalize(readMember(contract.archives.get('1.1.0'), declaration)))
const matrix = []
function report() {
return writeJson(path.join(output, 'report.json'), {
task: 'PKG-AUTO-THUMB-08',
node: process.version,
packages,
matrix,
scope: 'Actual npm 1.1.0 and packed candidate installed outside the workspace. Root historical type shape and conditional runtime entry are checked with exact negative lines. Registration and module identities use installed artifacts; no native extraction or device acceptance is implied.',
})
}
for (const plugin of [{ ...contract.baseline.release, archive: contract.archives.get('1.1.0'), label: 'published-1.1.0' }, { ...candidate, label: 'candidate' }]) {
const consumer = consumerDirectory()
try {
writeJson(path.join(consumer, 'package.json'), { name: 'auto-thumbnail-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.log`), run([yarn, 'install', '--offline', '--frozen-lockfile', '--force', '--ignore-scripts', '--non-interactive'], consumer))
assert.deepEqual(fs.readFileSync(path.join(consumer, 'yarn.lock')), lock)
for (const pkg of [core, plugin]) {
for (const member of packedFiles(pkg.archive)) {
assert.deepEqual(fs.readFileSync(path.join(consumer, 'node_modules', pkg.name, member.replace(/^package\//, ''))), readMember(pkg.archive, member), member)
}
}
fs.writeFileSync(path.join(output, `${plugin.label}-yarn.lock`), lock)
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, interop = true) => {
fs.writeFileSync(filename, source)
const program = compiler.createProgram([filename], { ...options, esModuleInterop: interop })
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(item => ({
file: item.file ? path.relative(consumer, item.file.fileName).replaceAll('\\', '/') : null,
code: item.code,
line: item.file && item.start !== undefined ? item.file.getLineAndCharacterOfPosition(item.start).line + 1 : null,
message: compiler.flattenDiagnosticMessageText(item.messageText, '\n'),
}))
}
const entry = { plugin: plugin.label, compiler: compiler.version, mode }
matrix.push(entry)
let source = publicSource(mode)
entry.public = compile(source)
if (plugin.label === 'published-1.1.0' && mode === 'node10-commonjs') {
const lines = source.split('\n')
assert.deepEqual(entry.public.map(item => ({ code: item.code, line: item.line })), [
{ code: 2307, line: lines.findIndex(line => line.includes('from \'artplayer-plugin-auto-thumbnail/legacy\'')) + 1 },
...(compiler === compat ? [{ code: 2344, line: lines.findIndex(line => line.startsWith('type ExactLegacy')) + 1 }] : []),
], 'Preserve the historical Node10 legacy subpath failure')
source = source.replace('from \'artplayer-plugin-auto-thumbnail/legacy\'', 'from \'artplayer-plugin-auto-thumbnail\'')
entry.rootOnly = compile(source)
assert.deepEqual(entry.rootOnly, [])
}
else {
assert.deepEqual(entry.public, [], `${plugin.label} ${compiler.version} ${mode}`)
}
entry.publicInvalid = compile(`${source}\n${publicInvalid.join('\n')}`)
assert.deepEqual(entry.publicInvalid.map(item => item.line), publicInvalid.map((_, index) => source.split('\n').length + 1 + index))
if (plugin.label === 'candidate') {
const runtime = runtimeSource(mode)
entry.runtime = compile(runtime)
entry.runtimeDeclarations = declarations
assert.deepEqual(entry.runtime, [], `${compiler.version} ${mode}`)
assert(declarations.includes(`node_modules/${name}/types/runtime.${next ? mode.endsWith('-cjs') ? 'd.cts' : 'd.mts' : mode === 'bundler-esm' ? 'd.mts' : 'd.ts'}`))
entry.runtimeInvalid = compile(`${runtime}\n${runtimeInvalid.join('\n')}`)
assert.deepEqual(entry.runtimeInvalid.map(item => item.line), runtimeInvalid.map((_, index) => runtime.split('\n').length + 1 + index))
if (mode === 'node10-commonjs' || mode === 'nodenext-cjs') {
entry.noInterop = compile(runtimeSource(`${mode}-no-interop`), false)
assert.deepEqual(entry.noInterop, [])
}
}
report()
}
const probe = path.join(consumer, 'runtime.cjs')
fs.writeFileSync(probe, `const assert = require('node:assert/strict');
(async () => {
const names = ${JSON.stringify([name, `${name}/legacy`, ...(plugin.label === 'candidate' ? [`${name}/runtime`] : [])])};
const commonjs = names.map(name => require(name));
const modules = await Promise.all(names.map(name => import(name)));
for (const factory of [...commonjs, ...modules.map(module => module.default)]) {
assert.equal(typeof factory, 'function'); assert.equal(factory.default, undefined);
const callbacks = new Map();
const art = { option: {url: '/never-loaded.mp4'}, on(name, callback) { callbacks.set(name, callback); }, off(name, callback) { if (callbacks.get(name) === callback) callbacks.delete(name); } };
const pending = factory({})(art);
assert(pending instanceof Promise); assert.equal(pending.name, undefined);
assert.deepEqual(await pending, {name: 'artplayerPluginAutoThumbnail'});
assert(callbacks.has('video:loadedmetadata'));
${plugin.label === 'candidate' ? 'callbacks.get(\'destroy\')(); assert.equal(callbacks.size, 0);' : ''}
}
${plugin.label === 'candidate' ? 'assert.equal(commonjs[2], commonjs[0]); assert.equal(modules[2].default, modules[0].default);' : ''}
console.log('Installed root/legacy/runtime module shapes and Promise registration verified; no native extraction.');
})().catch(error => { console.error(error); process.exitCode = 1; });`)
fs.writeFileSync(path.join(output, `${plugin.label}-runtime.log`), run([probe], consumer))
}
finally {
report()
removeConsumer(consumer)
}
}
console.log(`Passed ${matrix.length} installed compiler profiles; ${output}`)
@@ -0,0 +1,63 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
// eslint-disable-next-line test/no-import-node-test -- Repository 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 { publicInvalid, publicSource, runtimeInvalid, runtimeSource } from './auto-thumbnail-consumer.mjs'
import { verifyAutoThumbnailContract } from './auto-thumbnail-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('Auto Thumbnail preserves actual npm 1.1.0 root bytes and historical factory extraction', async () => {
const contract = await verifyAutoThumbnailContract()
const current = fs.readFileSync('packages/artplayer-plugin-auto-thumbnail/types/artplayer-plugin-auto-thumbnail.d.ts', 'utf8')
const previous = readMember(contract.archives.get('1.1.0'), 'package/types/artplayer-plugin-auto-thumbnail.d.ts').toString()
assert.equal(current.replaceAll('\r\n', '\n'), previous.replaceAll('\r\n', '\n'))
for (const [compiler, mode] of modes) {
const source = publicSource(mode)
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${compiler.version} ${mode}`)
const invalid = checkConsumer(compiler, mode, `${source}\n${publicInvalid.join('\n')}`)
assert.deepEqual(invalid.map(item => item.line), publicInvalid.map((_, index) => source.split('\n').length + 1 + index))
}
})
test('Auto Thumbnail runtime types expose only the real asynchronous factory and options', () => {
for (const [compiler, mode] of modes) {
const source = runtimeSource(mode)
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${compiler.version} ${mode}`)
const invalid = checkConsumer(compiler, mode, `${source}\n${runtimeInvalid.join('\n')}`)
assert.deepEqual(invalid.map(item => item.line), runtimeInvalid.map((_, index) => source.split('\n').length + 1 + index))
}
assert.deepEqual(checkConsumer(ts, 'nodenext-cjs', `import runtime = require('artplayer-plugin-auto-thumbnail/runtime');
import type Artplayer from 'artplayer'; declare const art: Artplayer;
const option: runtime.Option = {}; const result: Promise<runtime.Result> = runtime(option)(art);
const factory: runtime.Factory = runtime; void [result, factory];`), [])
})
test('Auto Thumbnail generated editor globals replace conflicting export assignments and retain old calls', async () => {
const source = fs.readFileSync('packages/artplayer-plugin-auto-thumbnail/types/artplayer-plugin-auto-thumbnail.d.ts', 'utf8')
const generated = generatePluginEditorDeclaration(source, 'artplayerPluginAutoThumbnail')
const file = 'docs/assets/ts/artplayer-plugin-auto-thumbnail.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;
type Option = Parameters<typeof artplayerPluginAutoThumbnail>[0];
type Result = ReturnType<ReturnType<typeof artplayerPluginAutoThumbnail>>;
const option: Option = { width: 80, number: 12 };
const result: Result = artplayerPluginAutoThumbnail(option)(art);
const replacement: typeof artplayerPluginAutoThumbnail = _option => _art => result;`
for (const compiler of [ts, compat]) {
assert.deepEqual(checkPluginEditorDeclaration(generated, core, consumer, compiler), [])
const invalid = checkPluginEditorDeclaration(generated, core, `${consumer}\nartplayerPluginAutoThumbnail();\nconst bad: Promise<Result> = result;`, compiler)
assert.deepEqual(invalid.map(item => item.code), [2554, 2739])
const previous = `${source.replace(/^import.*$/gim, '')}\nexport = artplayerPluginAutoThumbnail;\nexport as namespace artplayerPluginAutoThumbnail;\n`
assert(checkPluginEditorDeclaration(previous, core, '', compiler).some(item => item.code === 2309))
}
})
+24 -1
View File
@@ -1811,7 +1811,8 @@
"PKG-AUTO-THUMB-03",
"ENG-04",
"ENG-06",
"CORE-07"
"CORE-07",
"PKG-AUTO-THUMB-08"
],
"status": "todo",
"risk": "H",
@@ -4838,6 +4839,28 @@
"changes/2026-09-14-PKG-AUTO-THUMB-07-canvas.md",
"baselines/auto-thumbnail-canvas-validation.json"
]
},
{
"id": "PKG-AUTO-THUMB-08",
"phase": "5 包迁移:artplayer-plugin-auto-thumbnail",
"title": "保留旧声明并提供准确异步类型入口",
"scope": [
"artplayer-plugin-auto-thumbnail"
],
"dependsOn": [
"PKG-AUTO-THUMB-02",
"ENG-04",
"ENG-06",
"CORE-07"
],
"status": "done",
"risk": "M",
"deliverable": "保留实际 npm 1.1.0 根声明;新增同实现 /runtime 类型、编辑器声明及隔离安装消费者",
"acceptance": "原 Parameters/ReturnType/替代函数不变;准确 Promise 类型正反例、CJS/ESM 条件入口和声明解析通过;不把像素与设备缺口计为通过",
"evidence": [
"changes/2026-09-14-PKG-AUTO-THUMB-08-types.md",
"baselines/auto-thumbnail-types-validation.json"
]
}
]
}