mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-11 22:06:17 -08:00
refactor(tooling): [MOD-PLUGIN-01] generate typed plugins with safe output handling
This commit is contained in:
1 parent
07d5bfef2e
commit
28c7abb2bb
32 files changed
+958
-161
No files matched your search
+7
-5
@@ -40,14 +40,14 @@
|
||||
"test:dash-control": "node --test test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js",
|
||||
"dev": "npx cross-env NODE_ENV=development node ./scripts/dev.js",
|
||||
"build": "npx cross-env NODE_ENV=production node ./scripts/build.js",
|
||||
"lint": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"packages/artplayer-vitepress/{browser,build}/**/*.ts\" \"scripts/{docs-smoke,editor-declarations,documentation,site-build}/**/*.ts\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --no-fix",
|
||||
"lint": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"packages/artplayer-vitepress/{browser,build}/**/*.ts\" \"scripts/{docs-smoke,editor-declarations,documentation,site-build}/**/*.ts\" \"scripts/plugin/*.{js,ts}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --no-fix",
|
||||
"build:all": "yarn ci:build && yarn lint",
|
||||
"check:toolchain": "node scripts/check-toolchain.mjs",
|
||||
"lint:fix": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"packages/artplayer-vitepress/{browser,build}/**/*.ts\" \"scripts/{docs-smoke,editor-declarations,documentation,site-build}/**/*.ts\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --fix",
|
||||
"lint:fix": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"packages/artplayer-vitepress/{browser,build}/**/*.ts\" \"scripts/{docs-smoke,editor-declarations,documentation,site-build}/**/*.ts\" \"scripts/plugin/*.{js,ts}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.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/package-check.test.js test/declarations.test.js test/editor-types.test.js test/coverage.test.js test/performance-report.test.js test/media-gate.test.js test/ci-summary.test.js test/package-runtime.test.js test/library-build.test.js test/site-loading.test.js test/documentation-pipeline.test.js test/site-build.test.js test/site-markdown.test.js test/site-editor.test.js",
|
||||
"test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js test/package-check.test.js test/declarations.test.js test/editor-types.test.js test/coverage.test.js test/performance-report.test.js test/media-gate.test.js test/ci-summary.test.js test/package-runtime.test.js test/library-build.test.js test/site-loading.test.js test/documentation-pipeline.test.js test/site-build.test.js test/site-markdown.test.js test/site-editor.test.js test/plugin-scaffold.test.js",
|
||||
"test:baseline": "node --test refactor/scripts/*.test.mjs",
|
||||
"ci:check": "yarn check:toolchain --strict && yarn check:commits --report && yarn check:impact --report && yarn check:ci && yarn test:contracts && yarn check:contracts --report && yarn check:plan && yarn lint && yarn typecheck:docs-tools && yarn check:docs-smoke && yarn check:editor-types && yarn check:llm && yarn typecheck:site-assets && yarn check:site-assets && yarn check:types && yarn typecheck && yarn typecheck:react && yarn lint:react && yarn typecheck:vue && yarn lint:vue && yarn test",
|
||||
"ci:check": "yarn check:toolchain --strict && yarn check:commits --report && yarn check:impact --report && yarn check:ci && yarn test:contracts && yarn check:contracts --report && yarn check:plan && yarn lint && yarn typecheck:scaffold && yarn typecheck:docs-tools && yarn check:docs-smoke && yarn check:editor-types && yarn check:llm && yarn typecheck:site-assets && yarn check:site-assets && yarn check:types && yarn typecheck && yarn typecheck:react && yarn lint:react && yarn typecheck:vue && yarn lint:vue && yarn test",
|
||||
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:llm && yarn build:test && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js test/asr-distribution.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
@@ -127,7 +127,9 @@
|
||||
"typecheck:site-assets": "node node_modules/typescript/bin/tsc -p packages/artplayer-vitepress/browser/tsconfig.json --noEmit",
|
||||
"check:llm": "node scripts/build-llm.js --check",
|
||||
"test:site-build": "node --test test/site-build.test.js test/build-docs.test.js test/site-markdown.test.js",
|
||||
"test:site-editor": "node --test test/site-editor.test.js"
|
||||
"test:site-editor": "node --test test/site-editor.test.js",
|
||||
"typecheck:scaffold": "node node_modules/typescript/bin/tsc -p scripts/tsconfig.scaffold.json --noEmit",
|
||||
"test:scaffold": "node --test test/plugin-scaffold.test.js"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"task": "MOD-PLUGIN-01",
|
||||
"date": "2026-09-14",
|
||||
"baselineCommit": "07d5bfef2e815e8038362bb836b9c496c7ef9862",
|
||||
"environment": {
|
||||
"os": "Windows",
|
||||
"node": "24.21.0",
|
||||
"yarn": "1.22.22",
|
||||
"typescript": "5.9.3",
|
||||
"classicTypescript": "4.3.5"
|
||||
},
|
||||
"results": {
|
||||
"scaffold": {
|
||||
"passed": 11,
|
||||
"failed": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"ci": {
|
||||
"passed": 50,
|
||||
"failed": 0
|
||||
},
|
||||
"strictTypes": "passed",
|
||||
"toolchain": "passed",
|
||||
"lint": {
|
||||
"errors": 0,
|
||||
"warnings": 1,
|
||||
"existingWarning": "docs/assets/ts/artplayer.d.ts unused ts/no-namespace directive"
|
||||
},
|
||||
"generatedPackage": {
|
||||
"formats": [
|
||||
"UMD es2020",
|
||||
"UMD es2015 legacy",
|
||||
"ESM es2020"
|
||||
],
|
||||
"exports": [
|
||||
"ESM default",
|
||||
"CommonJS direct and .default",
|
||||
"legacy ESM and CommonJS"
|
||||
],
|
||||
"types": [
|
||||
"strict source",
|
||||
"TS5.9.3 NodeNext .mts/.cts",
|
||||
"TS4.3.5 classic positive/negative"
|
||||
],
|
||||
"lint": "zero warnings",
|
||||
"dom": "linkedom duplicate stylesheet and no-DOM imports"
|
||||
},
|
||||
"baseline": {
|
||||
"passed": 522,
|
||||
"failed": 0,
|
||||
"skipped": 0
|
||||
}
|
||||
},
|
||||
"limitations": [
|
||||
"No real browser playback or remote CI execution",
|
||||
"No new real workspace; integration output is an ignored fixture",
|
||||
"No all-files atomicity or killed-process recovery",
|
||||
"No new dependencies or changes to yarn.lock"
|
||||
],
|
||||
"sourceFiles": [
|
||||
{
|
||||
"path": "scripts/plugin/create.js",
|
||||
"sha256": "ebc413b32037561698efd3c19776681cca57f259f09fab86e8d06d419b9a6647"
|
||||
},
|
||||
{
|
||||
"path": "scripts/plugin/cli.ts",
|
||||
"sha256": "0aa466a75c6e808ab652ca5409134e8618c1b0d96908c4a27362927c2f5084e4"
|
||||
},
|
||||
{
|
||||
"path": "scripts/plugin/render.ts",
|
||||
"sha256": "339485dd33bb6bf1e2602f89d65d88e6a4aea6aa4133f19c64b6cda9de466090"
|
||||
},
|
||||
{
|
||||
"path": "scripts/plugin/publish.ts",
|
||||
"sha256": "41604d49c22c5e30e69c17a4664055c2cd6442de473b85e3550a5cc96c6aca0f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/tsconfig.scaffold.json",
|
||||
"sha256": "54388f6b1cc1c4173fcce70bbacd9512469f89bb19145be6aeb8c66eb6bd6fbc"
|
||||
},
|
||||
{
|
||||
"path": "scripts/plugin/template/package.json",
|
||||
"sha256": "3a8b700ba7f662cba8324678743339acdc1db33b7f9e7f37908971ff7125c982"
|
||||
},
|
||||
{
|
||||
"path": "scripts/plugin/template/src/index.ts.tpl",
|
||||
"sha256": "53183f3a3476c1c3225cf57c167b4bd24df7d5c46949b38889ebbe9326fceb72"
|
||||
},
|
||||
{
|
||||
"path": "scripts/plugin/template/src/stylesheet.ts.tpl",
|
||||
"sha256": "c9239de63c8b3a675fb0f8b1bd9f1a33544cae29864b8d16076a379ad89a5427"
|
||||
},
|
||||
{
|
||||
"path": "scripts/plugin/template/src/assets.d.ts.tpl",
|
||||
"sha256": "abc216473dd0869001b5a88ed7add80f0449e71d1d3af872a6df9631f96e4641"
|
||||
},
|
||||
{
|
||||
"path": "scripts/plugin/template/src/style.less",
|
||||
"sha256": "64c690eacbc33b44517d1fe84c53dbce3b76d97ed54ea273c6400119b46033d2"
|
||||
},
|
||||
{
|
||||
"path": "scripts/plugin/template/types/api.d.ts.tpl",
|
||||
"sha256": "24dd42ddae63405c6752528077b435f8ee398bc233efaab064707475a049b48a"
|
||||
},
|
||||
{
|
||||
"path": "scripts/plugin/template/types/artplayer-plugin-{{name}}.d.ts.tpl",
|
||||
"sha256": "82d68fc8311076f9e9466fb0e73927390501770dc02d2fdc828a637638f69949"
|
||||
},
|
||||
{
|
||||
"path": "scripts/plugin/template/types/artplayer-plugin-{{name}}.d.mts.tpl",
|
||||
"sha256": "63dbe5a3e754f380abb7de53e3d8de7074d424892b4f724465111c139cb65b71"
|
||||
},
|
||||
{
|
||||
"path": "scripts/plugin/template/test/plugin.test.mjs.tpl",
|
||||
"sha256": "e60bf566d665ac0d77a4d41899f26b01a24acf2516204e285f87c763f5b6ff57"
|
||||
},
|
||||
{
|
||||
"path": "scripts/plugin/template/tsconfig.json",
|
||||
"sha256": "528081768e5c3a6361dd0a2cfc59f953257caeae70bd5305b2db2fe4d98009ad"
|
||||
},
|
||||
{
|
||||
"path": "test/plugin-scaffold.test.js",
|
||||
"sha256": "ac9580ea49e937abc5129324ee5de930320dea59ba556f71ad9b1612a224f981"
|
||||
}
|
||||
],
|
||||
"logs": [
|
||||
{
|
||||
"path": "refactor/.cache/scaffold-initial.log",
|
||||
"sha256": "6cd75297d36e46b1e4f9e462d296383dd76304d2befed6c6662d1fe21e74f5f0"
|
||||
},
|
||||
{
|
||||
"path": "refactor/.cache/scaffold-generated-lint-initial.log",
|
||||
"sha256": "99d420f2c2e4a76248b59bb8593297f127337d4eb5ddb81463e9e4ea80269bbc"
|
||||
},
|
||||
{
|
||||
"path": "refactor/.cache/scaffold-final.log",
|
||||
"sha256": "8505990bf0131dfa3598fa985cdbc7e294c6a62a2f1e905c864a082ac63f0179"
|
||||
},
|
||||
{
|
||||
"path": "refactor/.cache/scaffold-types.log",
|
||||
"sha256": "4bff36c4b15e17ba27c5065a7db072e324500a827be1e11ebb60d370271ff336"
|
||||
},
|
||||
{
|
||||
"path": "refactor/.cache/scaffold-lint.log",
|
||||
"sha256": "928ec73d5f8ea3b16e4bc051293d425692bd393df790abecb9ee320adecdff62"
|
||||
},
|
||||
{
|
||||
"path": "refactor/.cache/scaffold-ci.log",
|
||||
"sha256": "6cd851ff61057e007c24ff0898578c42dabb634096cb8b45d36bde7fd3114be0"
|
||||
},
|
||||
{
|
||||
"path": "refactor/.cache/scaffold-toolchain.log",
|
||||
"sha256": "1d5e2306c57bc1e90ae5625e0924e56cde9e27eb3cf89c64bfe735a36dd42863"
|
||||
},
|
||||
{
|
||||
"path": "refactor/.cache/scaffold-baseline.log",
|
||||
"sha256": "2ef047976b63d707e3e4fde2e3475b4d0b53c699c6120826870f0bec3816d971"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
# 已实现的 JS/TS 构建与开发入口
|
||||
|
||||
MOD-PLUGIN-01 将 `yarn create:plugin` 的实现拆为严格 TS 渲染/文件写入/CLI,
|
||||
保留旧 JS 命令入口。生成 TS 工厂、CJS/ESM 声明、分发消费测试与维护文档;
|
||||
拒绝覆盖既有包/示例,失败时回退自己的写入并保留外部修改。
|
||||
运行 `yarn typecheck:scaffold` 与 `yarn test:scaffold`,具体限制和模块地图见
|
||||
[生成器维护指南](../scripts/plugin/README.md)。MOD-02 继续负责 dev/build/utils,
|
||||
本子任务不代表全部工程脚本已经迁移。
|
||||
|
||||
ENG-06 保留原来的 Vite 7.3.6、Terser、三种库产物及路径,增加选包参数和 TS 入口;未切换 bundler 或运行时依赖。固定工具链仍为 Node 24.21.0 / Yarn 1.22.22。
|
||||
|
||||
ENG-12 将库配置的 publicDir 设为 false,防止 core/public 的声明源码被复制到 dist。
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# MOD-PLUGIN-01: TypeScript plugin scaffold
|
||||
|
||||
## Scope and compatibility
|
||||
|
||||
The original generator at `07d5bfef2e815e8038362bb836b9c496c7ef9862` overwrites an
|
||||
existing dotted demo file, and its README uses the hyphenated name instead of that
|
||||
actual file name. The frozen implementation is executed in a fixture to reproduce
|
||||
both defects. The old command and valid name/global/example mapping remain; invalid
|
||||
names with empty segments and extra arguments now fail before writing. No existing
|
||||
package, public declaration, built distribution, or real example was regenerated.
|
||||
|
||||
`MOD-PLUGIN-01` is a child dependency of MOD-02. It finishes the generator/template
|
||||
part independently; development/build/shared-config migration remains with MOD-02.
|
||||
|
||||
## Implementation and maintenance
|
||||
|
||||
The checked JS command shim delegates to strict TS CLI, pure rendering and filesystem
|
||||
publication modules. Rendering validates placeholders, name and output collisions.
|
||||
Publication stages complete files, exclusively reserves a new package and hard-links
|
||||
without replacing existing destinations. Caught failures roll back owned unchanged
|
||||
files and empty directories, preserving edits and concurrent output with recovery
|
||||
paths. Existing symlink/junction destinations are rejected. This is per-file atomic
|
||||
publication, not all-files/crash atomicity or adversarial filesystem protection.
|
||||
|
||||
The new template includes TS source, a once-per-document stylesheet import with an
|
||||
SSR guard, explicit shared option/result declarations, distinct CJS/ESM declaration
|
||||
entries, main/module/legacy formats, direct/default CommonJS calls, package-consumer
|
||||
tests and a maintenance README. The demo links to the actual dotted example.
|
||||
The factory remains synchronous. New package version starts at 1.0.0; the existing
|
||||
packages' independent next-major targets are unchanged.
|
||||
|
||||
The module map, failure recovery boundaries and reproducible commands live in
|
||||
[scripts/plugin/README.md](../../scripts/plugin/README.md). New dependencies: none.
|
||||
Generated manifests declare the ArtPlayer host as a peer; generation does not install
|
||||
or update the root lock, inventories or release scope. Root `test:scaffold`,
|
||||
`typecheck:scaffold`, lint coverage, Node tests and CI typecheck now cover this tool.
|
||||
|
||||
## Verification and actual limits
|
||||
|
||||
- Node 24.21.0, Yarn Classic 1.22.22, Windows. Strict toolchain check passed.
|
||||
- `yarn typecheck:scaffold`: strict TS modules plus JS shim passed.
|
||||
- `yarn test:scaffold`: 11 passed, zero skipped. Tests cover the frozen defects,
|
||||
malformed template/name input, existing/competing outputs, mid-write rollback,
|
||||
retention of externally edited files, redirected directories and CLI invocation
|
||||
from another directory. A generated fixture is linted with zero warnings, built
|
||||
through the real three-format production CLI, then consumed through actual
|
||||
ESM/CJS/legacy package exports. Strict source, TS 5.9.3 NodeNext import/require and
|
||||
TS 4.3.5 classic consumers pass positive and negative cases. Repeated stylesheet
|
||||
loading and no-DOM import are checked with linkedom/VM, not real media playback.
|
||||
- `yarn lint`: passed with the one pre-existing unused directive warning in
|
||||
`docs/assets/ts/artplayer.d.ts`.
|
||||
- `yarn test:ci`: 50 passed. This proves local workflow-policy regressions, not a
|
||||
remote GitHub run.
|
||||
- `yarn test:baseline`: 522 passed, zero skipped; frozen package/runtime/type and
|
||||
refactor workflow regression checks remain intact.
|
||||
- Initial integration failure was the nested Node test runner's inherited context,
|
||||
which suppressed the child reporter. The runner now removes NODE_TEST_CONTEXT;
|
||||
child test completion is asserted. Generated-output lint then found manifest
|
||||
array ordering and the old template Less indentation; both templates corrected.
|
||||
Initial logs are retained separately from the successful run.
|
||||
|
||||
Evidence and source/log hashes: [scaffold-validation.json](../baselines/scaffold-validation.json).
|
||||
Fixtures are ignored and removed through checked paths; no 23rd real workspace was
|
||||
created. No frozen reinstall was needed because dependencies and yarn.lock are
|
||||
unchanged. No browser playback, device, full release or publication claims arise
|
||||
from this tooling task. Future generated plugin features need their own tests.
|
||||
|
||||
## Rollback and next work
|
||||
|
||||
Revert this task's dedicated commit to restore the previous generator, templates and
|
||||
script integration; that also restores its known overwrite defect. Existing package
|
||||
artifacts require no rollback. MOD-02 will migrate the remaining build/development
|
||||
responsibilities while preserving command behavior. Local commit only; no push/npm.
|
||||
@@ -11,6 +11,7 @@
|
||||
| `yarn typecheck` | 根/迁移包严格检查、当前与兼容 TS 消费;历史 NodeNext ESM 错误单独核对,见 typechecking.md |
|
||||
| `yarn typecheck:react` / `yarn typecheck:vue` | 原 React TSX / Vue SFC 示例严格检查,ci:check 同时执行对应 lint |
|
||||
| `yarn typecheck:docs-tools` / `yarn check:docs-smoke` | 严格检查 TS 示例/声明生成器及 JS/MJS 门面;只读核对确定性生成的 readiness smoke,ci:check 执行 |
|
||||
| `yarn typecheck:scaffold` / `yarn test:scaffold` | 严格检查插件生成器与旧 JS 入口;生成包真实构建/类型/分发消费和写入失败回归;分别接入 ci:check 与 test:node |
|
||||
| `yarn check:editor-types` | 只读核对全部编辑器声明、SDK notices 和实际 libUris;主 TS 5.9.3/历史 4.3.5 整组语义检查,ci:check 执行 |
|
||||
| `yarn build:site-assets` / `yarn check:site-assets` | 从站点 browser/ TS 生成三个经典脚本;check 只读,root build:docs 先生成;完整 VitePress 构建另行验收 |
|
||||
| `yarn typecheck:site-assets` | 严格检查站点 browser/ 模块;生成脚本由 typecheck:docs-tools 的 checkJs 覆盖;均进入 ci:check |
|
||||
|
||||
+5
-3
@@ -2,9 +2,9 @@
|
||||
|
||||
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 243 项,范围 22 个包及工作区/示例。
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 244 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 57 / doing 16 / blocked 0 / done 170 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 57 / doing 16 / blocked 0 / done 171 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -400,7 +400,8 @@
|
||||
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| MOD-01 | workspace<br>Bun 固定版本干净安装试点 | ENG-09 | 独立目录的 Bun 安装与 Yarn 冻结基线对比,默认仍为用户选定的 Yarn | Node 测试仍通过;安装与资源一致才决定采用;不改 bundler;不自动替换默认 packageManager 或维护锁文件 | H | todo |
|
||||
| MOD-02 | workspace<br>整理剩余开发/构建脚本与插件模板 | ENG-06, SITE-03 | dev/build/utils/create-plugin 的 TS 与可测 CLI,模板同时提供旧 API | 旧脚本入口保留、新插件类型/测试/示例齐全,Lerna 改动单独取证 | M | todo |
|
||||
| MOD-PLUGIN-01 | workspace<br>迁移插件生成器与类型化模板 | ENG-06, SITE-03 | 保留 create:plugin CLI;纯渲染/文件发布边界、TS 工厂/声明/测试/示例及维护指南 | 防止覆盖现有包或示例;失败回退;生成包可构建并通过严格类型、旧模块消费及测试;不自动修改既有包或运行安装发布 | M | done |
|
||||
| MOD-02 | workspace<br>整理剩余开发/构建脚本与插件模板 | ENG-06, SITE-03, MOD-PLUGIN-01 | dev/build/utils/create-plugin 的 TS 与可测 CLI,模板同时提供旧 API | 旧脚本入口保留、新插件类型/测试/示例齐全,Lerna 改动单独取证 | M | todo |
|
||||
| MOD-03 | workspace<br>测量并优化核心热路径 | CORE-22, ENG-08 | DOM 读写、进度更新、持久化、初始化的测量与改进 | 相同设备媒体多次比较,契约不变,收益及无效尝试记录;复用 BASE-06 的原始样本与测量限制,至少三组同环境旧新配对,不以单次变快宣称收益 | M | todo |
|
||||
| MOD-04 | workspace<br>测量并优化重型插件/proxy | PKG-DANMUKU-09, PKG-MASK-06, PKG-MB-10, ENG-08 | 帧/队列/推理/音画同步与资源长期运行比较 | 不改默认算法/阈值,性能改善有证据;无收益则保留旧实现 | M | todo |
|
||||
| MOD-05 | workspace<br>完成工具链与性能采用决策 | MOD-01, MOD-02, MOD-03, MOD-04 | 最终 runtime/packageManager/构建配置及性能台账 | 干净安装和全包检查通过;Bun 未采用有理由,不为状态强行切换 | M | todo |
|
||||
@@ -620,6 +621,7 @@
|
||||
- SITE-07: [记录](site-inventory.md) [记录](baselines/site-provenance.json)
|
||||
- EX-01: [记录](changes/2026-09-14-EX-01-react-consumer.md) [记录](baselines/react-consumer-validation.json) [记录](scripts/react-consumer.mjs)
|
||||
- EX-02: [记录](changes/2026-09-14-EX-02-vue-consumer.md) [记录](baselines/vue-consumer-validation.json) [记录](scripts/vue-consumer.mjs)
|
||||
- MOD-PLUGIN-01: [记录](changes/2026-09-14-MOD-PLUGIN-01-scaffold.md) [记录](baselines/scaffold-validation.json)
|
||||
- PKG-FACTORY-01: [记录](baselines/factory-assignment-gaps.json) [记录](baselines/factory-compatibility-proposals.json) [记录](factory-compatibility-decision.md) [记录](changes/2026-09-12-PKG-FACTORY-01-decision.md) [记录](type-compatibility-policy.md) [记录](baselines/factory-compatibility-validation.json) [记录](changes/2026-09-13-PKG-FACTORY-01-compatible-types.md)
|
||||
- CORE-25: [记录](changes/2026-09-13-CORE-25-defaults-ssr.md) [记录](baselines/defaults-ssr-validation.json)
|
||||
- ENG-12: [记录](changes/2026-09-13-ENG-12-library-public.md) [记录](baselines/library-public-validation.json)
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# 进度与证据
|
||||
|
||||
## MOD-PLUGIN-01 插件生成器与 TS 模板完成
|
||||
|
||||
生成器拆为严格 TS 的模板渲染、文件写入和 CLI,保留 create.js 旧命令入口及
|
||||
合法命名规则。冻结旧脚本复现示例覆盖和 README 链接不匹配;候选拒绝覆盖,
|
||||
失败时回退自己的未修改文件,保留外部修改并报告恢复路径。生成 TS 工厂、
|
||||
CJS/ESM 声明、三格式分发消费测试和维护指南,不改既有包或添加真实 workspace。
|
||||
11 项目标测试、522 项 baseline、50 项 CI 回归通过;生成包 lint 零 warning,
|
||||
源码及新旧编译器严格检查通过,全仓 lint 保留 1 条既有 warning。初始子进程
|
||||
测试报告问题及模板格式失败日志保留;详见[变更记录](changes/2026-09-14-MOD-PLUGIN-01-scaffold.md)
|
||||
和[证据](baselines/scaffold-validation.json)。没有新增依赖、安装、推送或发布。
|
||||
DOM 证据是 linkedom,不是未来插件功能的真实播放验收;文件写入不承诺进程
|
||||
被杀后的自动恢复。244 项变为 171 done、16 doing、57 todo。下一步 MOD-02
|
||||
继续 dev/build/utils 等剩余工程脚本;VAST 默认行为选择仍待用户答复。
|
||||
|
||||
|
||||
## VTT Thumbnail 五核心组合检查点
|
||||
|
||||
PKG-VTT-THUMB-05 进入 doing。冻结真实核心5.1.6的197成员,和已有5.1.7、
|
||||
|
||||
@@ -261,3 +261,4 @@
|
||||
| SITE-EDITOR-FAILURE-01 | resolved / 已复现 | Desktop editor evaluates raw TypeScript and cannot settle FileReader errors or boot with denied storage | SITE-03 |
|
||||
| SITE-EDITOR-AMD-01 | resolved / 已复现 | Late Monaco language AMD initialization races with example dependency loading | SITE-03 |
|
||||
| VTT-CORE-NAME-01 | open / 已复现 | Published VTT 1.0.1 control name conflicts with the core placeholder introduced in 5.1.7 | PKG-VTT-THUMB-05, REL-08 |
|
||||
| SCAFFOLD-OUTPUT-01 | resolved / 已复现 | Plugin generation overwrites existing examples and links to a different example name | MOD-PLUGIN-01 |
|
||||
@@ -5892,6 +5892,28 @@
|
||||
"compatibleResolution": "Preserve current vtt-thumbnail and reserved core control identities; distinguish the actual working 1.0.1/5.1.6 pair from the already failing 5.1.7/5.4.0/candidate pair. Decide the supported historical pairing/migration or a compatible accommodation during release review without hiding duplicate-control failures.",
|
||||
"closureCriteria": "Historical working and failing browser pairs remain reproducible; any claimed accommodation preserves reserved-control ownership and duplicate-registration contracts, or an explicit reviewed historical support/migration boundary is recorded.",
|
||||
"workspaceState": "Final three-engine suite passes nine assertions of this historical rejection, not nine successful compatibility cells. No production control behavior was changed."
|
||||
},
|
||||
{
|
||||
"id": "SCAFFOLD-OUTPUT-01",
|
||||
"title": "Plugin generation overwrites existing examples and links to a different example name",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"MOD-PLUGIN-01"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/changes/2026-09-14-MOD-PLUGIN-01-scaffold.md",
|
||||
"refactor/baselines/scaffold-validation.json",
|
||||
"test/plugin-scaffold.test.js"
|
||||
],
|
||||
"compatibleResolution": "Keep valid CLI and package/global/example names; reject existing outputs and generate the README from the actual example name.",
|
||||
"closureCriteria": "Reproduce both historical defects and verify candidate collision/rollback behavior and a built typed package.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-14-MOD-PLUGIN-01-scaffold.md",
|
||||
"refactor/baselines/scaffold-validation.json",
|
||||
"test/plugin-scaffold.test.js"
|
||||
],
|
||||
"resolutionRationale": "Frozen old generator reproduces overwrite and mismatched URL. Candidate refuses old or concurrent output, preserves external edits on rollback, generates matching dotted URLs and passes actual three-format package consumers and strict types."
|
||||
}
|
||||
]
|
||||
}
|
||||
+22
-1
@@ -4427,6 +4427,26 @@
|
||||
"acceptance": "Node 测试仍通过;安装与资源一致才决定采用;不改 bundler;不自动替换默认 packageManager 或维护锁文件",
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "MOD-PLUGIN-01",
|
||||
"phase": "7 工具链与性能",
|
||||
"title": "迁移插件生成器与类型化模板",
|
||||
"scope": [
|
||||
"workspace"
|
||||
],
|
||||
"dependsOn": [
|
||||
"ENG-06",
|
||||
"SITE-03"
|
||||
],
|
||||
"status": "done",
|
||||
"risk": "M",
|
||||
"deliverable": "保留 create:plugin CLI;纯渲染/文件发布边界、TS 工厂/声明/测试/示例及维护指南",
|
||||
"acceptance": "防止覆盖现有包或示例;失败回退;生成包可构建并通过严格类型、旧模块消费及测试;不自动修改既有包或运行安装发布",
|
||||
"evidence": [
|
||||
"changes/2026-09-14-MOD-PLUGIN-01-scaffold.md",
|
||||
"baselines/scaffold-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "MOD-02",
|
||||
"phase": "7 工具链与性能",
|
||||
@@ -4436,7 +4456,8 @@
|
||||
],
|
||||
"dependsOn": [
|
||||
"ENG-06",
|
||||
"SITE-03"
|
||||
"SITE-03",
|
||||
"MOD-PLUGIN-01"
|
||||
],
|
||||
"status": "todo",
|
||||
"risk": "M",
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# 已实现的类型检查与迁移入口
|
||||
|
||||
`yarn typecheck:scaffold` 检查 scripts/plugin 的严格 TS 模块及旧 JS 命令入口。
|
||||
模板本身保留占位符,`yarn test:scaffold` 对实际渲染包执行源码检查和
|
||||
TS 5.9.3 NodeNext / TS 4.3.5 classic 消费者正反例;生成声明地图见
|
||||
[插件生成器维护指南](../scripts/plugin/README.md)。
|
||||
|
||||
PKG-ADS-04 已协调公开声明:根入口保留历史输入,/runtime 使用准确输入,同一 JS
|
||||
实现不进行字符串时长转换。CJS/ESM 桥、五种消费者模式、实际旧声明对照与真实 Monaco
|
||||
生成运行已接入。`ads-source.ts` 检查源码与公开工厂的赋值关系;`ads.ts` 包含十个
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# Plugin scaffold maintenance
|
||||
|
||||
Run `yarn create:plugin some-name` from the repository. The historical
|
||||
`node scripts/plugin/create.js some-name` path remains available and always targets
|
||||
the repository containing that script, even if the current directory differs.
|
||||
Use Node from `.node-version` and Yarn Classic 1.22.22. `--help` writes nothing.
|
||||
Names must be lowercase words separated by single hyphens; malformed names and
|
||||
extra arguments now fail before writing. Existing valid CLI names keep their
|
||||
package/global/example naming convention (`some-name` / `artplayerPluginSomeName`
|
||||
/ `some.name.js`). The README demo now uses the actual dotted example name.
|
||||
|
||||
## Ownership and write sequence
|
||||
|
||||
| Module | Responsibility |
|
||||
| --- | --- |
|
||||
| `create.js` | Checked JS compatibility shim; repository root, argv and failure exit status |
|
||||
| `cli.ts` | Validate command shape, render then publish, print next steps |
|
||||
| `render.ts` | Read text templates into a deterministic relative-path/content map; validate names, placeholders and output collisions |
|
||||
| `publish.ts` | Preflight destinations, stage complete text, exclusively reserve the package name and link files, roll back owned writes on failure |
|
||||
| `template/` | Generated package source, declarations, manifest, built-package tests and maintenance guide |
|
||||
|
||||
Templates ending in `.tpl` lose that suffix. `{{name}}`, `{{export}}` and
|
||||
`{{example}}` are the only replacements, including in filenames. Templates are
|
||||
UTF-8 text; links and unknown placeholders fail during rendering. Invalid TS
|
||||
placeholder identifiers are intentionally stored as `.tpl`; lint the renderer
|
||||
normally, and typecheck/build its rendered output through the integration test.
|
||||
|
||||
The writer refuses existing packages (including empty directories), examples and
|
||||
redirected destination paths. An exclusive temporary directory inside `packages/`
|
||||
holds the complete text before the final package is reserved. Hard links publish
|
||||
each complete file without overwriting another writer. Filesystem support for
|
||||
same-volume hard links is required; failure is reported and owned writes roll back.
|
||||
Staging never contains a top-level package.json and is ignored by project discovery.
|
||||
|
||||
On a caught failure, only files still matching this operation's written contents
|
||||
are removed, followed by empty directories created by this operation. Changed files
|
||||
and nonempty directories remain, with recovery paths in the error and its cause.
|
||||
The staged directory is removed only after checking its actual path and parent.
|
||||
The injected link function is an internal test seam for write failure/collision
|
||||
cases, not a CLI option. Publication is atomic per file, **not** a transaction over
|
||||
all files or crash recovery: a killed process may leave a partial new package or
|
||||
staging directory for manual inspection. This is not protection against an actor
|
||||
actively replacing filesystem ancestors between checks.
|
||||
|
||||
## Generated package contract
|
||||
|
||||
The synchronous TS factory returns its registration name, with a self `.default`
|
||||
alias for CommonJS users. The three existing build formats and `/legacy` entry
|
||||
remain. CommonJS/older-TS `.d.ts` and ESM `.d.mts` share `types/api.d.ts`, also used
|
||||
by the runtime source. The initial options type deliberately has no fields;
|
||||
implement explicit owned fields and behavior tests when developing the plugin.
|
||||
Styles are injected once per document at module evaluation, with a no-DOM import
|
||||
guard. During document loading this now inserts the style immediately instead of
|
||||
scheduling a DOMContentLoaded handler; imports after loading remain supported.
|
||||
These are templates for new packages;
|
||||
the command never rewrites an existing plugin's API or stylesheet behavior.
|
||||
|
||||
Generation does not install dependencies, change the root Yarn lock or inventories,
|
||||
or publish. New packages start at 1.0.0; the existing 22 packages' separate major
|
||||
upgrade policy remains unchanged. Before adding a new real workspace to CI, update
|
||||
the explicit package/demo/type/contract coverage and release scope through its own
|
||||
task, then update only the root lock. The scaffold's `artplayer: "*"` peer is a
|
||||
declaration of the host dependency, not evidence for every historical version.
|
||||
|
||||
## Validation
|
||||
|
||||
```sh
|
||||
yarn typecheck:scaffold
|
||||
yarn test:scaffold
|
||||
yarn lint
|
||||
```
|
||||
|
||||
`test/plugin-scaffold.test.js` freezes the former generator at
|
||||
`07d5bfef2e815e8038362bb836b9c496c7ef9862`, reproduces overwritten examples and
|
||||
incorrect demo names, then checks validation, concurrent collision, rollback,
|
||||
externally edited output retention and redirected directories. It runs the actual
|
||||
old CLI path from a different directory and builds a generated fixture using
|
||||
`scripts/build.js`. Built package tests exercise package export conditions, aliases
|
||||
and synchronous results; strict source, NodeNext ESM/CJS and TS 4.3.5 consumers are
|
||||
compiled. The DOM test checks duplicate stylesheet/import ownership with linkedom;
|
||||
it is not browser playback evidence for a future plugin's features.
|
||||
|
||||
Fixtures live under the ignored `refactor/.cache/` and are removed through checked
|
||||
paths. Tests do not add a 23rd workspace or alter real examples. Root `test:node`
|
||||
includes this suite, and `ci:check` includes the strict scaffold typecheck. Remote
|
||||
CI execution remains a separate release requirement.
|
||||
@@ -0,0 +1,16 @@
|
||||
import { publishPlugin } from './publish.ts'
|
||||
import { renderPlugin } from './render.ts'
|
||||
|
||||
export function createPlugin(root: string, args: string[]): void {
|
||||
if (args.length === 1 && args[0] === '--help') {
|
||||
console.log('Usage: yarn create:plugin <lowercase-plugin-name>')
|
||||
return
|
||||
}
|
||||
if (args.length !== 1 || !args[0])
|
||||
throw new Error('Provide exactly one plugin name. Use yarn create:plugin --help')
|
||||
const name = args[0]
|
||||
publishPlugin(root, name, renderPlugin(name))
|
||||
console.log(`Created artplayer-plugin-${name} and example ${name.replaceAll('-', '.')}.js`)
|
||||
console.log(`Next: yarn build artplayer-plugin-${name}; yarn workspace artplayer-plugin-${name} test`)
|
||||
console.log('Register the new workspace/demo in the refactor inventories and update the root Yarn lock before CI.')
|
||||
}
|
||||
+6
-102
@@ -1,107 +1,11 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createPlugin } from './cli.ts'
|
||||
|
||||
// Get the current file path
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// Get the plugin name from command line arguments
|
||||
const args = process.argv.slice(2)
|
||||
const pluginName = args[0]
|
||||
|
||||
if (!pluginName) {
|
||||
console.error('Please provide a plugin name, e.g., npm run create:plugin somePluginName')
|
||||
process.exit(1)
|
||||
try {
|
||||
createPlugin(fileURLToPath(new URL('../../', import.meta.url)), process.argv.slice(2))
|
||||
}
|
||||
|
||||
// Validate plugin name
|
||||
const pluginNameRegex = /^[a-z-]+$/
|
||||
if (!pluginNameRegex.test(pluginName)) {
|
||||
console.error('Invalid plugin name. Only lowercase letters and hyphens are allowed.')
|
||||
process.exit(1)
|
||||
catch (error) {
|
||||
console.error(error instanceof Error ? error.message : error)
|
||||
process.exitCode = 1
|
||||
}
|
||||
|
||||
// Convert plugin name to export name (PascalCase)
|
||||
const exportName = pluginName
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join('')
|
||||
const exportNameFull = `artplayerPlugin${exportName}`
|
||||
|
||||
// Define paths
|
||||
const templateDir = path.join(__dirname, 'template')
|
||||
const destDir = path.join(__dirname, '../../packages', `artplayer-plugin-${pluginName}`)
|
||||
const exampleDir = path.join(__dirname, '../../docs/assets/example')
|
||||
const exampleFileName = `${pluginName.replace(/-/g, '.')}.js`
|
||||
const exampleFile = path.join(exampleDir, exampleFileName)
|
||||
|
||||
// Check if a plugin with the same name already exists
|
||||
if (fs.existsSync(destDir)) {
|
||||
console.error(`Plugin ${pluginName} already exists. Please choose another name.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Create the destination directory
|
||||
fs.mkdirSync(destDir, { recursive: true })
|
||||
|
||||
// Read files from the template directory and copy them to the destination directory
|
||||
function copyTemplateFiles(templateDir, destDir) {
|
||||
const files = fs.readdirSync(templateDir)
|
||||
|
||||
files.forEach((file) => {
|
||||
const templateFilePath = path.join(templateDir, file)
|
||||
let destFilePath = path.join(destDir, file)
|
||||
|
||||
const stats = fs.statSync(templateFilePath)
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
fs.mkdirSync(destFilePath, { recursive: true })
|
||||
copyTemplateFiles(templateFilePath, destFilePath)
|
||||
}
|
||||
else {
|
||||
// Replace placeholders in the file name
|
||||
destFilePath = destFilePath.replace(/\{\{name\}\}/g, pluginName)
|
||||
|
||||
let content = fs.readFileSync(templateFilePath, 'utf8')
|
||||
|
||||
// Replace placeholders in the file content
|
||||
content = content.replace(/\{\{name\}\}/g, pluginName)
|
||||
content = content.replace(/\{\{export\}\}/g, exportNameFull)
|
||||
|
||||
fs.writeFileSync(destFilePath, content)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Create the example file
|
||||
function createExampleFile() {
|
||||
const exampleContent = `
|
||||
// npm i artplayer-plugin-${pluginName}
|
||||
// import ${exportNameFull} from 'artplayer-plugin-${pluginName}';
|
||||
|
||||
var art = new Artplayer({
|
||||
container: '.artplayer-app',
|
||||
url: '/assets/sample/video.mp4',
|
||||
plugins: [
|
||||
${exportNameFull}({
|
||||
//
|
||||
}),
|
||||
],
|
||||
});
|
||||
`.trim()
|
||||
|
||||
if (!fs.existsSync(exampleDir)) {
|
||||
fs.mkdirSync(exampleDir, { recursive: true })
|
||||
}
|
||||
|
||||
fs.writeFileSync(exampleFile, exampleContent)
|
||||
console.log(`Example file created at ${exampleFile}`)
|
||||
}
|
||||
|
||||
// Start copying template files and creating example file
|
||||
copyTemplateFiles(templateDir, destDir)
|
||||
createExampleFile()
|
||||
|
||||
console.log(`Plugin ${pluginName} has been successfully created at ${destDir}`)
|
||||
@@ -0,0 +1,96 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
function checkedPath(root: string, relative: string): string {
|
||||
const target = path.resolve(root, relative)
|
||||
const within = path.relative(root, target)
|
||||
if (!within || within.startsWith('..') || path.isAbsolute(within))
|
||||
throw new Error(`Output escapes the repository: ${relative}`)
|
||||
let current = root
|
||||
for (const part of within.split(path.sep)) {
|
||||
current = path.join(current, part)
|
||||
try {
|
||||
if (fs.lstatSync(current).isSymbolicLink())
|
||||
throw new Error(`Refusing redirected output: ${current}`)
|
||||
}
|
||||
catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
function removeStage(stage: string, packages: string): void {
|
||||
if (fs.realpathSync(stage) !== stage || path.dirname(stage) !== packages || !path.basename(stage).startsWith('.artplayer-scaffold-'))
|
||||
throw new Error(`Refusing redirected scaffold cleanup: ${stage}`)
|
||||
fs.rmSync(stage, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
export function publishPlugin(root: string, name: string, files: ReadonlyMap<string, string>, link: typeof fs.linkSync = fs.linkSync): void {
|
||||
root = fs.realpathSync(root)
|
||||
const packages = checkedPath(root, 'packages')
|
||||
if (!fs.statSync(packages).isDirectory())
|
||||
throw new Error('The repository packages directory is missing')
|
||||
const destination = checkedPath(root, `packages/artplayer-plugin-${name}`)
|
||||
if (fs.existsSync(destination))
|
||||
throw new Error(`Plugin ${name} already exists`)
|
||||
for (const relative of files.keys()) {
|
||||
if (fs.existsSync(checkedPath(root, relative)))
|
||||
throw new Error(`Refusing to overwrite existing output: ${relative}`)
|
||||
}
|
||||
const stage = fs.mkdtempSync(path.join(packages, '.artplayer-scaffold-'))
|
||||
const createdDirectories: string[] = []
|
||||
const createdFiles: { target: string, content: string }[] = []
|
||||
function directory(target: string) {
|
||||
if (fs.existsSync(target))
|
||||
return
|
||||
directory(path.dirname(target))
|
||||
fs.mkdirSync(target)
|
||||
createdDirectories.push(target)
|
||||
}
|
||||
try {
|
||||
let index = 0
|
||||
for (const content of files.values())
|
||||
fs.writeFileSync(path.join(stage, String(index++)), content, { flag: 'wx' })
|
||||
// Reserve the package name without replacing even an empty concurrently-created directory.
|
||||
fs.mkdirSync(destination)
|
||||
createdDirectories.push(destination)
|
||||
index = 0
|
||||
for (const [relative, content] of files) {
|
||||
const target = checkedPath(root, relative)
|
||||
directory(path.dirname(target))
|
||||
link(path.join(stage, String(index++)), target)
|
||||
createdFiles.push({ target, content })
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
const retained: string[] = []
|
||||
for (const { target, content } of createdFiles.reverse()) {
|
||||
try {
|
||||
if (fs.lstatSync(target).isSymbolicLink() || fs.readFileSync(target, 'utf8') !== content)
|
||||
retained.push(target)
|
||||
else fs.unlinkSync(target)
|
||||
}
|
||||
catch (failure) {
|
||||
if ((failure as NodeJS.ErrnoException).code !== 'ENOENT')
|
||||
retained.push(target)
|
||||
}
|
||||
}
|
||||
for (const directory of createdDirectories.reverse()) {
|
||||
try {
|
||||
fs.rmdirSync(directory)
|
||||
}
|
||||
catch (failure) {
|
||||
if ((failure as NodeJS.ErrnoException).code !== 'ENOENT')
|
||||
retained.push(directory)
|
||||
}
|
||||
}
|
||||
if (retained.length)
|
||||
throw new Error(`Scaffolding failed; preserve changed outputs for recovery: ${retained.join(', ')}`, { cause: error })
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
removeStage(stage, packages)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export function renderPlugin(name: string, template = fileURLToPath(new URL('./template', import.meta.url))): Map<string, string> {
|
||||
if (!/^[a-z]+(?:-[a-z]+)*$/.test(name))
|
||||
throw new Error('Use lowercase words separated by single hyphens for the plugin name')
|
||||
const replacements: Record<string, string> = {
|
||||
name,
|
||||
export: `artplayerPlugin${name.split('-').map(word => word[0]!.toUpperCase() + word.slice(1)).join('')}`,
|
||||
example: name.replaceAll('-', '.'),
|
||||
}
|
||||
const replace = (text: string) => text.replace(/\{\{(\w+)\}\}/g, (_, key: string) => {
|
||||
if (!Object.hasOwn(replacements, key))
|
||||
throw new Error(`Unknown template placeholder: ${key}`)
|
||||
return replacements[key]!
|
||||
})
|
||||
const files = new Map<string, string>()
|
||||
function visit(directory: string, relative = '') {
|
||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
const source = path.join(directory, entry.name)
|
||||
const target = path.posix.join(relative, entry.name)
|
||||
if (entry.isSymbolicLink())
|
||||
throw new Error(`Template links are not supported: ${source}`)
|
||||
if (entry.isDirectory()) {
|
||||
visit(source, target)
|
||||
}
|
||||
else if (entry.isFile()) {
|
||||
const output = `packages/artplayer-plugin-${name}/${replace(target).replace(/\.tpl$/, '')}`
|
||||
if (files.has(output))
|
||||
throw new Error(`Duplicate template output: ${output}`)
|
||||
files.set(output, replace(fs.readFileSync(source, 'utf8')).replaceAll('\r\n', '\n'))
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unsupported template entry: ${source}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
visit(template)
|
||||
files.set(`docs/assets/example/${replacements.example}.js`, `// npm i artplayer-plugin-${name}
|
||||
// import ${replacements.export} from 'artplayer-plugin-${name}';
|
||||
|
||||
var art = new Artplayer({
|
||||
container: '.artplayer-app',
|
||||
url: '/assets/sample/video.mp4',
|
||||
plugins: [${replacements.export}({})],
|
||||
});
|
||||
`)
|
||||
return files
|
||||
}
|
||||
@@ -4,7 +4,62 @@
|
||||
|
||||
## Demo
|
||||
|
||||
[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-plugin-{{name}}/index.js&example={{name}})
|
||||
[Local demo](http://localhost:8082/?libs=./uncompiled/artplayer-plugin-{{name}}/index.js&example={{example}})
|
||||
|
||||
Run `yarn dev artplayer-plugin-{{name}}` from the repository root first.
|
||||
The generated example is `docs/assets/example/{{example}}.js`.
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import {{export}} from 'artplayer-plugin-{{name}}'
|
||||
|
||||
const art = new Artplayer({
|
||||
container: '.artplayer-app',
|
||||
url: '/assets/sample/video.mp4',
|
||||
plugins: [{{export}}({})],
|
||||
})
|
||||
```
|
||||
|
||||
The factory also supports direct CommonJS `require`, its `.default` alias,
|
||||
the `/legacy` entry, and the browser global `{{export}}`.
|
||||
This scaffold only registers a name; implement and test the intended behavior.
|
||||
|
||||
## Maintenance
|
||||
|
||||
| File | Responsibility |
|
||||
| --- | --- |
|
||||
| `src/index.ts` | Synchronous plugin factory and registration result |
|
||||
| `src/stylesheet.ts` | One stylesheet per document on module evaluation; safe to import without a DOM |
|
||||
| `src/style.less` | Plugin styles; retain the package selector when adding UI |
|
||||
| `src/assets.d.ts` | Build-time inline stylesheet declaration |
|
||||
| `types/api.d.ts` | Owned option/result types shared by source and public entrypoints |
|
||||
| `types/artplayer-plugin-{{name}}.d.ts` | CommonJS and older TypeScript entry |
|
||||
| `types/artplayer-plugin-{{name}}.d.mts` | Native ESM declaration entry |
|
||||
| `test/plugin.test.mjs` | Built ESM/CommonJS/legacy synchronous factory and alias contracts |
|
||||
|
||||
Replace the empty `Option` contract with explicit fields when implementing behavior.
|
||||
Keep the return value synchronous unless the plugin explicitly requires an async contract.
|
||||
Keep source and both declaration entrypoints consistent; test real consumers when changing them.
|
||||
The stylesheet has document lifetime and is shared by instances. Resources added by the
|
||||
plugin (listeners, timers, requests, DOM, workers) need per-instance ownership and cleanup
|
||||
on `art.on('destroy', ...)`; guard late callbacks and source changes. Add behavior-specific
|
||||
normal, error, concurrency and cleanup tests, plus real browser checks for media/UI work.
|
||||
|
||||
Use the repository Node version from `.node-version` and Yarn Classic 1.22.22:
|
||||
|
||||
```sh
|
||||
yarn workspace artplayer-plugin-{{name}} typecheck
|
||||
yarn build artplayer-plugin-{{name}}
|
||||
yarn workspace artplayer-plugin-{{name}} test
|
||||
```
|
||||
|
||||
Build before testing; the tests consume the actual distribution files. ESM and modern
|
||||
UMD target es2020, legacy UMD targets es2015. The generator uses the existing toolchain;
|
||||
it does not install dependencies, publish, or update the root lockfile. Register this
|
||||
workspace/demo in the refactor inventories and validation mappings before CI, and update
|
||||
only the root `yarn.lock`. The declared ArtPlayer peer does not itself prove compatibility:
|
||||
test the core versions and devices required by the implemented features.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -19,20 +19,41 @@
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/artplayer-plugin-{{name}}.d.ts",
|
||||
"import": "./dist/artplayer-plugin-{{name}}.mjs",
|
||||
"require": "./dist/artplayer-plugin-{{name}}.js"
|
||||
"import": {
|
||||
"types": "./types/artplayer-plugin-{{name}}.d.mts",
|
||||
"default": "./dist/artplayer-plugin-{{name}}.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./types/artplayer-plugin-{{name}}.d.ts",
|
||||
"default": "./dist/artplayer-plugin-{{name}}.js"
|
||||
},
|
||||
"default": "./dist/artplayer-plugin-{{name}}.js"
|
||||
},
|
||||
"./legacy": {
|
||||
"types": "./types/artplayer-plugin-{{name}}.d.ts",
|
||||
"import": "./dist/artplayer-plugin-{{name}}.legacy.js",
|
||||
"require": "./dist/artplayer-plugin-{{name}}.legacy.js"
|
||||
|
||||
}
|
||||
"import": {
|
||||
"types": "./types/artplayer-plugin-{{name}}.d.mts",
|
||||
"default": "./dist/artplayer-plugin-{{name}}.legacy.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./types/artplayer-plugin-{{name}}.d.ts",
|
||||
"default": "./dist/artplayer-plugin-{{name}}.legacy.js"
|
||||
},
|
||||
"default": "./dist/artplayer-plugin-{{name}}.legacy.js"
|
||||
},
|
||||
"./dist/*": "./dist/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "dist/artplayer-plugin-{{name}}.js",
|
||||
"module": "./dist/artplayer-plugin-{{name}}.mjs",
|
||||
"types": "types/artplayer-plugin-{{name}}.d.ts",
|
||||
"legacy": "dist/artplayer-plugin-{{name}}.legacy.js",
|
||||
"files": ["README.md", "dist", "types"],
|
||||
"scripts": {
|
||||
"typecheck": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json --noEmit",
|
||||
"test": "node --test test/plugin.test.mjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"artplayer": "*"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
declare module '*.less?inline' {
|
||||
const css: string
|
||||
export default css
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import style from './style.less?inline';
|
||||
|
||||
export default function {{export}}(option) {
|
||||
return (art) => {
|
||||
return {
|
||||
name: '{{export}}',
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
const id = 'artplayer-plugin-{{name}}';
|
||||
let $style = document.getElementById(id)
|
||||
if (!$style) {
|
||||
$style = document.createElement('style')
|
||||
$style.id = id
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.head.appendChild($style)
|
||||
})
|
||||
}
|
||||
else {
|
||||
(document.head || document.documentElement).appendChild($style)
|
||||
}
|
||||
}
|
||||
$style.textContent = style
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { Option, Result } from '../types/api.js'
|
||||
import './stylesheet'
|
||||
|
||||
function {{export}}(_option: Option = {}) {
|
||||
return (_art: Artplayer): Result => ({ name: '{{export}}' })
|
||||
}
|
||||
|
||||
export default Object.assign({{export}}, { default: {{export}} })
|
||||
@@ -1,3 +1,3 @@
|
||||
.artplayer-plugin-{{name}} {
|
||||
//
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import css from './style.less?inline'
|
||||
|
||||
// The stylesheet belongs to the module, not individual player instances.
|
||||
if (typeof document !== 'undefined') {
|
||||
const id = 'artplayer-plugin-{{name}}'
|
||||
if (!document.getElementById(id)) {
|
||||
const style = document.createElement('style')
|
||||
style.id = id
|
||||
style.textContent = css
|
||||
;(document.head || document.documentElement).appendChild(style)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createRequire } from 'node:module'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Verify the generated distribution contracts.
|
||||
import test from 'node:test'
|
||||
import plugin from 'artplayer-plugin-{{name}}'
|
||||
import legacy from 'artplayer-plugin-{{name}}/legacy'
|
||||
|
||||
test('classic factory and historical default alias return the synchronous plugin result', () => {
|
||||
const require = createRequire(import.meta.url)
|
||||
for (const factory of [plugin, legacy, require('artplayer-plugin-{{name}}'), require('artplayer-plugin-{{name}}/legacy')]) {
|
||||
assert.equal(factory.default, factory)
|
||||
const result = factory({})({})
|
||||
assert.deepEqual(result, { name: '{{export}}' })
|
||||
assert.equal(result instanceof Promise, false)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src", "types"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
|
||||
// Replace this empty option type with the documented options your plugin owns.
|
||||
export type Option = Record<string, never>
|
||||
export interface Result { name: '{{export}}' }
|
||||
export type Factory = (option?: Option) => (art: Artplayer) => Result
|
||||
export type RuntimeFactory = Factory & { default: Factory }
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { RuntimeFactory } from './api.js'
|
||||
|
||||
declare const {{export}}: RuntimeFactory
|
||||
export default {{export}}
|
||||
export type { Factory, Option, Result, RuntimeFactory } from './api.js'
|
||||
@@ -1,13 +0,0 @@
|
||||
import type Artplayer from 'artplayer';
|
||||
|
||||
interface Option {
|
||||
//
|
||||
}
|
||||
|
||||
interface Result {
|
||||
name: '{{export}}';
|
||||
}
|
||||
|
||||
declare const {{export}}: (option: Option) => (art: Artplayer) => Result;
|
||||
|
||||
export default {{export}}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { RuntimeFactory } from './api.js'
|
||||
|
||||
declare const {{export}}: RuntimeFactory
|
||||
// eslint-disable-next-line ts/no-redeclare -- Expose types on the CommonJS factory.
|
||||
declare namespace {{export}} {
|
||||
type Option = import('./api.js').Option
|
||||
type Result = import('./api.js').Result
|
||||
type Factory = import('./api.js').Factory
|
||||
type RuntimeFactory = import('./api.js').RuntimeFactory
|
||||
}
|
||||
export = {{export}}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node"],
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"allowImportingTsExtensions": true,
|
||||
"checkJs": true
|
||||
},
|
||||
"include": ["plugin/*.ts", "plugin/create.js"]
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
# Tests and fixture ownership
|
||||
|
||||
`yarn test:scaffold` covers old plugin-generator defects, exclusive writes and
|
||||
rollback, the historical CLI path, and actual generated package builds and type
|
||||
consumers. It runs in ignored fixtures without adding a workspace or real demo;
|
||||
see `scripts/plugin/README.md`. `yarn typecheck:scaffold` checks its TS modules and
|
||||
JS command shim. Both are wired into the existing Node/CI checks.
|
||||
|
||||
`site-loading.test.js` freezes the old mobile loader's failure/race reproduction
|
||||
and checks shared loader ownership, retries/cancellation, query encoding and
|
||||
language rules. Actual local pages and Monaco are covered by site-loading.spec.js.
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync, spawnSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Exercise scaffold rollback and actual generated consumers.
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import vm from 'node:vm'
|
||||
import { parseHTML } from 'linkedom'
|
||||
import { createPlugin } from '../scripts/plugin/cli.ts'
|
||||
import { publishPlugin } from '../scripts/plugin/publish.ts'
|
||||
import { renderPlugin } from '../scripts/plugin/render.ts'
|
||||
|
||||
const repository = fileURLToPath(new URL('../', import.meta.url))
|
||||
const cache = path.join(repository, 'refactor/.cache')
|
||||
const baseline = '07d5bfef2e815e8038362bb836b9c496c7ef9862'
|
||||
const name = 'scaffold-test'
|
||||
const packageName = `artplayer-plugin-${name}`
|
||||
|
||||
function fixture(t) {
|
||||
fs.mkdirSync(cache, { recursive: true })
|
||||
const root = fs.mkdtempSync(path.join(cache, 'plugin-scaffold-'))
|
||||
fs.mkdirSync(path.join(root, 'packages'))
|
||||
t.after(() => {
|
||||
assert.equal(fs.realpathSync(root), root)
|
||||
assert.equal(path.dirname(root), fs.realpathSync(cache))
|
||||
assert.ok(path.basename(root).startsWith('plugin-scaffold-'))
|
||||
fs.rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
return root
|
||||
}
|
||||
|
||||
function write(root, relative, text) {
|
||||
const file = path.join(root, relative)
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true })
|
||||
fs.writeFileSync(file, text)
|
||||
return file
|
||||
}
|
||||
|
||||
function run(args, cwd) {
|
||||
const env = { ...process.env }
|
||||
delete env.NODE_TEST_CONTEXT
|
||||
const result = spawnSync(process.execPath, args, { cwd, env, encoding: 'utf8', timeout: 120000 })
|
||||
assert.equal(result.status, 0, `${args.join(' ')}\n${result.stdout}\n${result.stderr}\n${result.error || ''}`)
|
||||
return result.stdout
|
||||
}
|
||||
|
||||
test('historical generator overwrites an existing example and produces a mismatched demo link', (t) => {
|
||||
const root = fixture(t)
|
||||
const files = execFileSync('git', ['ls-tree', '-r', '--name-only', baseline, 'scripts/plugin'], { cwd: repository, encoding: 'utf8' }).trim().split(/\r?\n/)
|
||||
for (const relative of files)
|
||||
write(root, relative, execFileSync('git', ['show', `${baseline}:${relative}`], { cwd: repository }))
|
||||
write(root, 'package.json', '{"type":"module"}')
|
||||
const example = write(root, 'docs/assets/example/scaffold.test.js', '// owned example')
|
||||
run(['scripts/plugin/create.js', name], root)
|
||||
assert.notEqual(fs.readFileSync(example, 'utf8'), '// owned example')
|
||||
const readme = fs.readFileSync(path.join(root, `packages/${packageName}/README.md`), 'utf8')
|
||||
assert.match(readme, /example=scaffold-test/)
|
||||
assert.equal(fs.existsSync(path.join(root, 'docs/assets/example/scaffold-test.js')), false)
|
||||
})
|
||||
|
||||
test('rendering creates consistent names and deterministic typed templates without writing', () => {
|
||||
const files = renderPlugin(name)
|
||||
assert.deepEqual(files, renderPlugin(name))
|
||||
assert.ok(files.has(`packages/${packageName}/src/index.ts`))
|
||||
assert.ok(files.has('docs/assets/example/scaffold.test.js'))
|
||||
assert.match(files.get(`packages/${packageName}/README.md`), /example=scaffold.test/)
|
||||
for (const [file, content] of files) {
|
||||
assert.doesNotMatch(file, /\{\{|\.tpl$/)
|
||||
assert.doesNotMatch(content, /\{\{/)
|
||||
}
|
||||
for (const invalid of ['', '-', '-name', 'name-', 'two--words', '../escape', 'Upper', 'foo/bar', 'a_b'])
|
||||
assert.throws(() => renderPlugin(invalid), /lowercase/)
|
||||
})
|
||||
|
||||
test('unknown placeholders and duplicate rendered paths fail before publishing', (t) => {
|
||||
const root = fixture(t)
|
||||
const template = path.join(root, 'template')
|
||||
write(root, 'template/index.ts.tpl', '{{toString}}')
|
||||
assert.throws(() => renderPlugin(name, template), /Unknown template placeholder/)
|
||||
fs.writeFileSync(path.join(template, 'index.ts.tpl'), 'export {}')
|
||||
write(root, 'template/index.ts', 'export {}')
|
||||
assert.throws(() => renderPlugin(name, template), /Duplicate template output/)
|
||||
assert.deepEqual(fs.readdirSync(path.join(root, 'packages')), [])
|
||||
})
|
||||
|
||||
test('existing packages and examples are preserved including empty package directories', (t) => {
|
||||
const root = fixture(t)
|
||||
const example = write(root, 'docs/assets/example/scaffold.test.js', '// owned example')
|
||||
assert.throws(() => publishPlugin(root, name, renderPlugin(name)), /overwrite/)
|
||||
assert.equal(fs.readFileSync(example, 'utf8'), '// owned example')
|
||||
assert.deepEqual(fs.readdirSync(path.join(root, 'packages')), [])
|
||||
fs.mkdirSync(path.join(root, `packages/${packageName}`))
|
||||
assert.throws(() => publishPlugin(root, name, renderPlugin(name)), /already exists/)
|
||||
})
|
||||
|
||||
test('partial write failure removes only generated files and newly created directories', (t) => {
|
||||
const root = fixture(t)
|
||||
const sentinel = write(root, 'docs/keep.txt', 'keep')
|
||||
let count = 0
|
||||
assert.throws(() => publishPlugin(root, name, renderPlugin(name), (from, to) => {
|
||||
if (++count === 5)
|
||||
throw new Error('injected write failure')
|
||||
fs.linkSync(from, to)
|
||||
}), /injected write failure/)
|
||||
assert.deepEqual(fs.readdirSync(path.join(root, 'packages')), [])
|
||||
assert.deepEqual(fs.readdirSync(path.join(root, 'docs')), ['keep.txt'])
|
||||
assert.equal(fs.readFileSync(sentinel, 'utf8'), 'keep')
|
||||
})
|
||||
|
||||
test('a concurrent example collision cannot overwrite the other writer', (t) => {
|
||||
const root = fixture(t)
|
||||
assert.throws(() => publishPlugin(root, name, renderPlugin(name), (from, to) => {
|
||||
if (to.endsWith('scaffold.test.js'))
|
||||
fs.writeFileSync(to, '// concurrent writer', { flag: 'wx' })
|
||||
fs.linkSync(from, to)
|
||||
}), /preserve changed outputs/)
|
||||
assert.deepEqual(fs.readdirSync(path.join(root, 'packages')), [])
|
||||
assert.equal(fs.readFileSync(path.join(root, 'docs/assets/example/scaffold.test.js'), 'utf8'), '// concurrent writer')
|
||||
})
|
||||
|
||||
test('rollback retains an externally edited generated file and reports its recovery path', (t) => {
|
||||
const root = fixture(t)
|
||||
let first
|
||||
assert.throws(() => publishPlugin(root, name, renderPlugin(name), (from, to) => {
|
||||
if (first) {
|
||||
fs.writeFileSync(first, 'external edit')
|
||||
throw new Error('injected interruption')
|
||||
}
|
||||
fs.linkSync(from, to)
|
||||
first = to
|
||||
}), error => error.message.includes(first) && error.cause.message === 'injected interruption')
|
||||
assert.equal(fs.readFileSync(first, 'utf8'), 'external edit')
|
||||
assert.deepEqual(fs.readdirSync(path.join(root, 'packages')), [packageName])
|
||||
})
|
||||
|
||||
test('redirected output directories are rejected before writing through them', (t) => {
|
||||
const root = fixture(t)
|
||||
const outside = fixture(t)
|
||||
fs.symlinkSync(outside, path.join(root, 'docs'), 'junction')
|
||||
assert.throws(() => publishPlugin(root, name, renderPlugin(name)), /redirected output/)
|
||||
assert.deepEqual(fs.readdirSync(outside), ['packages'])
|
||||
assert.deepEqual(fs.readdirSync(path.join(root, 'packages')), [])
|
||||
fs.unlinkSync(path.join(root, 'docs'))
|
||||
})
|
||||
|
||||
test('CLI validates arguments and builds a complete package without installing or changing the lock', (t) => {
|
||||
const root = fixture(t)
|
||||
const lock = write(root, 'yarn.lock', '# existing lock\n')
|
||||
assert.throws(() => createPlugin(root, [name, 'extra']), /exactly one/)
|
||||
createPlugin(root, ['--help'])
|
||||
assert.deepEqual(fs.readdirSync(path.join(root, 'packages')), [])
|
||||
createPlugin(root, [name])
|
||||
assert.equal(fs.readFileSync(lock, 'utf8'), '# existing lock\n')
|
||||
for (const [relative, content] of renderPlugin(name))
|
||||
assert.equal(fs.readFileSync(path.join(root, relative), 'utf8'), content)
|
||||
})
|
||||
|
||||
test('legacy CLI path uses its repository root even when launched from another working directory', (t) => {
|
||||
const root = fixture(t)
|
||||
const elsewhere = fixture(t)
|
||||
fs.cpSync(path.join(repository, 'scripts/plugin'), path.join(root, 'scripts/plugin'), { recursive: true })
|
||||
write(root, 'package.json', '{"type":"module"}')
|
||||
const command = path.join(root, 'scripts/plugin/create.js')
|
||||
run([command, name], elsewhere)
|
||||
assert.ok(fs.existsSync(path.join(root, `packages/${packageName}/src/index.ts`)))
|
||||
assert.deepEqual(fs.readdirSync(path.join(elsewhere, 'packages')), [])
|
||||
const invalid = spawnSync(process.execPath, [command, '../escape'], { cwd: elsewhere, encoding: 'utf8' })
|
||||
assert.equal(invalid.status, 1)
|
||||
assert.match(invalid.stderr, /lowercase/)
|
||||
})
|
||||
|
||||
test('generated package passes real production builds, consumers, strict types and stylesheet lifecycle', (t) => {
|
||||
const root = fixture(t)
|
||||
createPlugin(root, [name])
|
||||
fs.copyFileSync(path.join(repository, 'tsconfig.base.json'), path.join(root, 'tsconfig.base.json'))
|
||||
const pkg = path.join(root, `packages/${packageName}`)
|
||||
const compiler = path.join(repository, 'node_modules/typescript/bin/tsc')
|
||||
run([path.join(repository, 'node_modules/eslint/bin/eslint.js'), '--no-ignore', '--max-warnings', '0', 'src', 'types', 'test/plugin.test.mjs', 'package.json'], pkg)
|
||||
run([compiler, '-p', 'tsconfig.json', '--noEmit'], pkg)
|
||||
const output = run([path.join(repository, 'scripts/build.js'), packageName], root)
|
||||
assert.match(output, /Finished building 1 package/)
|
||||
const testOutput = run(['--test', 'test/plugin.test.mjs'], pkg)
|
||||
assert.match(testOutput, /pass 1/)
|
||||
const consumer = `import plugin from '${packageName}'
|
||||
import legacy from '${packageName}/legacy'
|
||||
import type Artplayer from 'artplayer'
|
||||
declare const art: Artplayer
|
||||
const result: {name:'artplayerPluginScaffoldTest'} = plugin({})(art)
|
||||
legacy.default()(art)
|
||||
// @ts-expect-error The scaffold owns no option fields yet.
|
||||
plugin({unknown: true})
|
||||
// @ts-expect-error Registration requires an ArtPlayer instance.
|
||||
plugin()({})
|
||||
void result
|
||||
`
|
||||
write(pkg, 'test/consumer.mts', consumer)
|
||||
write(pkg, 'test/consumer.cts', consumer.replace(`import plugin from '${packageName}'`, `import plugin = require('${packageName}')`))
|
||||
write(pkg, 'test/tsconfig.json', JSON.stringify({ compilerOptions: { strict: true, noEmit: true, types: [], module: 'NodeNext', moduleResolution: 'NodeNext', target: 'ES2020', esModuleInterop: true }, include: ['consumer.mts', 'consumer.cts'] }))
|
||||
run([compiler, '-p', 'test/tsconfig.json'], pkg)
|
||||
write(pkg, 'test/classic.ts', consumer.replaceAll(`'${packageName}/legacy'`, `'../types/${packageName}'`).replaceAll(`'${packageName}'`, `'../types/${packageName}'`))
|
||||
write(pkg, 'test/tsconfig.classic.json', JSON.stringify({ compilerOptions: { strict: true, noEmit: true, types: [], module: 'CommonJS', moduleResolution: 'Node', target: 'ES2020', esModuleInterop: true }, files: ['classic.ts'] }))
|
||||
run([path.join(repository, 'node_modules/typescript-compat/bin/tsc'), '-p', 'test/tsconfig.classic.json'], pkg)
|
||||
const { document } = parseHTML('<html><head></head><body></body></html>')
|
||||
const context = vm.createContext({ document })
|
||||
for (const suffix of ['.js', '.legacy.js']) {
|
||||
const code = fs.readFileSync(path.join(pkg, `dist/${packageName}${suffix}`), 'utf8')
|
||||
vm.runInNewContext(code, {}) // Importing the UMD bundle without a DOM must work.
|
||||
vm.runInContext(code, context)
|
||||
vm.runInContext(code, context)
|
||||
assert.equal(context.artplayerPluginScaffoldTest.default, context.artplayerPluginScaffoldTest)
|
||||
assert.equal(context.artplayerPluginScaffoldTest()({}).name, 'artplayerPluginScaffoldTest')
|
||||
}
|
||||
assert.equal(document.querySelectorAll(`style#${packageName}`).length, 1)
|
||||
})
|
||||
Reference in new issue
Block a user