diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index afa9893d3..028d7d2ad 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -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 diff --git a/package.json b/package.json index dec329f3c..1fed1a267 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/packages/artplayer-plugin-chapter/.npmignore b/packages/artplayer-plugin-chapter/.npmignore index 4439c6874..2b272d565 100644 --- a/packages/artplayer-plugin-chapter/.npmignore +++ b/packages/artplayer-plugin-chapter/.npmignore @@ -2,4 +2,5 @@ node_modules .DS_Store yarn.lock package-lock.json -src \ No newline at end of file +src +tsconfig.json diff --git a/packages/artplayer/.npmignore b/packages/artplayer/.npmignore index 4439c6874..2b272d565 100644 --- a/packages/artplayer/.npmignore +++ b/packages/artplayer/.npmignore @@ -2,4 +2,5 @@ node_modules .DS_Store yarn.lock package-lock.json -src \ No newline at end of file +src +tsconfig.json diff --git a/refactor/README.md b/refactor/README.md index db4ce4815..e1be9f675 100644 --- a/refactor/README.md +++ b/refactor/README.md @@ -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) | 仓库外安装、运行时/声明检查、安装产物浏览器验证与严格发布检查 | ## 维护规则 diff --git a/refactor/baselines/package-validation.json b/refactor/baselines/package-validation.json new file mode 100644 index 000000000..e89ddd9d0 --- /dev/null +++ b/refactor/baselines/package-validation.json @@ -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 '/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(\"/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(\"/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(\"/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(\"/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(\"/packages/artplayer/types/artplayer\")'." + }, + { + "file": "test/types/consumer.ts", + "code": 2349, + "line": 18, + "message": "This expression is not callable.\n Type 'typeof import(\"/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" +} diff --git a/refactor/changes/2026-09-10-ENG-07-package-consumers.md b/refactor/changes/2026-09-10-ENG-07-package-consumers.md new file mode 100644 index 000000000..b9c9aea5f --- /dev/null +++ b/refactor/changes/2026-09-10-ENG-07-package-consumers.md @@ -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,已冻结发布基线保持不变。 diff --git a/refactor/ci-setup.md b/refactor/ci-setup.md index 017a3244a..8eefcb887 100644 --- a/refactor/ci-setup.md +++ b/refactor/ci-setup.md @@ -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。发布排队执行,不取消正在执行的发布。 diff --git a/refactor/plan.md b/refactor/plan.md index 203afeff3..0f62d341e 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -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
建立类型测试基础 | ENG-02, BASE-05 | 根与分包 tsconfig、显式 TS 依赖、正反例测试 | 核心/试点与迁移模块严格检查,未迁移第三方/包历史问题独立台账;明确最低/当前 TS 和各环境类型;复用 BASE-05 的四种消费模式,不以 skipLibCheck 掩盖 BASE-TYPE-01/03 | M | done | | ENG-05 | workspace
建立真实浏览器测试服务 | ENG-03, BASE-04, BASE-08 | Playwright projects、本地 Range/失败媒体服务;复用 docs 页面/样本的状态隔离、错误采集与候选资源映射 | Chromium/Firefox/WebKit 的基础播放 smoke 和报告可执行;以媒体状态断言判定通过,区分轻量用例与真实编辑器交互 | M | done | | ENG-06 | workspace
支持按包非交互与 JS/TS 构建 | ENG-02, BASE-05 | build/dev 入口解析、指定包参数、原交互保留 | 三种产物、Less/SVG/worker 和本地 8082 demo 正常;保持 BASE-05 的 AMD 同时写入全局行为及 i18n/legacy 入口 | H | done | -| ENG-07 | workspace
建立 tarball 消费与产物检查 | ENG-04, ENG-06 | 隔离 npm 消费 fixtures、API/声明/入口差分 | 不借 workspace 源码通过,能识别缺文件与默认导出变化 | H | todo | +| ENG-07 | workspace
建立 tarball 消费与产物检查 | ENG-04, ENG-06 | 隔离 npm 消费 fixtures、API/声明/入口差分 | 不借 workspace 源码通过,能识别缺文件与默认导出变化 | H | done | | ENG-08 | workspace
增加覆盖率、资源与性能报告 | ENG-03, ENG-05, BASE-06 | 覆盖率基线、资源清理断言、性能报告与阈值 | 关键生命周期分支有门槛,报告不靠无意义断言堆数量;将 BASE-06 的同环境多组配对、计时/压缩审查阈值与资源异常分开接入候选,不把历史现象冻结成正常要求 | M | todo | | ENG-09 | workspace
建立全包依赖影响和文档检查 | ENG-07, DOC-04 | 共享核心/构建影响映射、文档及每任务完成提交的 Git 审计接入;契约-支持版本-测试 ID-命令-候选/报告-任务的覆盖索引 | 核心变化触发必需生态检查;原有 DOC-01 至 04 基线例外明确,后续 done 任务不能缺失独立 commit;计划/已执行/缺证据分开,识别缺少验证归属的公开契约 | M | todo | | ENG-10 | workspace
建立历史失败分级和测试可靠性规则 | 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) diff --git a/refactor/progress.md b/refactor/progress.md index 5c3a93321..9faa18394 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -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 源码。运行时源码尚未迁移。 diff --git a/refactor/tasks.json b/refactor/tasks.json index 1a7435422..23cef36fc 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -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", diff --git a/scripts/package-check.mjs b/scripts/package-check.mjs new file mode 100644 index 000000000..0213fec86 --- /dev/null +++ b/scripts/package-check.mjs @@ -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 + }) +} diff --git a/scripts/package-consumer.mjs b/scripts/package-consumer.mjs new file mode 100644 index 000000000..4c15ff7c0 --- /dev/null +++ b/scripts/package-consumer.mjs @@ -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/`, '/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('/node_modules/', '/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 +} diff --git a/test/package-check.test.js b/test/package-check.test.js new file mode 100644 index 000000000..79ed66f59 --- /dev/null +++ b/test/package-check.test.js @@ -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) + } +}) diff --git a/test/package/README.md b/test/package/README.md new file mode 100644 index 000000000..fd8520538 --- /dev/null +++ b/test/package/README.md @@ -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`. diff --git a/test/package/known-types.json b/test/package/known-types.json new file mode 100644 index 000000000..dcf3d0d8a --- /dev/null +++ b/test/package/known-types.json @@ -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" } + ] +} diff --git a/test/package/runtime.cjs b/test/package/runtime.cjs new file mode 100644 index 000000000..e61810120 --- /dev/null +++ b/test/package/runtime.cjs @@ -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 +})