test(packages): ENG-07 verify installed tarball contracts and browsers

This commit is contained in:
Harvey Zhao committed 2026-09-10 23:04:19 +08:00
1 parent 12b047f9a6
commit a0286c4e95
17 files changed
+702 -9

No files matched your search

+11 -1
View File
@@ -40,6 +40,10 @@ jobs:
yarn install --frozen-lockfile --non-interactive
- name: Install test browsers
run: yarn test:browser:install
- name: Build and check installed tarballs
run: |
yarn test:package
node --input-type=module -e 'import fs from "node:fs"; const { output } = JSON.parse(fs.readFileSync("refactor/.cache/packages/latest.json")); fs.appendFileSync(process.env.GITHUB_ENV, `ARTPLAYER_BROWSER_ARTIFACTS=${output}/browser-artifacts.json\n`);'
- name: Run all three engines
run: yarn test:browser
- name: Upload browser evidence
@@ -47,7 +51,13 @@ jobs:
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: browser-${{ github.run_id }}-${{ github.run_attempt }}
path: refactor/.cache/browser/
path: |
refactor/.cache/browser/
refactor/.cache/packages/latest.json
refactor/.cache/packages/run-*/*.json
refactor/.cache/packages/run-*/*.log
refactor/.cache/packages/run-*/*.tgz
refactor/.cache/packages/run-*/artifacts/
include-hidden-files: true
retention-days: 14
if-no-files-found: warn
+4 -2
View File
@@ -43,7 +43,7 @@
"check:toolchain": "node scripts/check-toolchain.mjs",
"lint:fix": "eslint \"packages/*/{src,types,package.json}\" \"scripts/*.{js,mjs}\" \"test/*\" \"docs/assets/ts/*\" playwright.config.js --fix",
"check:plan": "node refactor/scripts/plan.mjs --check",
"test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js",
"test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js test/package-check.test.js",
"test:baseline": "node --test refactor/scripts/*.test.mjs",
"ci:check": "yarn check:toolchain --strict && yarn check:plan && yarn lint && yarn typecheck && yarn test",
"ci:build": "yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
@@ -53,7 +53,9 @@
"test": "yarn test:node && yarn test:baseline",
"test:browser": "playwright test",
"test:browser:install": "playwright install chromium firefox webkit",
"test:browser:report": "playwright show-report refactor/.cache/browser/html"
"test:browser:report": "playwright show-report refactor/.cache/browser/html",
"test:package": "node scripts/package-check.mjs",
"test:package:release": "node scripts/package-check.mjs --release"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
+2 -1
View File
@@ -2,4 +2,5 @@ node_modules
.DS_Store
yarn.lock
package-lock.json
src
src
tsconfig.json
+2 -1
View File
@@ -2,4 +2,5 @@ node_modules
.DS_Store
yarn.lock
package-lock.json
src
src
tsconfig.json
+1
View File
@@ -45,6 +45,7 @@
| [JS/TS 构建与开发](build-development.md) | 非交互选包、开发重建、资源夹具和 AMD 全局修复 |
| [测试目录维护说明](../test/README.md) | 统一测试命令、JS/TS loader、受控媒体及新旧公共契约 |
| [真实浏览器入口](../test/browser/README.md) | 三浏览器、解码画面与媒体状态、Range/失败服务及候选文件映射 |
| [实际打包消费](../test/package/README.md) | 仓库外安装、运行时/声明检查、安装产物浏览器验证与严格发布检查 |
## 维护规则
+205
View File
@@ -0,0 +1,205 @@
{
"task": "ENG-07",
"capturedAt": "2026-09-10T15:02:29.087Z",
"node": "24.21.0",
"packages": [
{
"name": "artplayer",
"version": "5.4.1",
"archive": "artplayer.tgz",
"sha256": "480a50f7a4a0ba6103523676ac0e94a17df4a779753e40ee6b57be498a6e2eb7",
"files": {
"package/README.md": "9b432fbd3145a33757a93e3fd01b7bd1c3369f51243c5147bca04c6f662b55e1",
"package/dist/artplayer.js": "9ad044a601dafc24f449b690403fb1c343b40e99ffb31043272189fc3c1e7bfa",
"package/dist/artplayer.legacy.js": "71eebcca3192000c58e20f2e3cb76e86c05e33b5a1fab0add20436a5933a3e87",
"package/dist/artplayer.mjs": "ac72f41624383ba853474161affb72b5887644449bf7cb18f6d18edeb2f6f997",
"package/dist/i18n/ar.js": "61c43469ee95a00dc624583f418653e3dcbf48b24ae6a4c203ad986234852365",
"package/dist/i18n/ar.mjs": "68922fdc9f80d3321ea0654c8ff8f5875b6715103d2b14ffcf17f93738b39cf9",
"package/dist/i18n/cs.js": "ad7bbf8853204a6955ef84bfbd932cab3f141a7e73a8145f28ba7a5b2b83b7c0",
"package/dist/i18n/cs.mjs": "29a489be767fda9d46b59c9adbaad323f17a5e67daf7f7aa78b7ce8fb5b4d63c",
"package/dist/i18n/es.js": "aaa95cc9645b734c58fba06fdbcdda962c4cb3a0e7eb6206f06d6168208cd86b",
"package/dist/i18n/es.mjs": "331d2ef74da2eec8f3304fec0b5340c795d9c1eec24e08167c9a7b66bf30973b",
"package/dist/i18n/fa.js": "c998467261cbdfd2c3fb16c0ae4b2c48c409f42f41aea35ed94766f1e12819d5",
"package/dist/i18n/fa.mjs": "b4c98894b46975cebf5ea9088bcf0b5bdfd408bc3c82f4e2e9128133c7de2d4d",
"package/dist/i18n/fr.js": "6b06e16b1fb01c8695fb5c10b32a00421399180de5dc5d845ce17010f2fd3e66",
"package/dist/i18n/fr.mjs": "abe8e3548fa7304f302e2c74818a6176edee8c3b65e33cb6f62e96d130749ee5",
"package/dist/i18n/id.js": "a5832c4bfc03c7f16418bf4b324d9773895541dc823ece9114741794737aad74",
"package/dist/i18n/id.mjs": "a40a433be6ca1ac1ee1810fe805ee60784d349e59b9ad82ea80201d660db4e68",
"package/dist/i18n/pl.js": "2491487062bdfc91d60f17c6e9a83ee196c991216d15f525fe3536b98873da2c",
"package/dist/i18n/pl.mjs": "ad8ea2c03785edbfe1f35761bec21b8a04213f9fa88a794d850bb3973564930f",
"package/dist/i18n/ru.js": "2d3a9ee6533cb6fa961f0a24a25ce63cc76992fca17e1769b6fa75104f8a1529",
"package/dist/i18n/ru.mjs": "a89b8c36cfd7c181c2b31aa823deda3d7c37b56b2b81746338c52ea1c1d93352",
"package/dist/i18n/tr.js": "438b06a4c3f479a2901b0085eaa7bdba8eaef87bdb07ff3faa559c85403fe433",
"package/dist/i18n/tr.mjs": "cc96f9cbb9c1587d5cd8f25b0b9800678ad76e93de089ae42c38dee16e6fe8fa",
"package/dist/i18n/vi.js": "1ffbd62843a902ea2967eab46432d3fc548f5633e53eed104314b523cacc46a8",
"package/dist/i18n/vi.mjs": "7872ded84622bcda20e8c1d70c597c2ba071b50701900efa47cc4683ee17f1a2",
"package/dist/i18n/zh-tw.js": "2d43577f98456488196921e1ec721695a95e1b8c827077286b02ce3fb987e8ff",
"package/dist/i18n/zh-tw.mjs": "7ec638b4babe59d3ab1d3e64abdd74a96fefb260a50e2cd7240f8d50a1340925",
"package/package.json": "41f55f66658b99a7915f42c79b16c1869329d57e5729b16fec74d7b6568a91b8",
"package/types/artplayer.d.ts": "98d3e7b65f9f81fba155da08fd47e4fb25f4c69279e3afd993c4a55070f8a850",
"package/types/component.d.ts": "facfa29d9092fe2c63a7d8357ce7d0faa5e32811d2fedeef6dc8b28ad5d04891",
"package/types/config.d.ts": "c4fd8681bf32b10c743e3e561c532520c8f32e43fd765524850b15e55cb91c72",
"package/types/cssVar.d.ts": "e027d82fb4f01c9ce7559cf679b2d5ee1962d3f7e5b4dd64ca21f472b933efb6",
"package/types/events.d.ts": "98c74aebb82455accc0b3523af5a1145bc9119da58c884200adc36e70a164b02",
"package/types/i18n.d.ts": "d6c01cb758d365d46d0f3d36687e97d6283a272bea7d8052aedb0cf6494e82a4",
"package/types/icons.d.ts": "0d8e8e52e52130dd4a29bab551dd7a2a954797e42e92a6e068c85d583e23e6a4",
"package/types/option.d.ts": "d4de53930c2fa9d59c77c4324d63f24b057bce6c4d0273c34a0ab06094dcea2c",
"package/types/player.d.ts": "986382bb6b4e82746fd2405092e69d0ef413988887a557a0fd4db85d64b78d54",
"package/types/quality.d.ts": "7ed6ed78cea52991b2f6ddd2bd3b7bd8c5146fdfff7486fe8b814c3e632f1567",
"package/types/setting.d.ts": "d12a75b656fff70cbb30be4bffec46d9c4273636e8722bce971a4d2b188712a1",
"package/types/subtitle.d.ts": "d25b470e47d6a9bd696d9c1ad8adb58e56a208f05695186d513bc8c43c2e8acf",
"package/types/template.d.ts": "68aed9e97d006d6be4fabb8504ca84dafab770b0925f4d6e7319e67c82f21b0d",
"package/types/utils.d.ts": "d3fb67b9187e1c81c6b3850694cf2627e927225ace160ae58c11631e9f673a98"
}
},
{
"name": "artplayer-plugin-chapter",
"version": "1.1.0",
"archive": "artplayer-plugin-chapter.tgz",
"sha256": "804d0c7229bc976399c8a6c8884c0b479bce2cecd87a92d05b2053cb9efebafa",
"files": {
"package/README.md": "07fd441f979179f29096ce082b2d4b55489681be26c84d53070e5037a145f60d",
"package/dist/artplayer-plugin-chapter.js": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0",
"package/dist/artplayer-plugin-chapter.legacy.js": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0",
"package/dist/artplayer-plugin-chapter.mjs": "61342a069b84e0731d44a2464d0dfe637d1514d0037c740371fc866c516d2209",
"package/package.json": "0fd20d82a9ddec642da8b26467b45c6c3e443f188ed75e7de3004e48efd4f766",
"package/types/artplayer-plugin-chapter.d.ts": "80338946aea05020abd285daca133fddfc81bd8b800345b2c76548fb93a555a9"
}
}
],
"runtimeChecks": [
"DIST.cjs",
"DIST.cjs-legacy",
"DIST.esm",
"DIST.esm-default-only",
"DIST.esm-legacy",
"DIST.global-core",
"DIST.amd-core",
"DIST.global-core-legacy",
"DIST.amd-core-legacy",
"DIST.global-chapter",
"DIST.amd-chapter",
"DIST.global-chapter-legacy",
"DIST.amd-chapter-legacy",
"SSR.import-template",
"SSR.constructor-browser-only",
"DIST.i18n-cjs-esm-global",
"DIST.exports-boundary",
"DIST.direct-esm-file",
"EVENT.chain-context-arguments",
"EVENT.once-reentry",
"EVENT.dispatch-snapshot",
"EVENT.off-original-once",
"EVENT.throw-propagation"
],
"types": [
{
"compiler": "5.9.3",
"mode": "node10-commonjs",
"diagnostics": [
{
"file": "test/types/legacy-plugin.ts",
"code": 2307,
"line": 1,
"message": "Cannot find module 'artplayer-plugin-chapter/legacy' or its corresponding type declarations.\n There are types at '<workspace>/packages/artplayer-plugin-chapter/types/artplayer-plugin-chapter.d.ts', but this result could not be resolved under your current 'moduleResolution' setting. Consider updating to 'node16', 'nodenext', or 'bundler'."
}
]
},
{
"compiler": "5.9.3",
"mode": "nodenext-cjs",
"diagnostics": []
},
{
"compiler": "5.9.3",
"mode": "bundler-esm",
"diagnostics": []
},
{
"compiler": "4.3.5",
"mode": "node10-commonjs",
"diagnostics": [
{
"file": "test/types/legacy-plugin.ts",
"code": 2307,
"line": 1,
"message": "Cannot find module 'artplayer-plugin-chapter/legacy' or its corresponding type declarations."
}
]
},
{
"compiler": "5.9.3",
"mode": "nodenext-esm",
"diagnostics": [
{
"file": "test/types/language.ts",
"code": 2351,
"line": 3,
"message": "This expression is not constructable.\n Type 'typeof import(\"<workspace>/packages/artplayer/types/artplayer\")' has no construct signatures."
},
{
"file": "test/types/legacy-plugin.ts",
"code": 2349,
"line": 2,
"message": "This expression is not callable.\n Type 'typeof import(\"<workspace>/packages/artplayer-plugin-chapter/types/artplayer-plugin-chapter\")' has no call signatures."
},
{
"file": "test/types/consumer.ts",
"code": 2349,
"line": 8,
"message": "This expression is not callable.\n Type 'typeof import(\"<workspace>/packages/artplayer-plugin-chapter/types/artplayer-plugin-chapter\")' has no call signatures."
},
{
"file": "test/types/consumer.ts",
"code": 2351,
"line": 10,
"message": "This expression is not constructable.\n Type 'typeof import(\"<workspace>/packages/artplayer/types/artplayer\")' has no construct signatures."
},
{
"file": "test/types/consumer.ts",
"code": 7006,
"line": 10,
"message": "Parameter 'player' implicitly has an 'any' type."
},
{
"file": "test/types/consumer.ts",
"code": 2709,
"line": 12,
"message": "Cannot use namespace 'Artplayer' as a type."
},
{
"file": "test/types/consumer.ts",
"code": 2683,
"line": 12,
"message": "'this' implicitly has type 'any' because it does not have a type annotation."
},
{
"file": "test/types/consumer.ts",
"code": 2339,
"line": 17,
"message": "Property 'html' does not exist on type 'typeof import(\"<workspace>/packages/artplayer/types/artplayer\")'."
},
{
"file": "test/types/consumer.ts",
"code": 2349,
"line": 18,
"message": "This expression is not callable.\n Type 'typeof import(\"<workspace>/packages/artplayer-plugin-chapter/types/artplayer-plugin-chapter\")' has no call signatures."
}
]
}
],
"knownTypeBlockers": 11,
"strictReleaseExit": 1,
"browser": {
"startTime": "2026-09-10T15:02:29.838Z",
"duration": 21788.194,
"expected": 39,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"browserArtifactSource": "refactor/.cache/packages/run-1AHowo/browser-artifacts.json",
"ciChecks": 57,
"negativeTests": 2,
"remoteCI": "not-run"
}
@@ -0,0 +1,35 @@
# ENG-07:安装实际 tarball 后验证
为核心和 chapter 建立 [打包消费者入口](../../test/package/README.md)。使用仓库构建脚本在
独立快照重建三格式和语言文件,Yarn pack 后在仓库外临时目录真实离线安装、frozen 重装。
逐文件核对 tarball 与安装内容 SHA;运行时与声明不能解析回 workspace 源码。
检查 main/module/legacy/types/exports、历史路径、默认导出、SSR、语言和 Emitter 契约。
包内新加的 tsconfig 是实现配置,已从这两个 npm 包中排除,不改变公开入口。
新增 `yarn test:package`、`yarn test:package:release`,不新增依赖。
CI 的 macOS 三引擎作业先构建并安装 tarball,再将安装后的候选文件交给浏览器;报告上传
排除带 node_modules 链接的构建快照。远端作业尚未执行,actionlint 本地通过。
## 验证
- 实际安装后的 23 项运行时检查通过,公开属性形状/描述符与默认配置对照通过。
- 五组 TS 组合中两组无诊断;其余精确匹配 11 条已知历史诊断。它们归 BASE-TYPE-01/03,
不是发布豁免;`test:package:release` 如预期以 1 退出并保存报告,等待 chapter 04/core 类型任务修正。
- 缺文件、缺通配导出、内部配置泄漏、删旧路径及实际移除 default/入口的两个反向测试通过。
- `yarn ci:check`:57 项 Node/基线测试通过;严格类型、lint、工具链及计划检查通过。
- 安装产物三浏览器回归 39 项通过,无重试或跳过;Chromium 153.0.8010.12、Firefox 155.0、
WebKit 26.6。包括发布核心配候选插件、真实解码/播放/seek/切源/销毁及章节交互。
- 指纹与诊断摘要见 [执行记录](../baselines/package-validation.json)。完整日志和 tarball 在
refactor/.cache/packages,浏览器证据在 refactor/.cache/browser;缓存不作为长期发布凭据。
发现并修正测试 lint 自动将 node:test 改写到未安装 Vitest 的问题,按现有 Node 测试约定
保留局部规则说明,没有安装第二套 runner。
## 边界与接续
这一步提供可复用的工程入口,初始实际包覆盖只有 core/chapter,不宣称所有插件已验证。
其他包随迁移增加独立消费者。API 形状检查不能替代行为测试;docs 完整编辑器、真机/SDK 和
远端 CI 仍由原任务负责。没有改生产运行时或发布版本,没有推送/发布。
下一项 PKG-CHAPTER-03 直接使用 TS 拆分区间、DOM 与生命周期,04 再完成公开声明和消费者修正,
不先进行一轮重复的 JS 拆分。回退使用本任务独立提交的 revert,已冻结发布基线保持不变。
+5
View File
@@ -31,6 +31,11 @@ Actions 固定完整 SHA,Node 来自 .node-version,Yarn 固定 1.22.22;安
## Pages 隔离与启用条件
ENG-07 将浏览器候选改为 `yarn test:package` 实际打包并安装后的文件,使用输出映射禁止
回退到源码。安装/构建日志、tarball、成员指纹和浏览器结果一并上传,构建快照的 node_modules
链接不上传。初始包范围为 core/chapter;精确历史类型诊断仍阻止严格发布检查通过。
见 [消费者说明](../test/package/README.md)。本地已验证,远端执行仍待 CI-04。
.github/workflows/pages.yml 仅支持手动运行,限定 master,且仓库变量 PAGES_DEPLOY_ENABLED 必须为 true。初始启用状态未经远端核实;没有修改该变量或其他 GitHub 设置。保留此门槛用于 CI-02 完成域名、路径和远端配置验收后再启用,不能直接自动部署 PR 产物。
Pages 先调用同一检查/构建 workflow,成功后上传同一次运行生成的完整 docs artifact,再在 github-pages 环境的独立作业调用官方 deploy-pages。仅 deploy 作业有 pages: write / id-token: write,没有 Git contents 写权限,也没有原先的 gh-pages force push。发布排队执行,不取消正在执行的发布。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 214 项,范围 22 个包及工作区/示例。
状态:todo 183 / doing 0 / blocked 0 / done 31 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 182 / doing 0 / blocked 0 / done 32 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -80,7 +80,7 @@
| ENG-04 | workspace<br>建立类型测试基础 | ENG-02, BASE-05 | 根与分包 tsconfig、显式 TS 依赖、正反例测试 | 核心/试点与迁移模块严格检查,未迁移第三方/包历史问题独立台账;明确最低/当前 TS 和各环境类型;复用 BASE-05 的四种消费模式,不以 skipLibCheck 掩盖 BASE-TYPE-01/03 | M | done |
| ENG-05 | workspace<br>建立真实浏览器测试服务 | ENG-03, BASE-04, BASE-08 | Playwright projects、本地 Range/失败媒体服务;复用 docs 页面/样本的状态隔离、错误采集与候选资源映射 | Chromium/Firefox/WebKit 的基础播放 smoke 和报告可执行;以媒体状态断言判定通过,区分轻量用例与真实编辑器交互 | M | done |
| ENG-06 | workspace<br>支持按包非交互与 JS/TS 构建 | ENG-02, BASE-05 | build/dev 入口解析、指定包参数、原交互保留 | 三种产物、Less/SVG/worker 和本地 8082 demo 正常;保持 BASE-05 的 AMD 同时写入全局行为及 i18n/legacy 入口 | H | done |
| ENG-07 | workspace<br>建立 tarball 消费与产物检查 | ENG-04, ENG-06 | 隔离 npm 消费 fixtures、API/声明/入口差分 | 不借 workspace 源码通过,能识别缺文件与默认导出变化 | H | todo |
| ENG-07 | workspace<br>建立 tarball 消费与产物检查 | ENG-04, ENG-06 | 隔离 npm 消费 fixtures、API/声明/入口差分 | 不借 workspace 源码通过,能识别缺文件与默认导出变化 | H | done |
| ENG-08 | workspace<br>增加覆盖率、资源与性能报告 | ENG-03, ENG-05, BASE-06 | 覆盖率基线、资源清理断言、性能报告与阈值 | 关键生命周期分支有门槛,报告不靠无意义断言堆数量;将 BASE-06 的同环境多组配对、计时/压缩审查阈值与资源异常分开接入候选,不把历史现象冻结成正常要求 | M | todo |
| ENG-09 | workspace<br>建立全包依赖影响和文档检查 | ENG-07, DOC-04 | 共享核心/构建影响映射、文档及每任务完成提交的 Git 审计接入;契约-支持版本-测试 ID-命令-候选/报告-任务的覆盖索引 | 核心变化触发必需生态检查;原有 DOC-01 至 04 基线例外明确,后续 done 任务不能缺失独立 commit;计划/已执行/缺证据分开,识别缺少验证归属的公开契约 | M | todo |
| ENG-10 | workspace<br>建立历史失败分级和测试可靠性规则 | ENG-03, ENG-04, ENG-05, ENG-07, BASE-07 | 历史失败 ID/环境/旧版复现/负责修复任务、逐模块门槛、受控等待与 trace/retry 规则 | 不靠全局忽略或无理由 skip 隐藏问题,新增回归阻止交付,设备缺口和偶发失败单独可见;以 risks.json 为统一差异索引;关闭必须有 resolutionEvidence/rationale,已复现、源码事实、未验证分开,登记不等于豁免 | M | todo |
@@ -432,5 +432,6 @@
- ENG-04: [记录](changes/2026-09-10-ENG-04-typechecking.md) [记录](typechecking.md)
- ENG-05: [记录](changes/2026-09-10-ENG-05-browser-tests.md) [记录](baselines/browser-validation.json)
- ENG-06: [记录](changes/2026-09-10-ENG-06-build-development.md) [记录](build-development.md) [记录](baselines/build-validation.json)
- ENG-07: [记录](changes/2026-09-10-ENG-07-package-consumers.md) [记录](baselines/package-validation.json)
- PKG-CHAPTER-01: [记录](changes/2026-09-10-PKG-CHAPTER-01-contract.md) [记录](baselines/chapter-contract.md)
- PKG-CHAPTER-02: [记录](changes/2026-09-10-PKG-CHAPTER-02-tests.md) [记录](baselines/chapter-validation.json)
+4
View File
@@ -1,5 +1,9 @@
# 进度与证据
## 当前实施:ENG-07 打包消费入口完成
实际重建、打包、仓库外安装及 23 项运行时检查通过;安装产物三浏览器 39 项通过,ci:check 57 项通过。精确的 11 条历史类型诊断仍阻止严格发布检查,未作为发布豁免。见 [交付](changes/2026-09-10-ENG-07-package-consumers.md)。当前 214 项,32 完成、182 待办;生产源码 TS 迁移仍为零。下一项直接实施 PKG-CHAPTER-03 的 TS 模块拆分,再以 04 完成公开类型和消费者闭环。
## 当前实施:PKG-CHAPTER-02 测试与声明修正完成
chapter 27 项真实浏览器回归通过,合并原 smoke 共 39 项;工厂 option 声明已扩展为可选并重新生成编辑器声明,新旧 TS 正反例通过,BASE-TYPE-02 关闭。见 [交付](changes/2026-09-10-PKG-CHAPTER-02-tests.md)。当前 214 项,31 完成、183 待办。PKG-CHAPTER-01 提交 70174952,本任务单独提交;下一项 ENG-07,随后直接以 TS 拆分 chapter 源码。运行时源码尚未迁移。
+5 -2
View File
@@ -573,11 +573,14 @@
"ENG-04",
"ENG-06"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "隔离 npm 消费 fixtures、API/声明/入口差分",
"acceptance": "不借 workspace 源码通过,能识别缺文件与默认导出变化",
"evidence": []
"evidence": [
"changes/2026-09-10-ENG-07-package-consumers.md",
"baselines/package-validation.json"
]
},
{
"id": "ENG-08",
+140
View File
@@ -0,0 +1,140 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { ensureArchive, hash, readMember } from '../refactor/scripts/releases.mjs'
import { consumerDirectory, names, readJson, removeConsumer, run, runtimeConsumer, typeConsumers, workspace, writeJson } from './package-consumer.mjs'
export function checkFiles(manifest, files, historical = []) {
for (const field of ['main', 'module', 'types', 'legacy']) {
assert(typeof manifest[field] === 'string', `Missing ${field}: ${manifest.name}`)
assert(files.includes(`package/${manifest[field].replace(/^\.\//, '')}`), `Missing ${field} file: ${manifest.name}`)
}
function visit(value) {
if (typeof value === 'string') {
assert(value.startsWith('./') && !value.includes('..'), 'Invalid export target')
const pattern = new RegExp(`^package/${value.slice(2).split('*').map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.+')}\$`)
assert(files.some(file => pattern.test(file)), `Missing export target: ${value}`)
}
else if (value && typeof value === 'object') {
for (const child of Object.values(value)) visit(child)
}
}
visit(manifest.exports)
for (const file of historical.filter(file => /^package\/(?:dist|types)\//.test(file)))
assert(files.includes(file), `Historical distribution file removed: ${file}`)
assert(!files.some(file => /^package\/(?:src|node_modules)\//.test(file)), 'Source/dependencies leaked into package')
assert(!files.includes('package/tsconfig.json'), 'Implementation tsconfig leaked into package')
}
export function packedFiles(archive) {
const args = { encoding: 'utf8', windowsHide: true }
const names = execFileSync('tar', ['-tzf', archive], args).trim().split(/\r?\n/)
const entries = execFileSync('tar', ['-tvzf', archive], args).trim().split(/\r?\n/)
assert.equal(names.length, entries.length, 'Archive listing mismatch')
assert.equal(new Set(names).size, names.length, 'Duplicate archive members')
return names.filter((name, index) => {
const type = entries[index][0]
assert(['d', '-'].includes(type), 'Package links and special files are unsupported')
assert((type === 'd' && name === 'package') || name.startsWith('package/'), 'Invalid package root')
assert(!name.includes('\\') && !name.split('/').includes('..'), 'Unsafe package member')
return type === '-'
}).sort()
}
export async function publishedConsumer() {
const dir = consumerDirectory()
const releases = readJson(path.join(workspace, 'refactor/baselines/releases.json')).releases
try {
for (const release of releases) {
const archive = await ensureArchive(release)
for (const [member, digest] of Object.entries(release.files)) {
const bytes = readMember(archive, member)
assert.equal(hash(bytes), digest)
const file = path.join(dir, 'node_modules', release.name, member.slice(8))
fs.mkdirSync(path.dirname(file), { recursive: true })
fs.writeFileSync(file, bytes)
}
}
return { dir, releases }
}
catch (error) {
removeConsumer(dir)
throw error
}
}
export async function checkPackages({ release = false } = {}) {
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Run yarn test:package with the pinned Yarn')
const yarn = process.env.npm_execpath
assert(yarn && fs.existsSync(yarn), 'Missing Yarn executable')
const parent = path.join(workspace, 'refactor/.cache/packages')
fs.mkdirSync(parent, { recursive: true })
const output = fs.mkdtempSync(path.join(parent, 'run-'))
const snapshot = path.join(output, 'build')
fs.mkdirSync(snapshot)
for (const file of ['package.json', 'yarn.lock']) fs.copyFileSync(path.join(workspace, file), path.join(snapshot, file))
fs.cpSync(path.join(workspace, 'scripts'), path.join(snapshot, 'scripts'), { recursive: true })
fs.symlinkSync(path.join(workspace, 'node_modules'), path.join(snapshot, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir')
for (const name of names) {
const source = path.join(workspace, 'packages', name)
fs.cpSync(source, path.join(snapshot, 'packages', name), { recursive: true, filter: file => !['dist', 'node_modules'].includes(path.basename(file)) })
}
fs.writeFileSync(path.join(output, 'build.log'), run(['scripts/build.js', ...names], snapshot) + run(['scripts/build-i18n.js'], snapshot))
const baseline = await publishedConsumer()
const installed = consumerDirectory()
try {
const oldRuntime = runtimeConsumer(baseline.dir)
const packages = []
for (const name of names) {
const archive = path.join(output, `${name}.tgz`)
fs.writeFileSync(path.join(output, `${name}-pack.log`), run([yarn, 'pack', '--filename', archive], path.join(snapshot, 'packages', name)))
const files = packedFiles(archive)
const manifest = JSON.parse(readMember(archive, 'package/package.json'))
assert.equal(manifest.name, name)
assert(!['preinstall', 'install', 'postinstall'].some(hook => manifest.scripts?.[hook]), 'Add lifecycle fixtures before accepting install hooks')
checkFiles(manifest, files, Object.keys(baseline.releases.find(r => r.name === name).files))
packages.push({ name, version: manifest.version, archive: path.basename(archive), sha256: hash(fs.readFileSync(archive)), files: Object.fromEntries(files.map(file => [file, hash(readMember(archive, file))])) })
}
writeJson(path.join(installed, 'package.json'), { private: true, name: 'artplayer-isolated-consumer', dependencies: Object.fromEntries(packages.map(pkg => [pkg.name, `file:${path.join(output, pkg.archive).replaceAll('\\', '/')}`])) })
fs.copyFileSync(path.join(workspace, 'yarn.lock'), path.join(installed, 'yarn.lock'))
fs.writeFileSync(path.join(output, 'install.log'), run([yarn, 'install', '--offline', '--ignore-scripts', '--non-interactive'], installed))
const lock = fs.readFileSync(path.join(installed, 'yarn.lock'))
fs.writeFileSync(path.join(output, 'frozen-install.log'), run([yarn, 'install', '--offline', '--frozen-lockfile', '--ignore-scripts', '--non-interactive'], installed))
assert.deepEqual(fs.readFileSync(path.join(installed, 'yarn.lock')), lock, 'Frozen consumer lock changed')
fs.writeFileSync(path.join(output, 'consumer-yarn.lock'), lock)
const artifacts = {}
for (const pkg of packages) {
const root = path.join(installed, 'node_modules', pkg.name)
assert.equal(fs.realpathSync(root), root, 'Installed package must not be a workspace link')
for (const [member, digest] of Object.entries(pkg.files))
assert.equal(hash(fs.readFileSync(path.join(root, member.slice(8)))), digest, `Installed bytes differ: ${member}`)
fs.cpSync(root, path.join(output, 'artifacts', pkg.name), { recursive: true })
artifacts[pkg.name] = `./artifacts/${pkg.name}/dist/${pkg.name}.js`
}
const runtime = runtimeConsumer(installed)
assert.deepEqual(runtime.observations.api, oldRuntime.observations.api, 'Published API shape/defaults changed')
const types = typeConsumers(installed)
const report = { task: 'ENG-07', capturedAt: new Date().toISOString(), node: process.versions.node, packages, runtime, publishedRuntime: oldRuntime, types, knownTypeBlockers: types.reduce((sum, result) => sum + result.diagnostics.length, 0) }
writeJson(path.join(output, 'report.json'), report)
writeJson(path.join(output, 'browser-artifacts.json'), artifacts)
writeJson(path.join(parent, 'latest.json'), { output: path.relative(workspace, output).replaceAll('\\', '/') })
console.log(`Installed tarball contracts passed: ${runtime.checks.length} runtime checks; ${types.filter(t => !t.diagnostics.length).length}/${types.length} type modes. Report: ${output}`)
if (release)
assert.equal(report.knownTypeBlockers, 0, 'Known type blockers remain; this candidate is not release-ready')
return report
}
finally {
removeConsumer(installed)
removeConsumer(baseline.dir)
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
checkPackages({ release: process.argv.includes('--release') }).catch((error) => {
console.error(error)
process.exitCode = 1
})
}
+90
View File
@@ -0,0 +1,90 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import ts from 'typescript'
import compat from 'typescript-compat'
export const workspace = fileURLToPath(new URL('../', import.meta.url))
export const names = ['artplayer', 'artplayer-plugin-chapter']
export const readJson = file => JSON.parse(fs.readFileSync(file, 'utf8'))
export const writeJson = (file, data) => fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`)
export const run = (args, cwd) => execFileSync(process.execPath, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 120000, maxBuffer: 8 * 1024 * 1024, windowsHide: true, env: { ...process.env, NODE_PATH: '' } })
export function consumerDirectory() {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'artplayer-consumer-'))
assert(!path.resolve(dir).startsWith(path.resolve(workspace)), 'Consumer must be outside workspace')
return dir
}
export function removeConsumer(dir) {
assert.equal(path.dirname(fs.realpathSync(dir)), fs.realpathSync(os.tmpdir()))
assert(path.basename(dir).startsWith('artplayer-consumer-'), 'Refusing unrelated temporary cleanup')
fs.rmSync(dir, { recursive: true, force: true })
}
export function runtimeConsumer(dir) {
fs.copyFileSync(path.join(workspace, 'test/package/runtime.cjs'), path.join(dir, 'runtime.cjs'))
fs.copyFileSync(path.join(workspace, 'test/contracts/emitter.js'), path.join(dir, 'emitter.mjs'))
writeJson(path.join(dir, 'expected.json'), { version: readJson(path.join(dir, 'node_modules/artplayer/package.json')).version })
return JSON.parse(run(['runtime.cjs'], dir))
}
export function typeConsumers(dir) {
const results = []
for (const [compiler, mode] of [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'bundler-esm'], [compat, 'node10-commonjs'], [ts, 'nodenext-esm']]) {
const next = mode.startsWith('nodenext')
const extension = next ? (mode.endsWith('-cjs') ? 'cts' : 'mts') : 'ts'
const inputs = [...(mode === 'nodenext-esm' ? ['public'] : ['public', 'chapter-options']), 'language', 'legacy-plugin']
const files = inputs.map((name) => {
const file = path.join(dir, `${name}.${extension}`)
const folder = ['language', 'legacy-plugin'].includes(name) ? 'refactor/fixtures/consumers' : 'test/types'
fs.copyFileSync(path.join(workspace, folder, `${name}.ts`), file)
return file
})
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,
}
const program = compiler.createProgram(files, options)
const libDir = path.dirname(compiler.sys.getExecutingFilePath())
for (const file of program.getSourceFiles()) {
const actual = fs.realpathSync(file.fileName)
assert(actual.startsWith(fs.realpathSync(dir) + path.sep)
|| (path.dirname(actual) === fs.realpathSync(libDir) && /^lib\..*\.d\.ts$/.test(path.basename(actual))), `Type resolution escaped consumer: ${file.fileName}`)
}
const diagnostics = compiler.getPreEmitDiagnostics(program).map(d => ({
file: d.file ? `test/types/${path.basename(d.file.fileName).replace(/\.[cm]ts$/, '.ts').replace(/^public\.ts$/, 'consumer.ts')}` : null,
code: d.code,
line: d.file && d.start !== undefined ? d.file.getLineAndCharacterOfPosition(d.start).line + 1 : null,
message: compiler.flattenDiagnosticMessageText(d.messageText, '\n').replaceAll(`${dir.replaceAll('\\', '/')}/node_modules/`, '<workspace>/packages/'),
}))
const expected = mode === 'nodenext-esm' ? readJson(path.join(workspace, 'test/types/known-diagnostics.json')).diagnostics : []
const historical = readJson(path.join(workspace, 'refactor/baselines/consumers.json')).types
const known = readJson(path.join(workspace, 'test/package/known-types.json')).cases
for (const fixture of ['language.ts', 'legacy-plugin.ts']) {
if (!known.some(item => item.fixture === fixture && item.mode === mode))
continue
const original = historical.find(result => result.mode === mode && result.fixture === fixture)
for (const diagnostic of original.diagnostics) {
expected.push({ ...diagnostic, file: `test/types/${fixture}`, message: compiler === compat
? diagnostic.message.split('\n')[0]
: diagnostic.message.replaceAll('<consumer>/node_modules/', '<workspace>/packages/') })
}
}
const ordered = values => [...values].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.code - b.code)
assert.deepEqual(ordered(diagnostics), ordered(expected), `Packed type regression: TS ${compiler.version} ${mode}`)
results.push({ compiler: compiler.version, mode, diagnostics, declarations: program.getSourceFiles().filter(f => f.fileName.includes('/node_modules/')).map(f => path.relative(dir, f.fileName).replaceAll('\\', '/')).sort() })
}
return results
}
+35
View File
@@ -0,0 +1,35 @@
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 -- This fixture exercises the repository's Node test runner.
import { test } from 'node:test'
import { checkFiles, publishedConsumer } from '../scripts/package-check.mjs'
import { removeConsumer, runtimeConsumer } from '../scripts/package-consumer.mjs'
test('Package checks reject missing files, missing wildcard exports and internal configuration', () => {
const manifest = { name: 'fixture', main: './dist/main.js', module: './dist/main.mjs', types: './types/main.d.ts', legacy: './dist/legacy.js', exports: { '.': './dist/main.js', './lang/*': './dist/lang/*.js' } }
const files = ['package/dist/main.js', 'package/dist/main.mjs', 'package/types/main.d.ts', 'package/dist/legacy.js', 'package/dist/lang/fr.js']
checkFiles(manifest, files)
for (const missing of files)
assert.throws(() => checkFiles(manifest, files.filter(file => file !== missing)), /Missing/)
assert.throws(() => checkFiles(manifest, [...files, 'package/tsconfig.json']), /tsconfig/)
assert.throws(() => checkFiles(manifest, files, ['package/dist/old.js']), /Historical/)
})
test('Actual isolated runtime rejects removed default exports and required files', async () => {
const { dir } = await publishedConsumer()
try {
assert.equal(runtimeConsumer(dir).checks.length, 23)
const esm = path.join(dir, 'node_modules/artplayer-plugin-chapter/dist/artplayer-plugin-chapter.mjs')
const original = fs.readFileSync(esm)
fs.writeFileSync(esm, 'export const removedDefault = true\n')
assert.throws(() => runtimeConsumer(dir), /Command failed/)
fs.writeFileSync(esm, original)
const entry = path.join(dir, 'node_modules/artplayer-plugin-chapter/dist/artplayer-plugin-chapter.js')
fs.unlinkSync(entry)
assert.throws(() => runtimeConsumer(dir), /Command failed/)
}
finally {
removeConsumer(dir)
}
})
+44
View File
@@ -0,0 +1,44 @@
# Installed package checks
Run `yarn test:package` with the pinned Node and Yarn after a frozen install. The
initial scope is core and chapter; extend `names` in `scripts/package-consumer.mjs`
and add package-specific consumers when migrating another package.
`scripts/package-check.mjs` copies source into an ignored build snapshot, rebuilds
all three formats and core languages using repository scripts, and runs Yarn pack.
It checks manifest targets, historical distribution files, archive members and
unexpected source/configuration files. It then installs the tarballs in a fresh
OS temporary directory outside the repository, using the root lock to resolve
runtime dependencies offline, and repeats installation with a frozen consumer lock.
Install hooks are disabled; packages with such hooks are rejected until covered.
Only the build snapshot links workspace build tools. The consumer has no workspace
package links, `NODE_PATH` is cleared, installed files must match archive hashes,
and compiler inputs may only resolve inside that consumer or the pinned compiler's
standard library. Temporary consumers are cleaned up after success or failure.
`runtime.cjs` tests CJS, ESM, UMD, AMD, legacy and language entrypoints, SSR import,
export restrictions and shared Emitter behavior. Public property descriptors and
default configuration are compared against the fixed published core/chapter.
This is a targeted compatibility check, not a complete behavioral API comparison.
Public, chapter-options, language and legacy-plugin TypeScript consumers run with
TS 5.9.3 (Node10, NodeNext CJS/ESM and Bundler) and TS 4.3.5 (Node10). Exact known
diagnostics are referenced by `known-types.json` and `test/types/known-diagnostics.json`.
Unexpected errors and unexpectedly removed errors both require investigation.
Remove a known case when its owning task fixes it; keep frozen release evidence intact.
`yarn test:package:release` additionally rejects any remaining known type errors.
Passing this command alone is not authorization or sufficient evidence to publish.
Reports, archives, installed artifact copies and build/install logs are under
`refactor/.cache/packages/run-*`; `latest.json` points to the last successful
compatibility run (a strict release rejection still leaves its report). For browser
validation set `ARTPLAYER_BROWSER_ARTIFACTS` to that run's `browser-artifacts.json`
and run `yarn test:browser`. The service then uses installed package bytes without
falling back to source. CI performs this sequence and uploads only reports and
artifacts, excluding the build snapshot's node_modules link.
`test/package-check.test.js` verifies missing targets, leaked configuration and
removed historical files, then deliberately removes an actual published default
export and required entry in an isolated consumer to prove runtime failures are
detected. It is part of `yarn test:node`.
+8
View File
@@ -0,0 +1,8 @@
{
"note": "Exact published diagnostic references, not release waivers. Remove a case only with a verified fix; never edit the frozen baseline to accept a candidate regression.",
"cases": [
{ "mode": "node10-commonjs", "fixture": "legacy-plugin.ts", "risk": "BASE-TYPE-03" },
{ "mode": "nodenext-esm", "fixture": "legacy-plugin.ts", "risk": "BASE-TYPE-01" },
{ "mode": "nodenext-esm", "fixture": "language.ts", "risk": "BASE-TYPE-01" }
]
}
+108
View File
@@ -0,0 +1,108 @@
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const process = require('node:process')
const { pathToFileURL } = require('node:url')
const vm = require('node:vm');
(async () => {
const checks = []
const observations = {}
const check = (id, value) => {
assert(value, id)
checks.push(id)
}
const root = __dirname
const load = relative => fs.readFileSync(path.join(root, 'node_modules', relative), 'utf8')
const core = require('artplayer')
const chapter = require('artplayer-plugin-chapter')
check('DIST.cjs', typeof core === 'function' && core.version === JSON.parse(fs.readFileSync(path.join(root, 'expected.json'))).version && typeof chapter({}) === 'function')
check('DIST.cjs-legacy', require('artplayer/legacy').version === core.version && typeof require('artplayer-plugin-chapter/legacy')({}) === 'function')
const esm = await import('artplayer')
const pluginEsm = await import('artplayer-plugin-chapter')
check('DIST.esm', esm.default.version === core.version && typeof pluginEsm.default({}) === 'function')
check('DIST.esm-default-only', Object.keys(esm).join() === 'default' && Object.keys(pluginEsm).join() === 'default')
const legacy = await import('artplayer/legacy')
const pluginLegacy = await import('artplayer-plugin-chapter/legacy')
check('DIST.esm-legacy', legacy.default.version === core.version && typeof pluginLegacy.default({}) === 'function')
for (const [name, filename, globalName] of [
['core', 'artplayer/dist/artplayer.js', 'Artplayer'],
['core-legacy', 'artplayer/dist/artplayer.legacy.js', 'Artplayer'],
['chapter', 'artplayer-plugin-chapter/dist/artplayer-plugin-chapter.js', 'artplayerPluginChapter'],
['chapter-legacy', 'artplayer-plugin-chapter/dist/artplayer-plugin-chapter.legacy.js', 'artplayerPluginChapter'],
]) {
const code = load(filename)
const global = vm.createContext({ console })
vm.runInContext(code, global, { timeout: 2000 })
check(`DIST.global-${name}`, typeof global[globalName] === 'function')
let value
let calls = 0
const define = (factory) => {
calls++
value = factory()
}
define.amd = {}
const amd = vm.createContext({ console, define })
vm.runInContext(code, amd, { timeout: 2000 })
check(`DIST.amd-${name}`, calls === 1 && typeof value === 'function' && amd[globalName] === value)
}
check('SSR.import-template', core.html.includes('art-video-player') && esm.default.html === core.html && typeof core.STYLE === 'string' && core.instances.length === 0)
observations.constructorErrors = []
for (const Class of [core, esm.default, legacy.default]) {
for (const useSSR of [false, true]) {
let error
try {
Reflect.construct(Class, [{ container: '#server', url: 'video.mp4', useSSR }])
}
catch (caught) { error = caught }
assert.equal(error?.message, 'Artplayer can only be used in the browser environment')
observations.constructorErrors.push({ useSSR, name: error.name, message: error.message })
}
}
check('SSR.constructor-browser-only', observations.constructorErrors.length === 6)
observations.languages = []
const languages = fs.readdirSync(path.join(root, 'node_modules/artplayer/dist/i18n')).filter(file => file.endsWith('.mjs')).map(file => file.slice(0, -4)).sort()
for (const name of languages) {
const cjs = require(`artplayer/i18n/${name}`)
const module = await import(`artplayer/i18n/${name}`)
assert.deepEqual(module.default, cjs)
const globalName = `artplayerI18n${name.replace(/(^|-)([a-z])/g, (_, _prefix, char) => char.toUpperCase())}`
const context = vm.createContext({})
vm.runInContext(load(`artplayer/dist/i18n/${name}.js`), context, { timeout: 2000 })
assert.equal(JSON.stringify(context[globalName]), JSON.stringify(cjs))
observations.languages.push({ name, globalName, keys: Object.keys(cjs).sort(), play: cjs.Play })
}
check('DIST.i18n-cjs-esm-global', languages.length > 0)
observations.blockedDeepImports = []
for (const specifier of ['artplayer/dist/artplayer.js', 'artplayer/types/artplayer.d.ts', 'artplayer-plugin-chapter/dist/artplayer-plugin-chapter.js']) {
let code
try {
require.resolve(specifier)
}
catch (error) { code = error.code }
assert.equal(code, 'ERR_PACKAGE_PATH_NOT_EXPORTED')
observations.blockedDeepImports.push({ specifier, code })
}
check('DIST.exports-boundary', observations.blockedDeepImports.length === 3)
observations.resolutions = {}
for (const specifier of ['artplayer', 'artplayer/legacy', 'artplayer-plugin-chapter', 'artplayer-plugin-chapter/legacy']) {
const resolved = require.resolve(specifier)
assert(resolved.startsWith(path.join(root, 'node_modules') + path.sep), 'Escaped isolated packages')
observations.resolutions[specifier] = path.relative(root, resolved).replaceAll('\\', '/')
}
// CDN-style absolute URLs bypass package exports; verify the actual files remain readable imports.
const direct = await import(pathToFileURL(path.join(root, 'node_modules/artplayer/dist/artplayer.mjs')).href)
check('DIST.direct-esm-file', direct.default === esm.default)
const { emitterContracts } = await import('./emitter.mjs')
for (const [id, run] of Object.entries(emitterContracts)) {
run(core.Emitter)
checks.push(id)
}
const shape = value => Object.fromEntries(Object.entries(Object.getOwnPropertyDescriptors(value)).map(([key, d]) => [key, { enumerable: d.enumerable, configurable: d.configurable, writable: d.writable, get: typeof d.get, set: typeof d.set, value: typeof d.value }]))
observations.api = { static: shape(core), prototype: shape(core.prototype), emitter: shape(core.Emitter.prototype), factory: shape(chapter), defaults: JSON.parse(JSON.stringify(core.option, (_, value) => typeof value === 'function' ? '$function' : value)) }
process.stdout.write(JSON.stringify({ checks, observations }))
})().catch((error) => {
console.error(error)
process.exitCode = 1
})