fix(canvas): [PKG-CANVAS-04] preserve public type and export compatibility

This commit is contained in:
Harvey Zhao committed 2026-09-12 13:29:10 +08:00
1 parent 2b319109cd
commit 224572032a
29 files changed
+682 -41

No files matched your search

@@ -0,0 +1,198 @@
{
"schemaVersion": 1,
"task": "PKG-CANVAS-04",
"status": "done",
"checks": [
{
"command": "node --test refactor/scripts/canvas-types.test.mjs",
"result": "pass",
"tests": 4,
"scope": "Exact published declarations, optional Parameters and exact Canvas result, five compiler modes with eleven negative checks, semantic editor globals and actual production exports"
},
{
"command": "yarn test:canvas-types-package",
"result": "pass with explicit historical diagnostics",
"cases": 17,
"candidateZeroDiagnosticCases": 7,
"historicalZeroDiagnosticCases": 9,
"historicalKnownFailureCases": 1,
"expectedHistoricalCodes": [
2349,
2349,
2322,
2344,
2344
],
"candidateInvalidUseRejectionsPerMode": 11,
"report": "refactor/.cache/canvas-package-types-NZAnqz/report.json",
"sha256": "05c8f0a0367e1478b59e839520e5c7572b3d5519df7323af21ead9e967524da8"
},
{
"command": "yarn build:ts && yarn build artplayer-proxy-canvas",
"result": "pass",
"scope": "Semantic editor declarations and real three-format outputs"
},
{
"command": "focused eslint --no-fix",
"result": "pass"
},
{
"command": "playwright test test/browser/canvas-lifecycle.spec.js",
"result": "pass",
"tests": 18,
"skipped": 0,
"browsers": [
{
"project": "chromium",
"version": "153.0.8010.12",
"platform": "win32"
},
{
"project": "firefox",
"version": "155.0",
"platform": "win32"
},
{
"project": "webkit",
"version": "26.6",
"platform": "win32"
}
],
"scope": "Candidate source with default alias, actual npm core5.4.0/candidate, native pixels/dimensions/resize/source/paused seek and terminal media/frame ownership",
"report": "refactor/.cache/canvas04-final-results/report.json",
"sha256": "af893ebcd22fa6312db1e02b222849135ca89568ecf196c88ce2947436cda0ae",
"resultsDirectory": "refactor/.cache/canvas04-final-results/results"
},
{
"command": "yarn ci:check",
"result": "pass",
"tests": {
"unit": 938,
"engineering": 14,
"baseline": 100,
"mainTotal": 1052,
"repeatedContractExecutions": 44
},
"strictProductionTypeScriptFiles": 284
}
],
"limitations": [
"Published 1.1.0 NodeNext ESM has five recorded diagnostics; expected historical failure is not zero-error compilation.",
"Full distribution/deep paths/demo and device gates remain tasks 05/06."
],
"sourceCommit": "2b319109cd191439d49d2cb701632c95ed04cff4",
"inputs": [
{
"file": "packages/artplayer-proxy-canvas/src/index.ts",
"sha256LF": "2124bfe63da80f7d0b25a2d0e397e13ca002dd74a4293a0ae023c3c3702a69ec"
},
{
"file": "packages/artplayer-proxy-canvas/package.json",
"sha256LF": "6c84b3b089dd341edaca26655289afcf834c5b4b67af8d56e0703155b004ae3a"
},
{
"file": "packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.ts",
"sha256LF": "85cdcf86ed12b383785f5303b41458d5d39117b83e74d9f7e99545e59ab4687b"
},
{
"file": "packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.mts",
"sha256LF": "fdff66b63ff24a93370e7f4e87ed6b19a1bd4a2305aa6db37da2c38af3415458"
},
{
"file": "packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.cts",
"sha256LF": "812d9c09a027834e45619dceb04f8b49876623771ad7ce570ab5920383679fea"
},
{
"file": "docs/assets/ts/artplayer-proxy-canvas.d.ts",
"sha256LF": "4f4163b4250bf001d7807c26e8e34e784eba70581997d43142febbd524a2a8f7"
},
{
"file": "test/types/canvas.ts",
"sha256LF": "ae59e12c59316c387bb41a443b65eae9ad747959e61e76c791356e8d68014d33"
},
{
"file": "refactor/fixtures/consumers/canvas-published.ts",
"sha256LF": "590578a232bd9b6716dc82e27d5b4d9ffd90943115a26ae6550f338daf40258d"
},
{
"file": "refactor/scripts/canvas-types.test.mjs",
"sha256LF": "df08de1af269201669dc93d5feb533f3c3cab1063bf94c98d1a3665e174e8fb1"
},
{
"file": "refactor/scripts/canvas-package-types.mjs",
"sha256LF": "71c4de28bc9595441ae2265fb4b45682acd637835315817310748aa4708885d7"
},
{
"file": "scripts/build-ts.js",
"sha256LF": "c9ece2c799cb81f7c35d4b5714c184c59a3a741379d3dcb970d8afbaaae98ad0"
},
{
"file": "scripts/typecheck.mjs",
"sha256LF": "583e4c855cb1541c7c2a9f812f91f32f5b5893c35db45cb457223c218250e2aa"
}
],
"artifacts": [
{
"file": "packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.js",
"sha256LF": "a901e5c5fc00a64a6c2ae9d4b2b846950412710ec9672d30640d40987d4f9e73"
},
{
"file": "packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.legacy.js",
"sha256LF": "052263fdd7ab657a0183d874a706c2cf82527233c5e60b58158caa0677718b97"
},
{
"file": "packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.mjs",
"sha256LF": "66ca7271f52545a8796f563f2d5ca04581bed8d60d218463923325acf5b73ccf"
},
{
"file": "docs/compiled/artplayer-proxy-canvas.js",
"sha256LF": "a901e5c5fc00a64a6c2ae9d4b2b846950412710ec9672d30640d40987d4f9e73"
},
{
"file": "docs/compiled/artplayer-proxy-canvas.legacy.js",
"sha256LF": "052263fdd7ab657a0183d874a706c2cf82527233c5e60b58158caa0677718b97"
},
{
"file": "docs/compiled/artplayer-proxy-canvas.mjs",
"sha256LF": "66ca7271f52545a8796f563f2d5ca04581bed8d60d218463923325acf5b73ccf"
}
],
"logs": [
{
"file": "refactor/.cache/canvas04-editor-build.log",
"sha256": "d6014f1ae611921f7d0fd47e1a27b5d5488f7ef6056731948edff8864b6653e9"
},
{
"file": "refactor/.cache/canvas04-build.log",
"sha256": "c9da09787163706b1f90adf55ff901b70963d7acc45df38e689789e0cea410b5"
},
{
"file": "refactor/.cache/canvas04-targeted.log",
"sha256": "a1d89c6c052c003d23d22b0b26a749eb34522b96191e49af31531052805b89ca"
},
{
"file": "refactor/.cache/canvas04-lint-final.log",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"file": "refactor/.cache/canvas04-installed-first.log",
"sha256": "589a52e4720ea68d6aeed8145ea0af8b88f7220a61dfd9909fc9cf1fb9ab4663"
},
{
"file": "refactor/.cache/canvas04-installed-verified.log",
"sha256": "e96728c91e3afe9431ab80addb09b2de45554720547c41a75ef6bbe402266e07"
},
{
"file": "refactor/.cache/canvas04-browser.log",
"sha256": "d4450a918655a45add2589c62326ce4b4f80373e3600b8cee8c10e9379e3df94"
},
{
"file": "refactor/.cache/canvas04-ci.log",
"sha256": "efc36adbaf3d23a8e90d66829afa01bf87fe8cc31eac927bbc5dc90f11093465"
}
],
"firstHistoricalFailure": {
"file": "refactor/.cache/canvas-package-types-qQ0exQ/published-1.1.0-5.9.3-nodenext-esm-diagnostics.json",
"scope": "Preserved exact five diagnostics before updating the expected historical failure count for the Canvas ReturnType consumer"
}
}
+17
View File
@@ -56,3 +56,20 @@ WebKit须验证intrinsic尺寸,不能仅alpha255;1px挂载曾令videoWidth/v
失败诊断与针对性回归见[03记录](changes/2026-09-12-PKG-CANVAS-03-lifecycle.md)和
[03证据](baselines/canvas-lifecycle-validation.json)。04公开类型/导出和05设备、06
完整分发/demo仍需完成;本步的strict内部类型不替代安装消费者验收。
## 04 公开类型和分发入口
`node --test refactor/scripts/canvas-types.test.mjs`运行四组基线:两代真实声明、五种
编译器模式、11项非法用法、生成编辑器全局及三格式导出身份。Parameters保持可选
callback,Result精确保持HTMLCanvasElement;MediaCanvas只作显式类型视图,不能
用于收窄旧返回值或假设所有浏览器具备全部媒体API。不要复制Ambilight的必填末重载。
`yarn test:canvas-types-package`在仓库外安装两份实际发布及候选,校验文件字节和
非workspace链接,离线安装与frozen reinstall均须通过。17个编译场景里,候选7个
及历史9个应零诊断;另1个npm1.1.0 NodeNext ESM保留2349/2349/2322/2344/2344
五个旧诊断。候选每种模式移除@ts-expect-error后须拒绝11项非法用法。
公开namespace是编辑器声明源,不手改docs/assets/ts生成文件。.default与工厂
自身同一引用;根ESM/CJS及legacy类型入口各自测试。完整路径/内容/demo仍留06,
真实Safari/移动设备仍留05。见[04实施记录](changes/2026-09-12-PKG-CANVAS-04-types.md)
和[04证据](baselines/canvas-types-validation.json)。
@@ -0,0 +1,24 @@
# PKG-CANVAS-04 公开类型与导出兼容
公开namespace定义Option、Result、Callable、Factory及显式MediaCanvas视图。Callable
只有可选callback签名,保持1.1.0的Parameters包含undefined;Result仍精确等于
HTMLCanvasElement,不把返回值收窄为必须具备全部媒体属性的交叉类型。MediaCanvas
供显式使用转发成员的消费者断言,Canvas自有成员优先,不声称新增浏览器能力。
入口default自别名兼容旧require.default与直接可调用工厂;ESM仅default运行时导出。
新增.d.mts/.d.cts入口包装和import/require条件类型,typesVersions保留旧TS legacy路径。
公开namespace接入语义编辑器声明生成,README和包内架构同步说明用法与边界。
四组专项覆盖五种旧/新编译器模式、11项非法用法、两份真实发布声明、生成全局声明
以及实际三格式产物。1.0.0回调必填到1.1.0可选是既有发布差异,候选不恢复必填限制。
新旧ReturnType消费者仍可赋值普通Canvas;没有借用Ads的类型推导修正授权。
仓库外真实安装先复现npm1.1.0在TS5.9.3 NodeNext ESM的五个精确诊断:
2349/2349/2322/2344/2344。默认导入被当作CommonJS模块namespace而不可调用,
额外ReturnType断言也受影响;该历史失败单独保留,不能把历史全部场景称为零诊断。
候选安装消费者必须无诊断,且移除@ts-expect-error后拒绝全部11项非法用法。
结果和输入哈希见[验证证据](../baselines/canvas-types-validation.json)。05真实设备和
06完整分发/demo验收仍需完成,本步不发布npm、不修改版本、不推送。
最终四组专项、编辑器/三格式构建、仓库外17场景和18项真实浏览器通过;17场景保留1项历史五诊断,不能等同17项零错误。完整CI1052项通过(938单元+14工程+100基线),另44项重复契约观察;284个生产TS文件严格检查。CANVAS-TYPE-01及CANVAS-DIST-01具体风险关闭,本任务done并独立本地提交,05/06及整体发布验收继续。
@@ -0,0 +1,11 @@
import Artplayer from 'artplayer'
import canvas from 'artplayer-proxy-canvas'
const callback = (context: CanvasRenderingContext2D, video: HTMLVideoElement) => context.drawImage(video, 0, 0)
const art = new Artplayer({ container: '#player', url: 'video.mp4', proxy: canvas(callback) })
const result = canvas(callback)(art)
const actual: HTMLCanvasElement = result
const context: CanvasRenderingContext2D | null = result.getContext('2d')
const accepted: Parameters<typeof canvas>[0] = callback
const originalResult: ReturnType<ReturnType<typeof canvas>> = document.createElement('canvas')
void [actual, context, accepted, originalResult]
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 221 项,范围 22 个包及工作区/示例。
状态:todo 123 / doing 6 / blocked 0 / done 92 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 122 / doing 6 / blocked 0 / done 93 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -321,7 +321,7 @@
| PKG-CANVAS-01 | artplayer-proxy-canvas<br>核对包契约与历史用法 | BASE-05 | callback、canvas 原方法、转发 media 属性/方法/事件 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-CANVAS-02 | artplayer-proxy-canvas<br>建立特有行为与错误测试 | PKG-CANVAS-01, ENG-03, ENG-05 | ready/loadeddata/canplay、play/seek、回调、resize 与销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-CANVAS-03 | artplayer-proxy-canvas<br>整理内部职责与资源 | PKG-CANVAS-02, CORE-06, CORE-11, CORE-16 | video adapter/原 canvas 方法/RAF 绘制/事件订阅分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-CANVAS-04 | artplayer-proxy-canvas<br>迁移自有源码和公开类型 | PKG-CANVAS-03, ENG-04, ENG-06, CORE-07 | canvas 与媒体能力的精确组合类型,保持 Result | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
| PKG-CANVAS-04 | artplayer-proxy-canvas<br>迁移自有源码和公开类型 | PKG-CANVAS-03, ENG-04, ENG-06, CORE-07 | canvas 与媒体能力的精确组合类型,保持 Result | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done |
| PKG-CANVAS-05 | artplayer-proxy-canvas<br>验证新旧核心和组合 | PKG-CANVAS-04, CORE-22 | 真实 video/canvas 绘制、字幕和 document PiP 恢复 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-CANVAS-06 | artplayer-proxy-canvas<br>验证分发并同步文档 | PKG-CANVAS-05, ENG-07 | canvas.js、三种产物、调用兼容与资源证据 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
@@ -509,3 +509,4 @@
- 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)
- PKG-CANVAS-04: [记录](changes/2026-09-12-PKG-CANVAS-04-types.md) [记录](baselines/canvas-types-validation.json) [记录](canvas-validation.md)
+11
View File
@@ -1,5 +1,16 @@
# 进度与证据
## 最新完成:Canvas-04 公开类型与两代导出
公开namespace保留可选Parameters和精确HTMLCanvasElement返回,新增显式MediaCanvas
视图;工厂.default自别名、ESM/CJS声明包装、旧TS legacy路径及语义编辑器生成同步。
仓库外安装17场景:候选7个/历史9个零诊断,另1个历史NodeNext ESM保留五个精确
诊断;候选每种模式拒绝11项非法用法。四组专项、18项真实浏览器及完整CI1052项
通过,另44项重复契约观察,284个生产TS文件严格检查。类型/两代导出具体风险关闭,
设备、Document PiP组合及完整demo/分发仍待05/06。当前221项:93 done、6 doing、
122 todo。见 [本步记录](changes/2026-09-12-PKG-CANVAS-04-types.md) 和
[验证证据](baselines/canvas-types-validation.json)。独立本地提交,无推送/发布。
## 最新完成:Canvas-03 严格TS职责与生命周期
生产源码拆为入口/媒体转发/底层video所有权/尺寸/绘制/调度六个严格TS模块,修复
+2 -2
View File
@@ -156,6 +156,6 @@
| AMBILIGHT-DIST-01 | resolved / 已复现 | Ambilight 1.0.0 CommonJS default namespace differs from callable 1.1.0 | PKG-AMBILIGHT-04, PKG-AMBILIGHT-06 |
| AMBILIGHT-PROXY-01 | resolved / 已复现 | Canvas proxy output pixels differ from forwarded video dimensions | PKG-AMBILIGHT-PROXY-01, PKG-AMBILIGHT-05 |
| CANVAS-LIFE-01 | resolved / 已复现 | Canvas proxy pending draw, bitmap and delayed subscriptions lack terminal ownership | PKG-CANVAS-02, PKG-CANVAS-03, PKG-CANVAS-05 |
| CANVAS-TYPE-01 | open / 源码/产物事实 | Canvas proxy declarations change required callback across generations and omit forwarded media capabilities | PKG-CANVAS-04, PKG-CANVAS-06 |
| CANVAS-DIST-01 | open / 已复现 | Canvas proxy published CommonJS namespace differs from current callable export | PKG-CANVAS-04, PKG-CANVAS-06 |
| CANVAS-TYPE-01 | resolved / 源码/产物事实 | Canvas proxy declarations change required callback across generations and omit forwarded media capabilities | PKG-CANVAS-04, PKG-CANVAS-06 |
| CANVAS-DIST-01 | resolved / 已复现 | Canvas proxy published CommonJS namespace differs from current callable export | PKG-CANVAS-04, PKG-CANVAS-06 |
| CANVAS-PIXEL-01 | resolved / 已复现 | Historical Canvas proxy produces transparent frames with detached native video in tested WebKit | PKG-CANVAS-03, PKG-CANVAS-05 |
+16 -4
View File
@@ -3376,12 +3376,18 @@
],
"compatibleResolution": "Keep optional current callback inference and actual canvas return while validating historical consumers and any additive media surface types.",
"closureCriteria": "Strict old/current compiler consumers and installed callback/result/import variants verified without new inference narrowing.",
"status": "open",
"status": "resolved",
"evidence": [
"refactor/baselines/canvas-contract.md",
"refactor/baselines/canvas-release.json",
"refactor/changes/2026-09-12-PKG-CANVAS-01-contract.md"
]
],
"resolutionEvidence": [
"refactor/changes/2026-09-12-PKG-CANVAS-04-types.md",
"refactor/baselines/canvas-types-validation.json"
],
"resolutionRationale": "Packed candidate compiles in seven old/current compiler configurations; Parameters stays optional and ReturnType stays exactly HTMLCanvasElement, including assigning an ordinary Canvas. Additive MediaCanvas is explicit opt-in. Eleven invalid uses are rejected in every candidate mode; exact archived declarations retain the pre-existing 1.0.0 required to 1.1.0 optional distinction. Candidate also corrects the recorded published 1.1.0 NodeNext ESM failure.",
"workspaceState": "Packed candidate compiles in seven old/current compiler configurations; Parameters stays optional and ReturnType stays exactly HTMLCanvasElement, including assigning an ordinary Canvas. Additive MediaCanvas is explicit opt-in. Eleven invalid uses are rejected in every candidate mode; exact archived declarations retain the pre-existing 1.0.0 required to 1.1.0 optional distinction. Candidate also corrects the recorded published 1.1.0 NodeNext ESM failure."
},
{
"id": "CANVAS-DIST-01",
@@ -3393,12 +3399,18 @@
],
"compatibleResolution": "Preserve both require.default and callable factory with actual global, legacy and ESM paths.",
"closureCriteria": "Packed runtime/type and real local demo/editor consumers pass.",
"status": "open",
"status": "resolved",
"evidence": [
"refactor/baselines/canvas-contract.md",
"refactor/baselines/canvas-release.json",
"refactor/changes/2026-09-12-PKG-CANVAS-01-contract.md"
]
],
"resolutionEvidence": [
"refactor/changes/2026-09-12-PKG-CANVAS-04-types.md",
"refactor/baselines/canvas-types-validation.json"
],
"resolutionRationale": "Production main/legacy/browser global and ESM plus real isolated installed root/legacy imports retain callable factory and self default identities. Seven installed type consumer modes validate the correct declaration wrapper including no-interop. Full deep paths/content/demo and device checks remain tasks 05/06.",
"workspaceState": "Production main/legacy/browser global and ESM plus real isolated installed root/legacy imports retain callable factory and self default identities. Seven installed type consumer modes validate the correct declaration wrapper including no-interop. Full deep paths/content/demo and device checks remain tasks 05/06."
},
{
"id": "CANVAS-PIXEL-01",
+104
View File
@@ -0,0 +1,104 @@
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 { verifyCanvasContract } from './canvas-contract.mjs'
import { hash, readMember } from './releases.mjs'
async function main() {
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use yarn test:canvas-types-package')
const yarn = process.env.npm_execpath
assert(yarn && fs.existsSync(yarn))
const output = fs.mkdtempSync(path.join(workspace, 'refactor/.cache/canvas-package-types-'))
console.log(`Canvas isolated package evidence: ${output}`)
const contract = await verifyCanvasContract()
const packages = []
for (const name of ['artplayer', 'artplayer-proxy-canvas']) {
const archive = path.join(output, `${name}.tgz`)
fs.writeFileSync(path.join(output, `${name}-pack.log`), run([yarn, 'pack', '--filename', archive], path.join(workspace, 'packages', name)))
const manifest = JSON.parse(readMember(archive, 'package/package.json'))
const files = packedFiles(archive)
checkFiles(manifest, files, name === contract.baseline.release.name ? Object.keys(contract.baseline.release.files) : [])
packages.push({ name, archive, sha256: hash(fs.readFileSync(archive)), files: Object.fromEntries(files.map(member => [member, hash(readMember(archive, member))])) })
}
const matrix = []
const core = packages[0]
const candidates = [
...[contract.baseline.previous[0], contract.baseline.release].map(release => ({ ...release, archive: contract.archives.get(release.version), label: `published-${release.version}` })),
{ ...packages[1], label: 'candidate' },
]
for (const plugin of candidates) {
const consumer = consumerDirectory()
try {
writeJson(path.join(consumer, 'package.json'), { name: 'canvas-isolated-consumer', private: true, dependencies: Object.fromEntries([core, plugin].map(pkg => [pkg.name, `file:${pkg.archive.replaceAll('\\', '/')}`])) })
fs.copyFileSync(path.join(workspace, 'yarn.lock'), path.join(consumer, 'yarn.lock'))
fs.writeFileSync(path.join(output, `${plugin.label}-install.log`), run([yarn, 'install', '--offline', '--ignore-scripts', '--non-interactive'], consumer))
const lock = fs.readFileSync(path.join(consumer, 'yarn.lock'))
fs.writeFileSync(path.join(output, `${plugin.label}-frozen-install.log`), run([yarn, 'install', '--offline', '--frozen-lockfile', '--ignore-scripts', '--non-interactive'], consumer))
assert.deepEqual(fs.readFileSync(path.join(consumer, 'yarn.lock')), lock)
fs.writeFileSync(path.join(output, `${plugin.label}-yarn.lock`), lock)
for (const pkg of [core, plugin]) {
const root = path.join(consumer, 'node_modules', pkg.name)
assert.equal(fs.realpathSync(root), root, 'Installed package must not be a workspace link')
for (const [member, expected] of Object.entries(pkg.files))
assert.equal(hash(fs.readFileSync(path.join(root, member.slice(8)))), expected, `Installed bytes differ: ${member}`)
}
const runtimeFile = path.join(consumer, 'exports.cjs')
fs.writeFileSync(runtimeFile, plugin.label === 'candidate'
? `const assert = require('node:assert/strict');
for (const name of ['artplayer-proxy-canvas', 'artplayer-proxy-canvas/legacy']) { const factory = require(name); assert.equal(typeof factory, 'function'); assert.equal(factory.default, factory); assert.equal(typeof factory(), 'function'); }
import('artplayer-proxy-canvas').then(module => { assert.deepEqual(Object.keys(module), ['default']); assert.equal(module.default.default, module.default); }).catch(error => { console.error(error); process.exitCode = 1; });`
: `const assert = require('node:assert/strict'); const value = require('artplayer-proxy-canvas'); assert.equal(typeof value, '${plugin.version === '1.0.0' ? 'object' : 'function'}'); assert.equal(typeof (value.default || value)(() => {}), 'function');`)
fs.writeFileSync(path.join(output, `${plugin.label}-runtime.log`), run([runtimeFile], consumer))
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
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 extension = next ? mode.endsWith('-cjs') ? 'cts' : 'mts' : 'ts'
const source = fs.readFileSync(path.join(workspace, plugin.label === 'candidate' ? 'test/types/canvas.ts' : 'refactor/fixtures/consumers/canvas-published.ts'), 'utf8')
const filename = path.join(consumer, `consumer.${extension}`)
const historical = path.join(consumer, `historical.${extension}`)
fs.copyFileSync(path.join(workspace, 'refactor/fixtures/consumers/canvas-published.ts'), historical)
const files = plugin.label === 'candidate' ? [filename, historical] : [filename]
if (mode === 'nodenext-cjs' && plugin.label !== 'published-1.1.0') {
const commonjs = path.join(consumer, 'commonjs.cts')
fs.writeFileSync(commonjs, `import canvas = require('artplayer-proxy-canvas'); canvas((ctx, video) => ctx.drawImage(video, 0, 0)); ${plugin.label === 'candidate' ? 'canvas.default();' : ''}`)
files.push(commonjs)
}
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: !mode.endsWith('-no-interop'), target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], module: next ? compiler.ModuleKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleKind.ESNext : compiler.ModuleKind.CommonJS, moduleResolution: next ? compiler.ModuleResolutionKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleResolutionKind.Bundler : compiler.ModuleResolutionKind.NodeJs }
function compile(code) {
fs.writeFileSync(filename, code)
const program = compiler.createProgram(files, options)
for (const file of program.getSourceFiles()) {
const actual = fs.realpathSync(file.fileName)
assert(actual.startsWith(fs.realpathSync(consumer) + path.sep) || (program.isSourceFileDefaultLibrary(file) && path.dirname(actual) === fs.realpathSync(path.dirname(compiler.sys.getExecutingFilePath()))), `Types escaped installed consumer: ${actual}`)
}
return compiler.getPreEmitDiagnostics(program).map(item => ({ code: item.code, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n') }))
}
const diagnostics = compile(source)
const historicalFailure = plugin.label === 'published-1.1.0' && mode === 'nodenext-esm'
writeJson(path.join(output, `${plugin.label}-${compiler.version}-${mode}-diagnostics.json`), { diagnostics, historicalFailure })
assert.deepEqual(diagnostics.map(item => item.code), historicalFailure ? [2349, 2349, 2322, 2344, 2344] : [], `${plugin.label} ${compiler.version} ${mode}`)
const invalid = plugin.label === 'candidate' ? compile(source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')) : []
if (plugin.label === 'candidate')
assert.equal(invalid.length, 11, 'Installed declarations must reject all invalid uses')
matrix.push({ plugin: plugin.label, compiler: compiler.version, mode, historicalFailure, diagnostics, invalid })
}
}
finally {
removeConsumer(consumer)
}
}
writeJson(path.join(output, 'report.json'), { suite: 'canvas-isolated-package-types', introducedBy: 'PKG-CANVAS-04', scope: 'Two actual published proxies and packed candidate installed outside workspace with packed candidate core; offline and frozen reinstall, exact bytes, compiler/default/import= contracts. Published 1.0.0 required callback became optional in 1.1.0; candidate retains optional Parameters and exact HTMLCanvasElement return inference. This is not native proxy media, device or complete distribution acceptance.', packages, published: candidates.slice(0, 2).map(pkg => ({ label: pkg.label, archive: pkg.archive, sha256: pkg.sha256 })), matrix })
console.log(`Canvas installed types passed: ${matrix.length} cases; ${output}`)
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
+91
View File
@@ -0,0 +1,91 @@
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 -- Node baseline runner.
import test from 'node:test'
import vm from 'node:vm'
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 { verifyCanvasContract } from './canvas-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('Canvas current types preserve optional Parameters and exact Canvas result, while rejecting eleven invalid uses', () => {
const source = fs.readFileSync('test/types/canvas.ts', 'utf8')
for (const [compiler, mode] of modes) {
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${compiler.version} ${mode}`)
assert.equal(checkConsumer(compiler, mode, source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')).length, 11)
}
assert.deepEqual(checkConsumer(ts, 'nodenext-cjs', `import canvas = require('artplayer-proxy-canvas'); import legacy = require('artplayer-proxy-canvas/legacy');
const callback: canvas.Option = (ctx, video) => ctx.drawImage(video, 0, 0); canvas(callback); canvas.default(); legacy(); legacy.default(callback);`), [])
})
test('Canvas published declarations distinguish required 1.0.0 and optional 1.1.0 callbacks without changing Canvas return assignability', async () => {
const contract = await verifyCanvasContract()
const source = fs.readFileSync('refactor/fixtures/consumers/canvas-published.ts', 'utf8')
const optional = `const omitted: Parameters<typeof canvas>[0] = undefined; canvas(); void omitted;`
for (const version of ['1.0.0', '1.1.0']) {
const old = readMember(contract.archives.get(version), 'package/types/artplayer-proxy-canvas.d.ts').toString()
for (const compiler of [ts, compat]) {
const filename = path.resolve('test/types/canvas-history.ts')
const declaration = path.resolve('test/types/canvas-history.d.ts')
function historical(code) {
const host = compiler.createCompilerHost({})
const original = host.getSourceFile.bind(host)
host.getSourceFile = (file, language, ...rest) => path.resolve(file) === filename
? compiler.createSourceFile(file, code.replace('from \'artplayer-proxy-canvas\'', 'from \'./canvas-history\''), language, true)
: path.resolve(file) === declaration ? compiler.createSourceFile(file, old, language, true) : original(file, language, ...rest)
const exists = host.fileExists.bind(host)
host.fileExists = file => path.resolve(file) === declaration || exists(file)
const program = compiler.createProgram([filename, declaration], { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: true, module: compiler.ModuleKind.CommonJS, moduleResolution: compiler.ModuleResolutionKind.NodeJs, target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'] }, host)
assert(program.getSourceFile(declaration))
return compiler.getPreEmitDiagnostics(program).map(item => item.code)
}
assert.deepEqual(historical(source), [])
assert.deepEqual(historical(`${source}\n${optional}`), version === '1.0.0' ? [2322, 2554] : [])
assert.deepEqual(checkConsumer(compiler, 'node10-commonjs', `${source}\n${optional}`), [])
}
}
})
test('Canvas editor globals are generated from public types with optional callback, media view and negative checking', async () => {
const source = fs.readFileSync('packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.ts', 'utf8')
const generated = generatePluginEditorDeclaration(source, 'artplayerProxyCanvas')
const file = 'docs/assets/ts/artplayer-proxy-canvas.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 = `artplayerProxyCanvas(); artplayerProxyCanvas.default((ctx, video) => ctx.drawImage(video, 0, 0));
const result: artplayerProxyCanvas.Result = document.createElement('canvas');
const media = result as artplayerProxyCanvas.MediaCanvas; media.play();
// @ts-expect-error Callback must be a function in public types.
artplayerProxyCanvas(42);`
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, 1)
}
})
test('Canvas production main, legacy, browser global and ESM preserve both callable and historical default identities', async () => {
for (const suffix of ['js', 'legacy.js']) {
const source = fs.readFileSync(`packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.${suffix}`, 'utf8')
const module = { exports: {} }
vm.runInNewContext(source, { module, exports: module.exports, window: {} })
assert.equal(typeof module.exports, 'function')
assert.equal(module.exports.default, module.exports)
assert.equal(typeof module.exports.default(), 'function')
const global = { window: {} }
vm.runInNewContext(source, global)
const factory = global.artplayerProxyCanvas || global.window.artplayerProxyCanvas
assert.equal(factory.default, factory)
}
const source = fs.readFileSync('packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.mjs', 'utf8')
const module = await import(`data:text/javascript,${encodeURIComponent(source)}`)
assert.deepEqual(Object.keys(module), ['default'])
assert.equal(module.default.default, module.default)
})
+6 -2
View File
@@ -3314,11 +3314,15 @@
"ENG-06",
"CORE-07"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "canvas 与媒体能力的精确组合类型,保持 Result",
"acceptance": "严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容",
"evidence": []
"evidence": [
"changes/2026-09-12-PKG-CANVAS-04-types.md",
"baselines/canvas-types-validation.json",
"canvas-validation.md"
]
},
{
"id": "PKG-CANVAS-05",