refactor(tooling): [MOD-02] split library build and development into typed modules

This commit is contained in:
Harvey Zhao committed 2026-09-14 10:46:25 +08:00
1 parent 28c7abb2bb
commit 401d4cdd65
36 files changed
+1644 -427

No files matched your search

+6 -4
View File
@@ -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\" \"scripts/plugin/*.{js,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,library}/**/*.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\" \"scripts/plugin/*.{js,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,library}/**/*.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/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: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: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:library && 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",
@@ -129,7 +129,9 @@
"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",
"typecheck:scaffold": "node node_modules/typescript/bin/tsc -p scripts/tsconfig.scaffold.json --noEmit",
"test:scaffold": "node --test test/plugin-scaffold.test.js"
"test:scaffold": "node --test test/plugin-scaffold.test.js",
"typecheck:library": "node node_modules/typescript/bin/tsc -p scripts/tsconfig.library.json --noEmit",
"test:library": "node --test refactor/scripts/build.test.mjs test/library-build.test.js test/performance-report.test.js test/plugin-scaffold.test.js"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
@@ -0,0 +1,736 @@
{
"task": "MOD-02",
"date": "2026-09-14",
"environment": {
"os": "Windows",
"node": "24.21.0",
"yarn": "1.22.22",
"typescript": "5.9.3",
"browsers": {
"chromium": "153.0.8010.12",
"firefox": "155.0",
"webkit": "26.6"
}
},
"results": {
"library": {
"passed": 18,
"failed": 0
},
"ci": {
"passed": 50,
"failed": 0
},
"browser": {
"passed": 3,
"failed": 0,
"skipped": 0,
"report": "refactor/.cache/library-browser-final/report.json"
},
"strictTypes": "passed",
"docsTypes": "passed",
"toolchain": "passed",
"lint": {
"errors": 0,
"existingWarnings": 1
},
"baseline": {
"passed": 522,
"failed": 0,
"skipped": 0
}
},
"wholeBuildComparison": {
"task": "MOD-02",
"baseline": "28c7abb2bb8dfb7c84acfc7c5ec05c5882623069",
"node": "v24.21.0",
"names": [
"artplayer",
"artplayer-plugin-ads",
"artplayer-plugin-ambilight",
"artplayer-plugin-asr",
"artplayer-plugin-audio-track",
"artplayer-plugin-auto-thumbnail",
"artplayer-plugin-chapter",
"artplayer-plugin-chromecast",
"artplayer-plugin-danmuku",
"artplayer-plugin-danmuku-mask",
"artplayer-plugin-dash-control",
"artplayer-plugin-document-pip",
"artplayer-plugin-hls-control",
"artplayer-plugin-jassub",
"artplayer-plugin-multiple-subtitles",
"artplayer-plugin-vast",
"artplayer-plugin-vtt-thumbnail",
"artplayer-proxy-canvas",
"artplayer-proxy-mediabunny",
"artplayer-tool-iframe",
"artplayer-tool-thumbnail"
],
"before": [
{
"path": "packages/artplayer/dist/artplayer.js",
"sha256": "6dc8ca6d6eb34aa8a056cbc31328efd634530b3c5c60f0014cb7ce719fabc335"
},
{
"path": "packages/artplayer/dist/artplayer.legacy.js",
"sha256": "80f74a8a39df4baf64cc00c65d21cd57c011bfcdead0da38eea077fbc67c5c04"
},
{
"path": "packages/artplayer/dist/artplayer.mjs",
"sha256": "432e2b0ad92009a09743e020bf7c928c8b8f18a52496622b16259ec9ab20059f"
},
{
"path": "packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.js",
"sha256": "1429541242f49c4d558eda242a63b6e57a876c2dba06ce9f631d71d315028023"
},
{
"path": "packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.legacy.js",
"sha256": "4f64ac015e0850b71f51cf01d9fc6f238c95414730c71e91d1240c40e28499d8"
},
{
"path": "packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.mjs",
"sha256": "8ad7df8145b2514467cac0f733151619c36fa965e620df575c0779604372503f"
},
{
"path": "packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.js",
"sha256": "21ee5227f26cfff6e82ee6932f6eafc9741fa3be0ec2f5252a47b53ceea14599"
},
{
"path": "packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.legacy.js",
"sha256": "c3ed1a54e44a0202811d70378cb19edf87b1ebd010d05fe96ebe64d0b4301d5d"
},
{
"path": "packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.mjs",
"sha256": "ef126959819c4973da27dbb1e60ccccc54386c23f8c7750ce1ba4d38f306aaaa"
},
{
"path": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.js",
"sha256": "06b386f6a4744801f4153944ed6e2bd37822a85e177aa2a0d800c30002ef87bf"
},
{
"path": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.legacy.js",
"sha256": "639ae9bdede0e26c8af0251b9c1ed12e7fba9e0b6b79cb0556290741040f9697"
},
{
"path": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.mjs",
"sha256": "1b7bac676d2c4976046b08fd7c1ec25724ff24952f5d93fc546e219466d975d8"
},
{
"path": "packages/artplayer-plugin-audio-track/dist/artplayer-plugin-audio-track.js",
"sha256": "23b10856628c5568f1aa7ad88e926c7da8d96edfadf6df872777062a78a4b4be"
},
{
"path": "packages/artplayer-plugin-audio-track/dist/artplayer-plugin-audio-track.legacy.js",
"sha256": "41b94e84e3d3729b9373cb13de2f64d5ea30b4c36627f38583bf4c3c9bc2160c"
},
{
"path": "packages/artplayer-plugin-audio-track/dist/artplayer-plugin-audio-track.mjs",
"sha256": "9caa5a6e11a2e777760dd2835426842a87d04fb081ea2e7d48c7229035fd89a9"
},
{
"path": "packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.js",
"sha256": "430ac17e9025d3cc6bf37bd83316db5067bf438b30c2727b43c3e5fc4910e1d0"
},
{
"path": "packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.legacy.js",
"sha256": "efb30986ffc6a28859ca37126af82824601f25f0a42f7ea2c75a41f3da2c50ef"
},
{
"path": "packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs",
"sha256": "6080dd90bde239515646899bdd8e71d17de24119523e4883025fbf39b1a6a302"
},
{
"path": "packages/artplayer-plugin-chapter/dist/artplayer-plugin-chapter.js",
"sha256": "179c3ea90ddc85da27b46f000110e32d464dfa5c8c394e98316dc1b75d6f2a72"
},
{
"path": "packages/artplayer-plugin-chapter/dist/artplayer-plugin-chapter.legacy.js",
"sha256": "dade919ed044cbf46ac16e505f538f86a86edccf177da2e14d79d111559d4bef"
},
{
"path": "packages/artplayer-plugin-chapter/dist/artplayer-plugin-chapter.mjs",
"sha256": "754fbcfc60add74d6b6ad3bc8db253df3eb14ace13ac8a5439be0592bd5512d0"
},
{
"path": "packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.js",
"sha256": "80b12b3081086488c9d80f03979c336d4a6fdb44dca298e218dfeaaadb295d00"
},
{
"path": "packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.legacy.js",
"sha256": "b21a30852403914a3bf3defc89aff11f2cc23b044ed68ccf667164b477f04172"
},
{
"path": "packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.mjs",
"sha256": "68d81fcab469f74fe9d9740842d989e10144f7ff28c7ade91ed22adee90f44d7"
},
{
"path": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js",
"sha256": "133b7412e1a468b34f4f7ab3779958259f13899e037833029931f02b0833d088"
},
{
"path": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js",
"sha256": "9f0cb9969419a7d0c02d54790c423353b83f775acd95fbdbaeeb29a7fc42c816"
},
{
"path": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.mjs",
"sha256": "356406cda2e3e47376a87c939b76fb7c55f86d7c3829127da625b01241c67a2f"
},
{
"path": "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.js",
"sha256": "406a8c28207d386cffe1e61dc79afd8a6a9d5cbd1dc8e1661b4febbf48182f55"
},
{
"path": "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.legacy.js",
"sha256": "62d61dcedd3e90fa480589c2f1e3168c97635232c98ffe8c25c7918a2f3c66db"
},
{
"path": "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.mjs",
"sha256": "51936576383129170125b33d2983858e0841ea507368970f6d154bc9d78583b1"
},
{
"path": "packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.js",
"sha256": "a594009be5c17406844f817c5a79930c6ed60add4a5ef0578783f163a1bae012"
},
{
"path": "packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.legacy.js",
"sha256": "b00a837a99f7a7cccd415af335cbf0c68f1e4267fa7cfd8d91e945e3dc634151"
},
{
"path": "packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.mjs",
"sha256": "0c5a7e4d84e9641778e0a22ff9c78db9bd4ac2f08f51358519443046a528fb93"
},
{
"path": "packages/artplayer-plugin-document-pip/dist/artplayer-plugin-document-pip.js",
"sha256": "56440aad3ed06be55ec3a57d781f7de239e9aecf40a067e88f2cffcd01a86d6a"
},
{
"path": "packages/artplayer-plugin-document-pip/dist/artplayer-plugin-document-pip.legacy.js",
"sha256": "67dd14de3cc16a04ae6c1938dcba61b1355f4a15e5fa06f62a41b18d14d4d356"
},
{
"path": "packages/artplayer-plugin-document-pip/dist/artplayer-plugin-document-pip.mjs",
"sha256": "541b4ad6e1cbadd9156ee1f317559c97b9fddae37668ef32046f1fa47c392ec2"
},
{
"path": "packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.js",
"sha256": "8e163d4ab6734cbc45e7f539de7db644c7faf36d0318469a86d300bf6365cca6"
},
{
"path": "packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.legacy.js",
"sha256": "fd7be1c5bf8f994b61fc040ff96eb628fc442568d99f4b5fee4f611bac1d28c0"
},
{
"path": "packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.mjs",
"sha256": "64286498c1e90c3927236318383db193698a9dcbf39f91e93fc2b4ea2239e786"
},
{
"path": "packages/artplayer-plugin-jassub/dist/artplayer-plugin-jassub.js",
"sha256": "fb8cd8b7b6fb57a7866f929cfe20f7f582c86af96101f23c572dd79b4912dc9f"
},
{
"path": "packages/artplayer-plugin-jassub/dist/artplayer-plugin-jassub.legacy.js",
"sha256": "e19fea01316f78245e4508999bc441c483cfb47abfa6b5afbba529af3d05bfd8"
},
{
"path": "packages/artplayer-plugin-jassub/dist/artplayer-plugin-jassub.mjs",
"sha256": "d5f9df0621de4b5a9a5a8a232e290f99a1d8a2eda1bc0f8a34a2cb530c95653e"
},
{
"path": "packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.js",
"sha256": "14f4de91201f6b2cd55ea22a042d456c7149e4a38c22bc2ee0107f8a5e417446"
},
{
"path": "packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.legacy.js",
"sha256": "2059a43e4634b6c1a924a1cb451c2348356d6c20d26628b975b1d3545394d323"
},
{
"path": "packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.mjs",
"sha256": "07e4f628b4038d7c250f6242c410ddb188148b4ed58bb592975532666412e3e2"
},
{
"path": "packages/artplayer-plugin-vast/dist/artplayer-plugin-vast.js",
"sha256": "874803ec726f04649bb636c1376de9e038754310ab0016a22da44f189b7a5ce0"
},
{
"path": "packages/artplayer-plugin-vast/dist/artplayer-plugin-vast.legacy.js",
"sha256": "8a4166838aae5d150dc1a18cc594fb038df425df42eff1944e0d095df060dbb9"
},
{
"path": "packages/artplayer-plugin-vast/dist/artplayer-plugin-vast.mjs",
"sha256": "ffbab3a7cd6c1852c42a726acd0e0a5dbbb636dc9e8c2e8a084df962ff6ef860"
},
{
"path": "packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.js",
"sha256": "df70ccb5261fd988ba8c2c2d689ca58653bb5426242ffd1076569a1d225997a7"
},
{
"path": "packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.legacy.js",
"sha256": "732ade2c647281cf26446d6c7433e916f8189e2fdf2687127a56556c415544b1"
},
{
"path": "packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.mjs",
"sha256": "d35e28015df65e27da0fc01d06db07bad58e08d498b2d11fedfea35e6c4339c1"
},
{
"path": "packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.js",
"sha256": "a901e5c5fc00a64a6c2ae9d4b2b846950412710ec9672d30640d40987d4f9e73"
},
{
"path": "packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.legacy.js",
"sha256": "052263fdd7ab657a0183d874a706c2cf82527233c5e60b58158caa0677718b97"
},
{
"path": "packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.mjs",
"sha256": "66ca7271f52545a8796f563f2d5ca04581bed8d60d218463923325acf5b73ccf"
},
{
"path": "packages/artplayer-proxy-mediabunny/dist/artplayer-proxy-mediabunny.js",
"sha256": "23ce59f86e7159eedd4c371219c5ed58db83341e1f3fa3c67dcb8f1259e243cb"
},
{
"path": "packages/artplayer-proxy-mediabunny/dist/artplayer-proxy-mediabunny.legacy.js",
"sha256": "2c523a4535ae71213d279b54abf3b1d164c81e3d8abe3d9c9906e9a9ac840c26"
},
{
"path": "packages/artplayer-proxy-mediabunny/dist/artplayer-proxy-mediabunny.mjs",
"sha256": "3126446aa6461b78e56855ac851a2abf5a12359c7354e5564af6efaa0903f3fa"
},
{
"path": "packages/artplayer-tool-iframe/dist/artplayer-tool-iframe.js",
"sha256": "7408fceccb000e1b304abe30ac9bc364da7860c3896ad5aaa6af427804c237e6"
},
{
"path": "packages/artplayer-tool-iframe/dist/artplayer-tool-iframe.legacy.js",
"sha256": "4e8bd39cdc37e98bff9cd19b5e0c9453ebe5231264242fc1c573aa96edb5ea8a"
},
{
"path": "packages/artplayer-tool-iframe/dist/artplayer-tool-iframe.mjs",
"sha256": "ac4c5efa670c0429fc95bdab7902df60f5b5540f0dad9c7e65c2bd660796dc83"
},
{
"path": "packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.js",
"sha256": "ef72d277e817c2f7312c10815ee91a74dbb9334285476d14fae7f4449b3d2758"
},
{
"path": "packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.legacy.js",
"sha256": "f1d62e3cee7e060e7ecff4a21e7589e2befb2e405976efcdee73719857d0be03"
},
{
"path": "packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.mjs",
"sha256": "abdbc7c28bcccc1a8dee543f1bd784f850c147856fc148c3bf4b8ea863f3d6ea"
}
],
"candidate": [
{
"path": "packages/artplayer/dist/artplayer.js",
"sha256": "6dc8ca6d6eb34aa8a056cbc31328efd634530b3c5c60f0014cb7ce719fabc335"
},
{
"path": "packages/artplayer/dist/artplayer.legacy.js",
"sha256": "80f74a8a39df4baf64cc00c65d21cd57c011bfcdead0da38eea077fbc67c5c04"
},
{
"path": "packages/artplayer/dist/artplayer.mjs",
"sha256": "432e2b0ad92009a09743e020bf7c928c8b8f18a52496622b16259ec9ab20059f"
},
{
"path": "packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.js",
"sha256": "1429541242f49c4d558eda242a63b6e57a876c2dba06ce9f631d71d315028023"
},
{
"path": "packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.legacy.js",
"sha256": "4f64ac015e0850b71f51cf01d9fc6f238c95414730c71e91d1240c40e28499d8"
},
{
"path": "packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.mjs",
"sha256": "8ad7df8145b2514467cac0f733151619c36fa965e620df575c0779604372503f"
},
{
"path": "packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.js",
"sha256": "21ee5227f26cfff6e82ee6932f6eafc9741fa3be0ec2f5252a47b53ceea14599"
},
{
"path": "packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.legacy.js",
"sha256": "c3ed1a54e44a0202811d70378cb19edf87b1ebd010d05fe96ebe64d0b4301d5d"
},
{
"path": "packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.mjs",
"sha256": "ef126959819c4973da27dbb1e60ccccc54386c23f8c7750ce1ba4d38f306aaaa"
},
{
"path": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.js",
"sha256": "06b386f6a4744801f4153944ed6e2bd37822a85e177aa2a0d800c30002ef87bf"
},
{
"path": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.legacy.js",
"sha256": "639ae9bdede0e26c8af0251b9c1ed12e7fba9e0b6b79cb0556290741040f9697"
},
{
"path": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.mjs",
"sha256": "1b7bac676d2c4976046b08fd7c1ec25724ff24952f5d93fc546e219466d975d8"
},
{
"path": "packages/artplayer-plugin-audio-track/dist/artplayer-plugin-audio-track.js",
"sha256": "23b10856628c5568f1aa7ad88e926c7da8d96edfadf6df872777062a78a4b4be"
},
{
"path": "packages/artplayer-plugin-audio-track/dist/artplayer-plugin-audio-track.legacy.js",
"sha256": "41b94e84e3d3729b9373cb13de2f64d5ea30b4c36627f38583bf4c3c9bc2160c"
},
{
"path": "packages/artplayer-plugin-audio-track/dist/artplayer-plugin-audio-track.mjs",
"sha256": "9caa5a6e11a2e777760dd2835426842a87d04fb081ea2e7d48c7229035fd89a9"
},
{
"path": "packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.js",
"sha256": "430ac17e9025d3cc6bf37bd83316db5067bf438b30c2727b43c3e5fc4910e1d0"
},
{
"path": "packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.legacy.js",
"sha256": "efb30986ffc6a28859ca37126af82824601f25f0a42f7ea2c75a41f3da2c50ef"
},
{
"path": "packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs",
"sha256": "6080dd90bde239515646899bdd8e71d17de24119523e4883025fbf39b1a6a302"
},
{
"path": "packages/artplayer-plugin-chapter/dist/artplayer-plugin-chapter.js",
"sha256": "179c3ea90ddc85da27b46f000110e32d464dfa5c8c394e98316dc1b75d6f2a72"
},
{
"path": "packages/artplayer-plugin-chapter/dist/artplayer-plugin-chapter.legacy.js",
"sha256": "dade919ed044cbf46ac16e505f538f86a86edccf177da2e14d79d111559d4bef"
},
{
"path": "packages/artplayer-plugin-chapter/dist/artplayer-plugin-chapter.mjs",
"sha256": "754fbcfc60add74d6b6ad3bc8db253df3eb14ace13ac8a5439be0592bd5512d0"
},
{
"path": "packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.js",
"sha256": "80b12b3081086488c9d80f03979c336d4a6fdb44dca298e218dfeaaadb295d00"
},
{
"path": "packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.legacy.js",
"sha256": "b21a30852403914a3bf3defc89aff11f2cc23b044ed68ccf667164b477f04172"
},
{
"path": "packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.mjs",
"sha256": "68d81fcab469f74fe9d9740842d989e10144f7ff28c7ade91ed22adee90f44d7"
},
{
"path": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js",
"sha256": "133b7412e1a468b34f4f7ab3779958259f13899e037833029931f02b0833d088"
},
{
"path": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js",
"sha256": "9f0cb9969419a7d0c02d54790c423353b83f775acd95fbdbaeeb29a7fc42c816"
},
{
"path": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.mjs",
"sha256": "356406cda2e3e47376a87c939b76fb7c55f86d7c3829127da625b01241c67a2f"
},
{
"path": "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.js",
"sha256": "406a8c28207d386cffe1e61dc79afd8a6a9d5cbd1dc8e1661b4febbf48182f55"
},
{
"path": "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.legacy.js",
"sha256": "62d61dcedd3e90fa480589c2f1e3168c97635232c98ffe8c25c7918a2f3c66db"
},
{
"path": "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.mjs",
"sha256": "51936576383129170125b33d2983858e0841ea507368970f6d154bc9d78583b1"
},
{
"path": "packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.js",
"sha256": "a594009be5c17406844f817c5a79930c6ed60add4a5ef0578783f163a1bae012"
},
{
"path": "packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.legacy.js",
"sha256": "b00a837a99f7a7cccd415af335cbf0c68f1e4267fa7cfd8d91e945e3dc634151"
},
{
"path": "packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.mjs",
"sha256": "0c5a7e4d84e9641778e0a22ff9c78db9bd4ac2f08f51358519443046a528fb93"
},
{
"path": "packages/artplayer-plugin-document-pip/dist/artplayer-plugin-document-pip.js",
"sha256": "56440aad3ed06be55ec3a57d781f7de239e9aecf40a067e88f2cffcd01a86d6a"
},
{
"path": "packages/artplayer-plugin-document-pip/dist/artplayer-plugin-document-pip.legacy.js",
"sha256": "67dd14de3cc16a04ae6c1938dcba61b1355f4a15e5fa06f62a41b18d14d4d356"
},
{
"path": "packages/artplayer-plugin-document-pip/dist/artplayer-plugin-document-pip.mjs",
"sha256": "541b4ad6e1cbadd9156ee1f317559c97b9fddae37668ef32046f1fa47c392ec2"
},
{
"path": "packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.js",
"sha256": "8e163d4ab6734cbc45e7f539de7db644c7faf36d0318469a86d300bf6365cca6"
},
{
"path": "packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.legacy.js",
"sha256": "fd7be1c5bf8f994b61fc040ff96eb628fc442568d99f4b5fee4f611bac1d28c0"
},
{
"path": "packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.mjs",
"sha256": "64286498c1e90c3927236318383db193698a9dcbf39f91e93fc2b4ea2239e786"
},
{
"path": "packages/artplayer-plugin-jassub/dist/artplayer-plugin-jassub.js",
"sha256": "fb8cd8b7b6fb57a7866f929cfe20f7f582c86af96101f23c572dd79b4912dc9f"
},
{
"path": "packages/artplayer-plugin-jassub/dist/artplayer-plugin-jassub.legacy.js",
"sha256": "e19fea01316f78245e4508999bc441c483cfb47abfa6b5afbba529af3d05bfd8"
},
{
"path": "packages/artplayer-plugin-jassub/dist/artplayer-plugin-jassub.mjs",
"sha256": "d5f9df0621de4b5a9a5a8a232e290f99a1d8a2eda1bc0f8a34a2cb530c95653e"
},
{
"path": "packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.js",
"sha256": "14f4de91201f6b2cd55ea22a042d456c7149e4a38c22bc2ee0107f8a5e417446"
},
{
"path": "packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.legacy.js",
"sha256": "2059a43e4634b6c1a924a1cb451c2348356d6c20d26628b975b1d3545394d323"
},
{
"path": "packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.mjs",
"sha256": "07e4f628b4038d7c250f6242c410ddb188148b4ed58bb592975532666412e3e2"
},
{
"path": "packages/artplayer-plugin-vast/dist/artplayer-plugin-vast.js",
"sha256": "874803ec726f04649bb636c1376de9e038754310ab0016a22da44f189b7a5ce0"
},
{
"path": "packages/artplayer-plugin-vast/dist/artplayer-plugin-vast.legacy.js",
"sha256": "8a4166838aae5d150dc1a18cc594fb038df425df42eff1944e0d095df060dbb9"
},
{
"path": "packages/artplayer-plugin-vast/dist/artplayer-plugin-vast.mjs",
"sha256": "ffbab3a7cd6c1852c42a726acd0e0a5dbbb636dc9e8c2e8a084df962ff6ef860"
},
{
"path": "packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.js",
"sha256": "df70ccb5261fd988ba8c2c2d689ca58653bb5426242ffd1076569a1d225997a7"
},
{
"path": "packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.legacy.js",
"sha256": "732ade2c647281cf26446d6c7433e916f8189e2fdf2687127a56556c415544b1"
},
{
"path": "packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.mjs",
"sha256": "d35e28015df65e27da0fc01d06db07bad58e08d498b2d11fedfea35e6c4339c1"
},
{
"path": "packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.js",
"sha256": "a901e5c5fc00a64a6c2ae9d4b2b846950412710ec9672d30640d40987d4f9e73"
},
{
"path": "packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.legacy.js",
"sha256": "052263fdd7ab657a0183d874a706c2cf82527233c5e60b58158caa0677718b97"
},
{
"path": "packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.mjs",
"sha256": "66ca7271f52545a8796f563f2d5ca04581bed8d60d218463923325acf5b73ccf"
},
{
"path": "packages/artplayer-proxy-mediabunny/dist/artplayer-proxy-mediabunny.js",
"sha256": "23ce59f86e7159eedd4c371219c5ed58db83341e1f3fa3c67dcb8f1259e243cb"
},
{
"path": "packages/artplayer-proxy-mediabunny/dist/artplayer-proxy-mediabunny.legacy.js",
"sha256": "2c523a4535ae71213d279b54abf3b1d164c81e3d8abe3d9c9906e9a9ac840c26"
},
{
"path": "packages/artplayer-proxy-mediabunny/dist/artplayer-proxy-mediabunny.mjs",
"sha256": "3126446aa6461b78e56855ac851a2abf5a12359c7354e5564af6efaa0903f3fa"
},
{
"path": "packages/artplayer-tool-iframe/dist/artplayer-tool-iframe.js",
"sha256": "7408fceccb000e1b304abe30ac9bc364da7860c3896ad5aaa6af427804c237e6"
},
{
"path": "packages/artplayer-tool-iframe/dist/artplayer-tool-iframe.legacy.js",
"sha256": "4e8bd39cdc37e98bff9cd19b5e0c9453ebe5231264242fc1c573aa96edb5ea8a"
},
{
"path": "packages/artplayer-tool-iframe/dist/artplayer-tool-iframe.mjs",
"sha256": "ac4c5efa670c0429fc95bdab7902df60f5b5540f0dad9c7e65c2bd660796dc83"
},
{
"path": "packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.js",
"sha256": "ef72d277e817c2f7312c10815ee91a74dbb9334285476d14fae7f4449b3d2758"
},
{
"path": "packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.legacy.js",
"sha256": "f1d62e3cee7e060e7ecff4a21e7589e2befb2e405976efcdee73719857d0be03"
},
{
"path": "packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.mjs",
"sha256": "abdbc7c28bcccc1a8dee543f1bd784f850c147856fc148c3bf4b8ea863f3d6ea"
}
],
"differences": []
},
"servorPortDefect": {
"task": "MOD-DEV-01",
"node": "v24.21.0",
"servor": "4.0.2",
"occupiedPort": 12671,
"code": 0,
"signal": null,
"output": "[ERR] The port you have specified is already in use!\n",
"interpretation": "Historical upstream defect reproduction, not candidate success: an occupied requested port exits the entire process with status zero."
},
"sources": [
{
"path": "scripts/build.js",
"sha256": "ec716c545d6bd0a06fdf7b616a487e0cc7605212de5c413bc247efa6737d5870"
},
{
"path": "scripts/dev.js",
"sha256": "86b13818172c2ab9ed6792f32e14af1b194783292264ff4476bcbe0f54697176"
},
{
"path": "scripts/utils.js",
"sha256": "b42450026f5e8a621b59cd84f91d5c1d6915f4ec026ad2ee0980b06f16f6ae79"
},
{
"path": "scripts/projects.js",
"sha256": "3956faff0032ab5b91daa45f390a664ca246559621f95fd2f53cef8bb8b58bdf"
},
{
"path": "scripts/rebuild.js",
"sha256": "de8d73c482ac22c7c197910a755eb38cf9f3e4719c2daa9d994dd9122668fddf"
},
{
"path": "scripts/build-analysis.mjs",
"sha256": "b5e62d1fcd9fd9216f8849a15b5fc70bf0280d978b9992c0ff5302927884c15f"
},
{
"path": "scripts/tsconfig.library.json",
"sha256": "3f47bba61969a74be0285c67d19293ffb736ac9831a9ff78abac4789dcaf5e50"
},
{
"path": "scripts/performance-artifacts.mjs",
"sha256": "5749bc5d75d9d12fe73c281cfa12741cec29e3fd7032324eaa63c6670774e9c4"
},
{
"path": "test/performance-report.test.js",
"sha256": "3092854bea819b4436e00a9cc26615119ccaffef5f745d6c1459e1ed9a33422d"
},
{
"path": "test/browser/library-development.spec.js",
"sha256": "d2f606432618319815d588edda2bf01513db4154b5ddef0f4813ed5cdd80ff27"
},
{
"path": "refactor/scripts/build.test.mjs",
"sha256": "41942c147d63880169c9c7b032b395b511ca0573dc4ac09ba54e01338013b521"
},
{
"path": "refactor/scripts/library-build-comparison.mjs",
"sha256": "956e697d4d67a0417c71398f345d44144f8c3a9997f262c2b014c7a3ca5c0725"
},
{
"path": "refactor/scripts/servor-port-probe.mjs",
"sha256": "c3ae47466a575755b63c74fafd25a8fd8df160b36c2fe119cb7f5dfadfd26e2b"
},
{
"path": "scripts/library/analysis.ts",
"sha256": "68ce6b33d0257ccd34a2abc5a75a7a6e06420cd93eaa00501cc04e38d48ad78b"
},
{
"path": "scripts/library/banner.ts",
"sha256": "c07b9b033cf1e3ad68e9ce80c0f9e7955d24abe8ad291e01ad22b5303c9f9ec5"
},
{
"path": "scripts/library/config.ts",
"sha256": "57ce344e05e5373a9c243a37a223e2403c67b25541b4c77f5289a83a008b6a23"
},
{
"path": "scripts/library/development.ts",
"sha256": "7b6af5ef9bc985def5d604ae0dbc75faa57be907edfb8a06a5420f18462ba5b4"
},
{
"path": "scripts/library/names.ts",
"sha256": "3749777e71f2f471e0edb4a1104d2c650d09643f57149a42f1c135dc79b7b1b5"
},
{
"path": "scripts/library/production.ts",
"sha256": "f309ec71319d7d99f703bba3d11f3db53c1c240d6af9d0d5e6a5e36eb3b1bfeb"
},
{
"path": "scripts/library/projects.ts",
"sha256": "fc658271482a4f293c21d606ac87b2668dbc88e3028b345a622587ebc0d151ca"
},
{
"path": "scripts/library/rebuild.ts",
"sha256": "01cfc0f5f58263baa891e7c783349f1ff212a423a03a6805aecea568a482aec0"
},
{
"path": "scripts/library/vendor.d.ts",
"sha256": "c64b7a80155c6ffc95683a208de01ba5d8d4c7b8e673b2dc5a51479a467e9f6a"
}
],
"logs": [
{
"path": "refactor/.cache/library-types.log",
"sha256": "258b10b22b662a74ceb797d7522c1fada9d3eb274e0b5d399297124213a6bb60"
},
{
"path": "refactor/.cache/library-docs-types.log",
"sha256": "a81eda6ec03482b64e1f2e445981950400f44c198f4a2807b4d8d0ce8a40e0c5"
},
{
"path": "refactor/.cache/library-final.log",
"sha256": "57ea9a6ad9a9dbe51021ed4e931a01eda245bf6075d2f96aa51361c7959d706f"
},
{
"path": "refactor/.cache/library-lint.log",
"sha256": "582d5ca5dd5743cc6c4ee0d79209e83ae092192fc08a4459108c9378da5977ca"
},
{
"path": "refactor/.cache/library-ci.log",
"sha256": "4f3da506e18e73d591b3639a45d58ff8d6d2dd85b10cdd6b9e6ea83e264e450d"
},
{
"path": "refactor/.cache/library-toolchain.log",
"sha256": "26a73433d38e21611f5fac5a5ee69f4fccbe6504ff8911e14804263ff971bcb2"
},
{
"path": "refactor/.cache/library-browser-initial.log",
"sha256": "5d89458936d534add82759a5d5c7c619573ca4926808b5ca9daf2862d4bfee16"
},
{
"path": "refactor/.cache/library-browser-reload-initial.log",
"sha256": "327d7313fff811981422a6b1006ec059571d3e2aabea1396076070657a4bd922"
},
{
"path": "refactor/.cache/library-browser-final.log",
"sha256": "f1ca8c33db2481eacfb8769dc2fbc304979b09d502042820aef6240752d4c795"
},
{
"path": "refactor/.cache/library-comparison.log",
"sha256": "3e3b74b295a44218b6266e0593af346a3686e3108db4b44822a25658faf44135"
},
{
"path": "refactor/.cache/servor-port-probe.log",
"sha256": "69d774eb3a5ce6a0b17e398d4fecd228054f2a42465703e9ddc7c1aefe261682"
},
{
"path": "refactor/.cache/library-baseline.log",
"sha256": "0379a619c3b1323b32ae74a95da757318a6dd9538fef586de987fb5d94644d78"
}
],
"limitations": [
"Windows browser evidence; remote OS matrices remain pending",
"No library media playback or npm publication claims",
"Servor occupied-port exit0 remains open in MOD-DEV-01",
"No new dependencies, Lerna changes or real dist/docs rebuild"
]
}
+10 -7
View File
@@ -4,8 +4,10 @@ MOD-PLUGIN-01 将 `yarn create:plugin` 的实现拆为严格 TS 渲染/文件写
保留旧 JS 命令入口。生成 TS 工厂、CJS/ESM 声明、分发消费测试与维护文档;
拒绝覆盖既有包/示例,失败时回退自己的写入并保留外部修改。
运行 `yarn typecheck:scaffold` 与 `yarn test:scaffold`,具体限制和模块地图见
[生成器维护指南](../scripts/plugin/README.md)。MOD-02 继续负责 dev/build/utils,
本子任务不代表全部工程脚本已经迁移。
[生成器维护指南](../scripts/plugin/README.md)。MOD-02 将剩余库构建/开发实现移到
scripts/library 的严格 TS 模块,保留旧命令/模块入口;其模块地图与生命周期限制见
[库工具维护指南](../scripts/library/README.md)。Servor 端口失败状态和关闭责任由
MOD-DEV-01 继续处理,不因 TS 迁移而豁免。
ENG-06 保留原来的 Vite 7.3.6、Terser、三种库产物及路径,增加选包参数和 TS 入口;未切换 bundler 或运行时依赖。固定工具链仍为 Node 24.21.0 / Yarn 1.22.22。
@@ -32,11 +34,12 @@ JS 包使用 src/index.js,TS 包使用 src/index.ts;必须恰有一个入口
| 文件 | 职责与维护点 |
| --- | --- |
| scripts/projects.js | 库包清单、CLI 选包、旧 prompts 入口、JS/TS 入口唯一性;utils.js 继续转出 getProjects,保留原脚本调用 |
| scripts/build.js | 三格式顺序构建、受限 dist 清理、banner、复制 docs/compiled;main=.js/es2020、legacy=.legacy.js/es2015、ESM=.mjs/es2020 |
| scripts/dev.js | 8082 服务、docs/uncompiled/包名/index.js、首次成功打开浏览器、源目录监听及错误后的继续开发 |
| scripts/rebuild.js | 合并构建期间的通知并串行重建,避免多个构建同时清空同一输出目录;失败后下一次请求仍可运行 |
| scripts/utils.js | 共享 Vite 资源/worker/output 配置与历史全局名称;AMD 补丁依据实际 UMD 全局参数,不假定压缩变量名 |
| scripts/projects.js | 兼容导出 library/projects.ts:库清单、CLI 选择和 JS/TS 唯一入口;保留原 prompts 交互 |
| scripts/build.js | 命令门面调用 library/production.ts;顺序构建、dist 清理、docs/compiled 复制和分析;banner 独立为 library/banner.ts |
| scripts/dev.js | 命令门面调用 library/development.ts;默认 8082、docs/uncompiled/包名/index.js、首次成功打开浏览器、源码监听和失败恢复 |
| scripts/rebuild.js | 兼容导出 library/rebuild.ts;合并通知、串行构建、失败后的下一次请求 |
| scripts/utils.js | 兼容导出 library/config.ts、names.ts 和 projects.ts;Vite/worker 配置与命名分离,AMD 补丁由 banner.ts 维护 |
| scripts/build-analysis.mjs | 兼容导出 library/analysis.ts;分析文件位于 cache,不进入 dist |
| types/assets.d.ts | Less inline、SVG URL 与 SVG raw 的字符串类型;特定 worker 类型边界随拥有该 worker 的包配置 |
| refactor/fixtures/build/ | 同时使用 TS、相邻 JS、Less、SVG raw、TS inline worker 的独立构建夹具;不属于发布包 |
| refactor/scripts/build.test.mjs | 真实 CLI 产物、错误不误清理、CJS/global/AMD/ESM、队列错误恢复;浏览器 worker/全库 AMD 使用 browser.html |
@@ -0,0 +1,73 @@
# MOD-02: Typed library build and development tooling
## Scope, structure and compatibility
Preserve the historical build/dev commands, selectors, global names, resource
handling, banner/notices, distribution filenames and analysis output. JS/MJS
entrypoints remain as checked shims. The implementation moves to strict TS modules
for production/development, config, banner handling, names, project selection,
rebuild queue and analysis; responsibilities and dependencies are documented in
[scripts/library/README.md](../../scripts/library/README.md). The scaffold part was
already completed independently as MOD-PLUGIN-01.
Installed Vite declarations constrain plugin hooks and config mutation. Narrow
ambient declarations cover only the consumed prompts2.4.2/Servor4.0.2 APIs, inspected
in their installed source. No dependencies, lockfile, Lerna or installation hooks
changed. Tool execution uses canonical Node24.21.0; built package consumption retains
its separate Node/browser compatibility requirements.
Manifest parsing now starts from unknown and rejects a missing/non-string version
before cleaning that package's dist. Performance evidence fingerprints every TS/JS
and JSON module under scripts/library as well as the previous shims/lock; changes
and additions/removals must invalidate old installed-artifact measurements. Markdown
is not a build input. The tests cover the previously unnoticed migration hazard
where unchanged shims could hide a changed TS config.
Development keeps port8082 and --no-open. Tests use the explicit internal run function
with a fixture port, leaving the existing8082 process untouched. Output directories
are created before Servor enumerates Linux watch directories; actual Linux execution
remains part of remote CI, not inferred from the Windows run. Build errors keep the
source watcher alive; queued changes remain serial.
## Validation
- `library-build-comparison.mjs`: 21 libraries / 63 artifacts built from the same
current sources and fixture paths with frozen scripts at
`28c7abb2bb8dfb7c84acfc7c5ec05c5882623069`, then current scripts. Every SHA-256
is identical. Full report and logs retained in .cache/library-comparison-gjDNPX.
Existing real dist/docs artifacts were not rebuilt or manually edited.
- `yarn test:library`: 18 passed, including CLI invalid inputs, malformed version
protection, TS/JS/Less/SVG/inline-worker builds, CJS/global/AMD/ESM, analysis output
identity, private-public exclusion, performance evidence and generated plugins.
- `yarn typecheck:library` and `yarn typecheck:docs-tools`: strict checks passed;
the old JS/MJS shims are checked too.
- `library-development.spec.js`: final 3 passed on Windows Chromium, Firefox and
WebKit. Real server, worker postMessage result, TS edits, automatic page reload
and syntax-error recovery were checked. No mocked dev server or playback claim.
- First browser run failed because --input-type=module leaked into Vite's Less
worker and broke CommonJS require; using a normal .mjs fixture launcher fixes the
test environment. A second run had Chromium navigation cancellation when manual
reload raced the real automatic reload; tests now assert the automatic update
itself. Those reports are retained separately; neither is called a production fix.
- `yarn lint`: passed with one existing docs/assets/ts/artplayer.d.ts warning.
`yarn test:ci`: 50 passed. Remote workflow runs are not claimed.
- `yarn test:baseline`: 522 passed, zero skipped. Strict toolchain and final
plan/risk checks also pass; dependencies and the root lock remain unchanged.
Source/log hashes, artifact comparison, browser results and final gate results are
recorded in [library-tooling-validation.json](../baselines/library-tooling-validation.json).
## Remaining server defect and next step
`servor-port-probe.mjs` binds its own random port, calls installed Servor4.0.2 in an
isolated child, and reproduces its error message followed by exit status0. This is
old upstream behavior, not a successful candidate check. DEV-SERVOR-PORT-01 remains
open; MOD-DEV-01 is added before the final tooling decision to fix failure status
and define HTTP/watch/reload/timeout shutdown ownership. Servor exposes no close
handle and currently owns these resources for process lifetime. No unrelated server
was terminated and no vendor patch or dependency replacement was hidden in this task.
MOD-02 completes the TS/structure and matching-output contract, not every dev-server
lifecycle issue, remote OS check or the entire refactor. Revert this dedicated task
commit to restore its old scripts and validation mapping together; do not revert
only the shims without their TS targets. Local commit only, no push or publication.
+1
View File
@@ -12,6 +12,7 @@
| `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 typecheck:library` / `yarn test:library` | 严格检查库构建/开发 TS 与旧门面;复用真实资源构建、分发、性能证据和生成器回归;类型检查接入 ci:check |
| `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 |
+6 -4
View File
@@ -2,9 +2,9 @@
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 244 项,范围 22 个包及工作区/示例。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 245 项,范围 22 个包及工作区/示例。
状态:todo 57 / doing 16 / blocked 0 / done 171 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 57 / doing 16 / blocked 0 / done 172 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -401,10 +401,11 @@
| --- | --- | --- | --- | --- | --- | --- |
| MOD-01 | workspace<br>Bun 固定版本干净安装试点 | ENG-09 | 独立目录的 Bun 安装与 Yarn 冻结基线对比,默认仍为用户选定的 Yarn | Node 测试仍通过;安装与资源一致才决定采用;不改 bundler;不自动替换默认 packageManager 或维护锁文件 | H | 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-02 | workspace<br>整理剩余开发/构建脚本与插件模板 | ENG-06, SITE-03, MOD-PLUGIN-01 | dev/build/utils/create-plugin 的 TS 与可测 CLI,模板同时提供旧 API | 旧脚本入口保留、新插件类型/测试/示例齐全,Lerna 改动单独取证 | M | done |
| MOD-DEV-01 | workspace<br>修复开发服务器失败状态并明确资源生命周期 | MOD-02 | 修复 Servor 占用端口却退出 0 的已复现问题;明确 HTTP/监听/热刷新连接与定时器的启动、失败和关闭责任 | 保持默认 8082、docs 页面、自动刷新和旧 dev 命令;端口冲突退出非零且不终止其他服务,连续启动/停止及编译错误恢复有实际验证;跨平台监听差异单独留证 | 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 |
| MOD-05 | workspace<br>完成工具链与性能采用决策 | MOD-01, MOD-02, MOD-03, MOD-04, MOD-DEV-01 | 最终 runtime/packageManager/构建配置及性能台账 | 干净安装和全包检查通过;Bun 未采用有理由,不为状态强行切换 | M | todo |
## 8 发布验收
@@ -622,6 +623,7 @@
- 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)
- MOD-02: [记录](changes/2026-09-14-MOD-02-library-tooling.md) [记录](baselines/library-tooling-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)
+18
View File
@@ -1,5 +1,23 @@
# 进度与证据
## MOD-02 库构建与开发脚本 TS 迁移完成
旧 build/dev/utils/projects/rebuild/build-analysis 入口保留,实现拆为严格 TS
模块,Vite 配置、banner/AMD、命名、包选择、队列和分析责任分离。63 个产物
在同一来源/路径下用旧脚本与新脚本构建,SHA-256 全部相同;真实 dist/docs 未改。
性能证据校验同步覆盖新的 TS 构建输入。默认8082保留,测试使用自有端口,未
终止原有服务。输出目录提前创建,避免漏入 Servor 初始 Linux 目录枚举;远端
Linux/macOS 实测仍待 CI。Windows 三浏览器验证真实 Worker、自动刷新、修改和
编译失败恢复最终3项通过,测试启动参数/手动刷新竞态的失败报告另存。
18项专项、522项baseline、50项CI回归通过;严格库/文档工具类型和工具链检查
通过,lint 保留1条既有warning。详情见[记录](changes/2026-09-14-MOD-02-library-tooling.md)
和[证据](baselines/library-tooling-validation.json)。没有新增依赖、Lerna变更或发布。
另用隔离子进程复现 Servor4.0.2 端口占用却退出0,新增开放风险及 MOD-DEV-01
作为下一任务处理启动失败和资源关闭责任,已接入最终工具链任务依赖。
245项:172 done、16 doing、57 todo。MOD-02 完成不代表该服务器缺陷或完整
重构完成。本地独立提交;VAST 默认行为选择仍待用户答复。
## MOD-PLUGIN-01 插件生成器与 TS 模板完成
生成器拆为严格 TS 的模板渲染、文件写入和 CLI,保留 create.js 旧命令入口及
+1
View File
@@ -262,3 +262,4 @@
| 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 |
| DEV-SERVOR-PORT-01 | open / 已复现 | Servor exits the entire dev process with status zero when the requested port is occupied | MOD-DEV-01 |
+17
View File
@@ -5914,6 +5914,23 @@
"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."
},
{
"id": "DEV-SERVOR-PORT-01",
"title": "Servor exits the entire dev process with status zero when the requested port is occupied",
"confirmation": "reproduced",
"status": "open",
"owners": [
"MOD-DEV-01"
],
"evidence": [
"refactor/changes/2026-09-14-MOD-02-library-tooling.md",
"refactor/scripts/servor-port-probe.mjs",
"refactor/baselines/library-tooling-validation.json"
],
"compatibleResolution": "Preserve the default dev URL and CLI while reporting startup failures with nonzero status, without stopping another service. Define ownership of HTTP, watch and reload resources.",
"closureCriteria": "Real occupied-port and repeated startup/shutdown checks pass for the candidate; build, reload and error recovery remain compatible. Separate source-observed resource ownership limitations from reproduced leaks.",
"workspaceState": "Pinned Servor4.0.2 probe prints the occupied-port error and exits0. TS migration retains that upstream behavior; MOD-DEV-01 must resolve it before the final tooling decision. Servor has no exposed close handle; this is a source observation, not a measured leak."
}
]
}
+4
View File
@@ -75,6 +75,10 @@ test('Normal build CLI compiles TypeScript, JS, Less, SVG and inline worker into
assert.equal(run([name]).status, 1)
assert(fs.existsSync(path.join(dist, 'preserve.txt')))
fs.unlinkSync(path.join(project, 'src/index.js'))
fs.writeFileSync(path.join(project, 'package.json'), JSON.stringify({ name, version: 1 }))
assert.equal(run([name]).status, 1, 'Malformed version must fail before dist is cleaned')
assert(fs.existsSync(path.join(dist, 'preserve.txt')))
fs.writeFileSync(path.join(project, 'package.json'), JSON.stringify({ name, version: '1.0.0' }))
execFileSync(process.execPath, [path.join(root, 'scripts/build.js'), name], { cwd: directory, env: { ...process.env, NODE_ENV: 'production' }, encoding: 'utf8', timeout: 60000 })
assert.deepEqual(fs.readdirSync(dist).sort(), [`${name}.js`, `${name}.legacy.js`, `${name}.mjs`])
function check(factory) {
@@ -0,0 +1,50 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
const root = fileURLToPath(new URL('../../', import.meta.url))
const baseline = '28c7abb2bb8dfb7c84acfc7c5ec05c5882623069'
const directory = fs.mkdtempSync(path.join(root, 'refactor/.cache/library-comparison-'))
const names = fs.readdirSync(path.join(root, 'packages')).filter(name => name !== 'artplayer-vitepress' && fs.existsSync(path.join(root, 'packages', name, 'package.json'))).sort()
const sha = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex')
fs.mkdirSync(path.join(directory, 'scripts'))
fs.writeFileSync(path.join(directory, 'package.json'), '{"type":"module"}\n')
for (const file of ['build.js', 'utils.js', 'projects.js', 'build-analysis.mjs'])
fs.writeFileSync(path.join(directory, 'scripts', file), execFileSync('git', ['show', `${baseline}:scripts/${file}`], { cwd: root }))
for (const name of names) {
const target = path.join(directory, 'packages', name)
fs.mkdirSync(target, { recursive: true })
for (const file of ['src', 'public', 'package.json', 'THIRD_PARTY_NOTICES']) {
const source = path.join(root, 'packages', name, file)
if (fs.existsSync(source))
fs.cpSync(source, path.join(target, file), { recursive: true })
}
}
const artifacts = () => names.flatMap(name => ['.js', '.legacy.js', '.mjs'].map(extension => {
const relative = `packages/${name}/dist/${name}${extension}`
return { path: relative, sha256: sha(path.join(directory, relative)) }
}))
function build(script, label) {
const log = fs.openSync(path.join(directory, `${label}.log`), 'w')
try {
execFileSync(process.execPath, [script, 'all'], { cwd: directory, stdio: ['ignore', log, log], timeout: 300000 })
}
finally {
fs.closeSync(log)
}
}
console.log(`Library comparison fixture: ${directory}`)
build(path.join(directory, 'scripts/build.js'), 'before')
const before = artifacts()
fs.writeFileSync(path.join(directory, 'before.json'), `${JSON.stringify(before, null, 2)}\n`)
build(path.join(root, 'scripts/build.js'), 'candidate')
const candidate = artifacts()
const differences = candidate.filter((item, index) => item.sha256 !== before[index].sha256)
const report = { task: 'MOD-02', baseline, node: process.version, names, before, candidate, differences }
fs.writeFileSync(path.join(directory, 'report.json'), `${JSON.stringify(report, null, 2)}\n`)
assert.deepEqual(differences, [], 'Production bytes changed across the tooling migration')
console.log(`Compared ${names.length} libraries / ${candidate.length} artifacts: identical SHA-256. Report: ${directory}/report.json`)
+35
View File
@@ -0,0 +1,35 @@
import assert from 'node:assert/strict'
import { spawn } from 'node:child_process'
import { once } from 'node:events'
import fs from 'node:fs'
import net from 'node:net'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
const root = fileURLToPath(new URL('../../', import.meta.url))
const server = net.createServer()
server.listen(0)
await once(server, 'listening')
const { port } = server.address()
try {
const child = spawn(process.execPath, ['--input-type=module', '--eval', "import servor from 'servor'; await servor({port:Number(process.env.ARTPLAYER_PROBE_PORT),reload:false}); process.exit(7)"], {
cwd: root,
env: { ...process.env, ARTPLAYER_PROBE_PORT: String(port) },
windowsHide: true,
timeout: 10000,
})
let output = ''
child.stdout.on('data', (data) => { output += data })
child.stderr.on('data', (data) => { output += data })
const [code, signal] = await once(child, 'exit')
const version = JSON.parse(fs.readFileSync(path.join(root, 'node_modules/servor/package.json'), 'utf8')).version
const result = { task: 'MOD-DEV-01', node: process.version, servor: version, occupiedPort: port, code, signal, output, interpretation: 'Historical upstream defect reproduction, not candidate success: an occupied requested port exits the entire process with status zero.' }
fs.writeFileSync(path.join(root, 'refactor/.cache/servor-port-probe.json'), `${JSON.stringify(result, null, 2)}\n`)
assert.match(output, /port.*already in use/)
assert.equal(code, 0, 'Pinned Servor no longer reproduces its historical successful failure exit')
console.log(JSON.stringify(result, null, 2))
}
finally {
await new Promise(resolve => server.close(resolve))
}
+22 -2
View File
@@ -4459,10 +4459,29 @@
"SITE-03",
"MOD-PLUGIN-01"
],
"status": "todo",
"status": "done",
"risk": "M",
"deliverable": "dev/build/utils/create-plugin 的 TS 与可测 CLI,模板同时提供旧 API",
"acceptance": "旧脚本入口保留、新插件类型/测试/示例齐全,Lerna 改动单独取证",
"evidence": [
"changes/2026-09-14-MOD-02-library-tooling.md",
"baselines/library-tooling-validation.json"
]
},
{
"id": "MOD-DEV-01",
"phase": "7 工具链与性能",
"title": "修复开发服务器失败状态并明确资源生命周期",
"scope": [
"workspace"
],
"dependsOn": [
"MOD-02"
],
"status": "todo",
"risk": "M",
"deliverable": "修复 Servor 占用端口却退出 0 的已复现问题;明确 HTTP/监听/热刷新连接与定时器的启动、失败和关闭责任",
"acceptance": "保持默认 8082、docs 页面、自动刷新和旧 dev 命令;端口冲突退出非零且不终止其他服务,连续启动/停止及编译错误恢复有实际验证;跨平台监听差异单独留证",
"evidence": []
},
{
@@ -4512,7 +4531,8 @@
"MOD-01",
"MOD-02",
"MOD-03",
"MOD-04"
"MOD-04",
"MOD-DEV-01"
],
"status": "todo",
"risk": "M",
+4
View File
@@ -1,5 +1,9 @@
# 已实现的类型检查与迁移入口
`yarn typecheck:library` 检查 scripts/library 的生产构建、开发、配置、banner、
分析、包选择和重建队列,同时检查旧 JS/MJS 门面。Vite 配置使用已安装的官方
类型;vendor.d.ts 只描述实际消费的 prompts2.4.2/Servor4.0.2 调用,不声明全 SDK。
`yarn typecheck:scaffold` 检查 scripts/plugin 的严格 TS 模块及旧 JS 命令入口。
模板本身保留占位符,`yarn test:scaffold` 对实际渲染包执行源码检查和
TS 5.9.3 NodeNext / TS 4.3.5 classic 消费者正反例;生成声明地图见
+1 -44
View File
@@ -1,44 +1 @@
import { Buffer } from 'node:buffer'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { gzipSync } from 'node:zlib'
export function createBuildAnalysis() {
const root = process.cwd()
const parent = path.join(root, 'refactor/.cache/build-analysis')
fs.mkdirSync(parent, { recursive: true })
const directory = fs.mkdtempSync(path.join(parent, 'run-'))
fs.writeFileSync(path.join(parent, 'latest.json'), `${JSON.stringify({ output: path.relative(root, directory).replaceAll('\\', '/') })}\n`)
return { root, directory }
}
export function buildAnalysisPlugin({ root, directory }, name, format) {
return {
name: 'artplayer-build-analysis',
generateBundle(_options, bundle) {
for (const chunk of Object.values(bundle).filter(item => item.type === 'chunk')) {
const modules = Object.entries(chunk.modules).map(([id, info]) => {
const normalized = id.replaceAll('\\', '/')
const dependency = normalized.lastIndexOf('/node_modules/')
const file = id.startsWith('\0')
? `virtual:${id.slice(1)}`
: dependency === -1 ? path.relative(root, id).replaceAll('\\', '/') : normalized.slice(dependency + 1)
return { file, renderedBytes: Buffer.byteLength(info.code || ''), renderedCodeUnits: info.renderedLength, originalCodeUnits: info.originalLength, renderedExports: info.renderedExports, removedExports: info.removedExports }
}).sort((a, b) => b.renderedBytes - a.renderedBytes || a.file.localeCompare(b.file))
const report = {
schemaVersion: 1,
name,
format,
node: process.version,
file: chunk.fileName,
artifact: { bytes: Buffer.byteLength(chunk.code), gzip9: gzipSync(chunk.code, { level: 9 }).length, sha256: createHash('sha256').update(chunk.code).digest('hex') },
note: 'Rendered UTF-8 bytes precede final chunk minification; they are not additive compressed sizes or download measurements. Rollup lengths are separately recorded as UTF-16 code units. Original code units describe Rollup input, not raw TypeScript.',
modules,
}
fs.writeFileSync(path.join(directory, `${name}-${format}.json`), `${JSON.stringify(report, null, 2)}\n`)
}
},
}
}
export { buildAnalysisPlugin, createBuildAnalysis } from './library/analysis.ts'
+1 -97
View File
@@ -1,101 +1,5 @@
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import cpy from 'cpy'
import { build as viteBuild } from 'vite'
import { buildAnalysisPlugin, createBuildAnalysis } from './build-analysis.mjs'
import { getEntryFile, selectProjects } from './projects.js'
import { getGlobalName, getProjects, getViteBuildConfig } from './utils.js'
const projects = getProjects()
const compiledPath = path.resolve('docs/compiled')
function getBanner(name, version) {
const noticeFile = path.join(projects[name], 'THIRD_PARTY_NOTICES')
const notices = fs.existsSync(noticeFile) ? fs.readFileSync(noticeFile, 'utf8').trim() : ''
const noticeBlock = notices ? `\n *\n${notices.split(/\r?\n/).map(line => line.trimEnd() ? ` * ${line.trimEnd()}` : ' *').join('\n')}` : ''
return `/*!
* ${name}.js v${version}
* Github: https://github.com/zhw2590582/ArtPlayer
* (c) 2017-${new Date().getFullYear()} Harvey Zhao
* Released under the MIT License.${noticeBlock}
*/`
}
const BUILD_FORMATS = {
main: { format: 'umd', ext: '.js', target: 'es2020', minify: 'terser' },
legacy: { format: 'umd', ext: '.legacy.js', target: 'es2015', minify: 'terser' },
esm: { format: 'es', ext: '.mjs', target: 'es2020', minify: false },
}
async function build(name, targetName, clean = false, analysis) {
const projectDir = projects[name]
const { version } = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf-8'))
const distDir = path.join(projectDir, 'dist')
const entryFile = getEntryFile(projectDir)
if (clean && fs.existsSync(distDir)) {
if (fs.realpathSync(distDir) !== path.join(fs.realpathSync(projectDir), 'dist'))
throw new Error(`Refusing to clean redirected dist directory: ${distDir}`)
fs.rmSync(distDir, { recursive: true, force: true })
}
const startTime = Date.now()
const { format, ext, target, minify } = BUILD_FORMATS[targetName]
const fileName = `${name}${ext}`
const config = getViteBuildConfig({
entry: entryFile,
outDir: distDir,
name: getGlobalName(name),
format,
fileName,
minify,
target,
banner: getBanner(name, version),
})
// Terser options for all builds (preserve banner comments starting with /*!)
const isLegacy = targetName === 'legacy'
config.build.terserOptions = {
ecma: isLegacy ? 5 : 2020,
compress: { ecma: isLegacy ? 5 : 2020, comparisons: false, inline: 2 },
mangle: { safari10: true },
format: {
ecma: isLegacy ? 5 : 2020,
comments: /^!/,
ascii_only: true,
max_line_len: false, // No line length limit
},
}
if (analysis)
config.build.rollupOptions.plugins.push(buildAnalysisPlugin(analysis, name, targetName))
await viteBuild({ root: projectDir, ...config })
const outFile = path.join(distDir, fileName)
const size = (fs.statSync(outFile).size / 1024).toFixed(2)
await cpy(outFile, compiledPath, { flat: true })
console.log(`✨ Built@${targetName} ${name}@${version} Time@${Date.now() - startTime}ms Size@${size}kb`)
}
async function buildProject(name, analysis) {
await build(name, 'main', true, analysis)
await build(name, 'legacy', false, analysis)
await build(name, 'esm', false, analysis)
}
async function runBuild() {
const { names, analyze } = await selectProjects(projects, 'build')
const analysis = analyze ? createBuildAnalysis() : undefined
for (const name of names)
await buildProject(name, analysis)
if (analysis)
console.log(`Build analysis: ${analysis.directory}`)
if (names.length)
console.log(`✅ Finished building ${names.length} package(s)!`)
}
import { runBuild } from './library/production.ts'
runBuild().catch((error) => {
console.error('❌ Build failed:', error)
+2 -71
View File
@@ -1,76 +1,7 @@
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import servor from 'servor'
import openBrowser from 'servor/utils/openBrowser.js'
import { build as viteBuild } from 'vite'
import { getEntryFile, selectProjects } from './projects.js'
import { createRebuildQueue } from './rebuild.js'
import { getGlobalName, getProjects, getViteBuildConfig } from './utils.js'
import { runDevelop } from './library/development.ts'
const projects = getProjects()
async function develop(name, open) {
const projectPath = projects[name]
const uncompiledPath = path.resolve(`docs/uncompiled/${name}`)
let browserOpened = false
const { url } = await servor({
root: 'docs',
fallback: 'index.html',
reload: true,
port: 8082,
})
fs.mkdirSync(uncompiledPath, { recursive: true })
async function buildBundle() {
const startTime = Date.now()
try {
const config = getViteBuildConfig({
entry: getEntryFile(projectPath),
outDir: uncompiledPath,
name: getGlobalName(name),
format: 'iife',
fileName: 'index.js',
minify: false,
emptyOutDir: true,
})
config.define['process.env.NODE_ENV'] = JSON.stringify('development')
await viteBuild({ root: projectPath, ...config })
console.log(`[${name}] ✅ Built in ${Date.now() - startTime}ms`)
if (open && !browserOpened) {
browserOpened = true
openBrowser(url)
}
}
catch (error) {
console.error(`[${name}] ❌ Build error:`, error)
}
}
const rebuild = createRebuildQueue(buildBundle)
await rebuild()
console.log(`[${name}] Demo: ${url}/?libs=./uncompiled/${name}/index.js`)
const srcPath = path.join(projectPath, 'src')
console.log(`[${name}] 👀 Watching ${srcPath}...`)
fs.watch(srcPath, { recursive: true }, async (_, filename) => {
if (filename) {
console.log(`[${name}] 📝 Changed: ${filename}`)
await rebuild()
}
})
}
(async () => {
const { names, open } = await selectProjects(projects, 'dev')
if (names.length)
await develop(names[0], open)
})().catch((error) => {
runDevelop().catch((error) => {
console.error('❌ Development server failed:', error)
process.exitCode = 1
})
+98
View File
@@ -0,0 +1,98 @@
# Library build and development tooling
Use Node from `.node-version` and Yarn Classic 1.22.22. The historical JS/MJS
entrypoints remain checked compatibility shims; implementation lives in strict TS.
No runtime/build dependency or bundler was replaced by this migration.
| Module | Responsibility |
| --- | --- |
| `projects.ts` | Deterministic workspace discovery, CLI selection, non-TTY failure, one JS/TS entry |
| `names.ts` | Historical camelCase/PascalCase library global names |
| `config.ts` | Vite configuration and resource/worker/output targets; typed with installed Vite declarations |
| `banner.ts` | Version/license/notices, UMD AMD/global identity, inline-worker banner removal |
| `production.ts` | Sequential package/format builds, checked dist cleanup, docs copy and optional analysis |
| `analysis.ts` | Per-build module and artifact size/hash reports outside dist |
| `development.ts` | Servor demo startup, initial IIFE build, source watcher, first-success browser opening |
| `rebuild.ts` | One active build Promise, coalesced pending changes and later retry after rejection |
| `vendor.d.ts` | Only the consumed prompts 2.4.2 / Servor 4.0.2 APIs; verified against installed sources |
`build.js` and `dev.js` own command execution/error reporting. `utils.js`,
`projects.js`, `rebuild.js` and `build-analysis.mjs` keep their previous named
exports. Importing implementation modules does not start a server/build or read
the current workspace; the explicit run functions do that work. TypeScript
annotation removal uses the canonical Node version; Node 20 consumer validation
concerns the built libraries, not executing these repository TS tools.
## Stable commands and build behavior
```sh
yarn build artplayer artplayer-plugin-chapter
yarn build all --analyze
yarn dev artplayer --no-open
yarn build --help
yarn dev --help
yarn typecheck:library
yarn test:library
```
No-argument TTY selection uses the same prompts UI. Missing names in non-TTY mode,
unknown/inherited project names and ambiguous JS/TS entries fail before cleaning
dist or starting the server. Build format order remains modern UMD `.js` es2020,
legacy UMD `.legacy.js` es2015, then ESM `.mjs` es2020. Main/legacy use the same
Terser options; ESM remains unminified. Notice text, worker handling, AMD globals,
default exports, copied docs paths and `--analyze` output remain intact. A missing
or non-string manifest version now fails before that package's dist cleanup.
Multi-package builds are sequential, not an all-packages transaction.
Vite types constrain configuration but do not replace source typechecking. Avoid
casts that hide incompatible plugin/output types. `config.ts` deliberately sets
`publicDir: false` so core declaration sources cannot leak into distribution files.
Keep unknown UMD wrappers as explicit failures when upgrading Vite/Terser.
Performance artifact verification fingerprints the JS entry shims and all code/JSON
under `scripts/library/`, including additions/removals. Do not reduce that coverage
to `config.ts` alone: banners, name mapping, project discovery or analysis may also
affect artifacts. Markdown maintenance changes are not build inputs.
## Development lifetime and remaining server work
CLI defaults remain port 8082 and docs/index.html, with output at
docs/uncompiled/<package>/index.js. The output directory is created before Servor
enumerates directories for its Linux file watchers. The internal `runDevelop`
function accepts a port only for isolated callers/tests; the CLI does not add a
new flag. Do not stop an unrelated server occupying 8082 to run a test.
Build errors are reported while keeping the source watcher alive. Changes during
a build schedule one subsequent build rather than concurrent writes. Browser
opening happens once after the first successful build; `--no-open` suppresses it.
Only the package src tree is watched; external dependencies/configuration changes
still require restart. Tests use real automatic reload rather than racing it with
a second manual navigation.
Servor still owns its HTTP server, docs watchers, reload sockets/timers and SIGINT
handler until process exit; its API exposes no close handle. MOD-DEV-01 tracks
replacing/adapting that lifetime and fixing its reproduced occupied-port exit code
zero. `node refactor/scripts/servor-port-probe.mjs` reproduces that upstream defect
in an isolated child without touching 8082. It is not a candidate-success test.
This migration does not claim those server limitations are resolved.
## Evidence and regression ownership
- `refactor/scripts/build.test.mjs`: CLI validation, serialization/error recovery,
real TS/JS/Less/SVG/inline-worker builds, CJS/global/AMD/ESM and analysis identity.
- `test/library-build.test.js`: public files never copied into library dist.
- `test/performance-report.test.js`: changed/new/removed TS build inputs cannot
reuse stale installed performance artifacts.
- `test/plugin-scaffold.test.js`: newly generated plugins use these real tools.
- `test/browser/library-development.spec.js`: real dev server in Chromium, Firefox
and WebKit, worker messaging, automatic reload, source edits and error recovery.
Uses fixture ports and disposes its own child; it does not test media playback.
- `node refactor/scripts/library-build-comparison.mjs`: copy current package sources
into a retained fixture, build all libraries with frozen pre-migration scripts
and current scripts at the same paths, compare all artifact hashes. This is an
explicit whole-build validation, not part of every quick unit test.
Remote Linux/macOS/Windows CI remains a separate release gate. The local browser
evidence for MOD-02 is Windows; source inspection of Linux watcher registration
does not substitute for a remote Linux run. Lerna and root installation hooks were
not changed. The scaffold's separate guide remains in `scripts/plugin/README.md`.
+45
View File
@@ -0,0 +1,45 @@
import type { Plugin } from 'vite'
import { Buffer } from 'node:buffer'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { gzipSync } from 'node:zlib'
export function createBuildAnalysis() {
const root = process.cwd()
const parent = path.join(root, 'refactor/.cache/build-analysis')
fs.mkdirSync(parent, { recursive: true })
const directory = fs.mkdtempSync(path.join(parent, 'run-'))
fs.writeFileSync(path.join(parent, 'latest.json'), `${JSON.stringify({ output: path.relative(root, directory).replaceAll('\\', '/') })}\n`)
return { root, directory }
}
export function buildAnalysisPlugin({ root, directory }: ReturnType<typeof createBuildAnalysis>, name: string, format: string): Plugin {
return {
name: 'artplayer-build-analysis',
generateBundle(_options, bundle) {
for (const chunk of Object.values(bundle).filter(item => item.type === 'chunk')) {
const modules = Object.entries(chunk.modules).map(([id, info]) => {
const normalized = id.replaceAll('\\', '/')
const dependency = normalized.lastIndexOf('/node_modules/')
const file = id.startsWith('\0')
? `virtual:${id.slice(1)}`
: dependency === -1 ? path.relative(root, id).replaceAll('\\', '/') : normalized.slice(dependency + 1)
return { file, renderedBytes: Buffer.byteLength(info.code || ''), renderedCodeUnits: info.renderedLength, originalCodeUnits: info.originalLength, renderedExports: info.renderedExports, removedExports: info.removedExports }
}).sort((a, b) => b.renderedBytes - a.renderedBytes || a.file.localeCompare(b.file))
const report = {
schemaVersion: 1,
name,
format,
node: process.version,
file: chunk.fileName,
artifact: { bytes: Buffer.byteLength(chunk.code), gzip9: gzipSync(chunk.code, { level: 9 }).length, sha256: createHash('sha256').update(chunk.code).digest('hex') },
note: 'Rendered UTF-8 bytes precede final chunk minification; they are not additive compressed sizes or download measurements. Rollup lengths are separately recorded as UTF-16 code units. Original code units describe Rollup input, not raw TypeScript.',
modules,
}
fs.writeFileSync(path.join(directory, `${name}-${format}.json`), `${JSON.stringify(report, null, 2)}\n`)
}
},
}
}
+53
View File
@@ -0,0 +1,53 @@
import type { Plugin } from 'vite'
import fs from 'node:fs'
import path from 'node:path'
export function getBanner(project: string, name: string, version: string): string {
const noticeFile = path.join(project, 'THIRD_PARTY_NOTICES')
const notices = fs.existsSync(noticeFile) ? fs.readFileSync(noticeFile, 'utf8').trim() : ''
const noticeBlock = notices ? `\n *\n${notices.split(/\r?\n/).map(line => line.trimEnd() ? ` * ${line.trimEnd()}` : ' *').join('\n')}` : ''
return `/*!
* ${name}.js v${version}
* Github: https://github.com/zhw2590582/ArtPlayer
* (c) 2017-${new Date().getFullYear()} Harvey Zhao
* Released under the MIT License.${noticeBlock}
*/`
}
export function bannerPlugin(name: string, banner: string): Plugin {
return {
name: 'add-banner-and-global',
generateBundle(outputOptions, bundle) {
for (const chunk of Object.values(bundle)) {
if (chunk.type !== 'chunk')
continue
let code = chunk.code.replace(/`\/\*![\s\S]*?\*\/\n/g, '`')
if (!code.startsWith('/*!'))
code = `${banner}\n${code}`
if (outputOptions.format === 'umd') {
const wrapper = code.replace(/^\/\*![\s\S]*?\*\/\s*/, '').match(/^[!(\s]*function\s*\(\s*([\w$]+)\s*,\s*([\w$]+)\s*\)/)
if (!wrapper)
throw new Error(`Unrecognized UMD wrapper for ${name}`)
const global = wrapper[1]
const amd = /["']function["']\s*===?\s*typeof define\s*&&\s*define\.amd\s*\?\s*define\(([\w$]+)\)\s*:/
if (!amd.test(code))
throw new Error(`Unrecognized AMD branch for ${name}`)
code = code.replace(amd, `"function"==typeof define&&define.amd?(${global}.${name}=$1(),define(function(){return ${global}.${name}})):`)
}
chunk.code = code
}
},
}
}
export function workerBannerPlugin(): Plugin {
return {
name: 'remove-worker-banner',
generateBundle(_, bundle) {
for (const chunk of Object.values(bundle)) {
if (chunk.type === 'chunk' && chunk.code.startsWith('/*!'))
chunk.code = chunk.code.replace(/^\/\*![\s\S]*?\*\/\s*/, '')
}
},
}
}
+41
View File
@@ -0,0 +1,41 @@
import type { BuildOptions, InlineConfig, LibraryFormats, Plugin } from 'vite'
import { bannerPlugin, workerBannerPlugin } from './banner.ts'
export interface LibraryConfigOptions {
entry: string
outDir: string
name: string
format: LibraryFormats
fileName: string
minify?: boolean | 'esbuild' | 'terser'
target?: string | string[]
banner?: string
emptyOutDir?: boolean
}
type LibraryConfig = InlineConfig & {
build: BuildOptions & { rollupOptions: NonNullable<BuildOptions['rollupOptions']> & { plugins: Plugin[] } }
define: Record<string, string>
}
export function getViteBuildConfig(options: LibraryConfigOptions): LibraryConfig {
const { entry, outDir, name, format, fileName, minify = 'esbuild', target = 'es2020', banner, emptyOutDir = false } = options
return {
configFile: false,
publicDir: false,
logLevel: 'warn',
build: {
outDir,
emptyOutDir,
minify,
target,
lib: { entry, name, formats: [format], fileName: () => fileName },
rollupOptions: {
output: { exports: 'default' },
plugins: banner ? [bannerPlugin(name, banner)] : [],
},
},
worker: { format: 'es', plugins: () => [workerBannerPlugin()] },
define: { 'process.env.NODE_ENV': JSON.stringify(minify ? 'production' : 'development') },
}
}
+73
View File
@@ -0,0 +1,73 @@
import fs from 'node:fs'
import path from 'node:path'
import servor from 'servor'
import openBrowser from 'servor/utils/openBrowser.js'
import { build as viteBuild } from 'vite'
import { getViteBuildConfig } from './config.ts'
import { getGlobalName } from './names.ts'
import { getEntryFile, getProjects, selectProjects } from './projects.ts'
import { createRebuildQueue } from './rebuild.ts'
async function develop(projectPath: string, name: string, open: boolean, port: number) {
const uncompiledPath = path.resolve(`docs/uncompiled/${name}`)
let browserOpened = false
// Servor enumerates existing directories when installing its Linux watchers.
fs.mkdirSync(uncompiledPath, { recursive: true })
const { url } = await servor({
root: 'docs',
fallback: 'index.html',
reload: true,
port,
})
async function buildBundle() {
const startTime = Date.now()
try {
const config = getViteBuildConfig({
entry: getEntryFile(projectPath),
outDir: uncompiledPath,
name: getGlobalName(name),
format: 'iife',
fileName: 'index.js',
minify: false,
emptyOutDir: true,
})
config.define['process.env.NODE_ENV'] = JSON.stringify('development')
await viteBuild({ root: projectPath, ...config })
console.log(`[${name}] ✅ Built in ${Date.now() - startTime}ms`)
if (open && !browserOpened) {
browserOpened = true
openBrowser(url)
}
}
catch (error) {
console.error(`[${name}] ❌ Build error:`, error)
}
}
const rebuild = createRebuildQueue(buildBundle)
await rebuild()
console.log(`[${name}] Demo: ${url}/?libs=./uncompiled/${name}/index.js`)
const srcPath = path.join(projectPath, 'src')
console.log(`[${name}] 👀 Watching ${srcPath}...`)
fs.watch(srcPath, { recursive: true }, async (_, filename) => {
if (filename) {
console.log(`[${name}] 📝 Changed: ${filename}`)
await rebuild()
}
})
}
export async function runDevelop({ port = 8082 }: { port?: number } = {}): Promise<void> {
const projects = getProjects()
const { names, open } = await selectProjects(projects, 'dev')
const name = names[0]
if (name)
await develop(projects[name]!, name, open, port)
}
+11
View File
@@ -0,0 +1,11 @@
export function toPascalCase(name: string): string {
return name.replace(/(^|-)([a-z])/g, (_, _prefix: string, letter: string) => letter.toUpperCase())
}
export function toCamelCase(name: string): string {
return name.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase())
}
export function getGlobalName(name: string): string {
return name === 'artplayer' || name.startsWith('artplayer-tool-') ? toPascalCase(name) : toCamelCase(name)
}
+88
View File
@@ -0,0 +1,88 @@
import fs from 'node:fs'
import path from 'node:path'
import cpy from 'cpy'
import { build as viteBuild } from 'vite'
import { buildAnalysisPlugin, createBuildAnalysis } from './analysis.ts'
import { getBanner } from './banner.ts'
import { getViteBuildConfig } from './config.ts'
import { getGlobalName } from './names.ts'
import { getEntryFile, getProjects, selectProjects } from './projects.ts'
const BUILD_FORMATS = {
main: { format: 'umd', ext: '.js', target: 'es2020', minify: 'terser' },
legacy: { format: 'umd', ext: '.legacy.js', target: 'es2015', minify: 'terser' },
esm: { format: 'es', ext: '.mjs', target: 'es2020', minify: false },
} as const
async function build(projectDir: string, compiledPath: string, name: string, targetName: keyof typeof BUILD_FORMATS, clean = false, analysis?: ReturnType<typeof createBuildAnalysis>) {
const manifest: unknown = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf-8'))
if (!manifest || typeof manifest !== 'object' || !('version' in manifest) || typeof manifest.version !== 'string')
throw new Error(`Missing package version: ${name}`)
const { version } = manifest
const distDir = path.join(projectDir, 'dist')
const entryFile = getEntryFile(projectDir)
if (clean && fs.existsSync(distDir)) {
if (fs.realpathSync(distDir) !== path.join(fs.realpathSync(projectDir), 'dist'))
throw new Error(`Refusing to clean redirected dist directory: ${distDir}`)
fs.rmSync(distDir, { recursive: true, force: true })
}
const startTime = Date.now()
const { format, ext, target, minify } = BUILD_FORMATS[targetName]
const fileName = `${name}${ext}`
const config = getViteBuildConfig({
entry: entryFile,
outDir: distDir,
name: getGlobalName(name),
format,
fileName,
minify,
target,
banner: getBanner(projectDir, name, version),
})
// Terser options for all builds (preserve banner comments starting with /*!)
const isLegacy = targetName === 'legacy'
config.build.terserOptions = {
ecma: isLegacy ? 5 : 2020,
compress: { ecma: isLegacy ? 5 : 2020, comparisons: false, inline: 2 },
mangle: { safari10: true },
format: {
ecma: isLegacy ? 5 : 2020,
comments: /^!/,
ascii_only: true,
max_line_len: false, // No line length limit
},
}
if (analysis)
config.build.rollupOptions.plugins.push(buildAnalysisPlugin(analysis, name, targetName))
await viteBuild({ root: projectDir, ...config })
const outFile = path.join(distDir, fileName)
const size = (fs.statSync(outFile).size / 1024).toFixed(2)
await cpy(outFile, compiledPath, { flat: true })
console.log(`✨ Built@${targetName} ${name}@${version} Time@${Date.now() - startTime}ms Size@${size}kb`)
}
async function buildProject(projectDir: string, compiledPath: string, name: string, analysis?: ReturnType<typeof createBuildAnalysis>) {
await build(projectDir, compiledPath, name, 'main', true, analysis)
await build(projectDir, compiledPath, name, 'legacy', false, analysis)
await build(projectDir, compiledPath, name, 'esm', false, analysis)
}
export async function runBuild(): Promise<void> {
const projects = getProjects()
const compiledPath = path.resolve('docs/compiled')
const { names, analyze } = await selectProjects(projects, 'build')
const analysis = analyze ? createBuildAnalysis() : undefined
for (const name of names)
await buildProject(projects[name]!, compiledPath, name, analysis)
if (analysis)
console.log(`Build analysis: ${analysis.directory}`)
if (names.length)
console.log(`✅ Finished building ${names.length} package(s)!`)
}
+72
View File
@@ -0,0 +1,72 @@
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import prompts from 'prompts'
export type Projects = Record<string, string>
export type Command = 'build' | 'dev'
export interface Selection { names: string[], open: boolean, analyze?: boolean, help?: boolean }
export function getProjects(root = process.cwd()): Projects {
const packages = path.join(root, 'packages')
return Object.fromEntries(fs.readdirSync(packages).sort()
.filter(name => name !== 'artplayer-vitepress' && fs.existsSync(path.join(packages, name, 'package.json')))
.map(name => [name, path.join(packages, name)]))
}
export function getEntryFile(projectDir: string): string {
const entries = ['index.js', 'index.ts'].map(name => path.join(projectDir, 'src', name)).filter(file => fs.existsSync(file) && fs.statSync(file).isFile())
if (entries.length !== 1)
throw new Error(`${path.basename(projectDir)} must have exactly one src/index.js or src/index.ts; found ${entries.length}`)
return entries[0]!
}
export function parseProjects(args: string[], projects: Projects, command: Command): Selection {
const names: string[] = []
let open = true
let analyze = false
for (const arg of args) {
if (arg === '--no-open' && command === 'dev')
open = false
else if (arg === '--analyze' && command === 'build')
analyze = true
else if (arg === '--help')
return { help: true, names: [], open }
else if (arg !== '--')
names.push(arg)
}
if (names.includes('all')) {
if (command !== 'build' || names.length !== 1)
throw new Error('Use build all on its own; dev accepts one package')
return { names: Object.keys(projects), open, analyze }
}
for (const name of names) {
if (!Object.hasOwn(projects, name))
throw new Error(`Unknown package: ${name}. Use yarn ${command} --help`)
}
if (command === 'dev' && names.length > 1)
throw new Error('dev accepts one package')
return { names: [...new Set(names)], open, analyze }
}
export async function selectProjects(projects: Projects, command: Command) {
const selection = parseProjects(process.argv.slice(2), projects, command)
if (selection.help) {
console.log(command === 'build' ? 'Usage: yarn build [all | package ...] [--analyze]' : 'Usage: yarn dev [package] [--no-open]')
return selection
}
if (!selection.names.length) {
if (!process.stdin.isTTY)
throw new Error(`Select a package explicitly in non-interactive mode: yarn ${command} artplayer`)
const { value } = await prompts({
type: 'select',
name: 'value',
message: `Which project do you want to ${command === 'build' ? 'build' : 'develop'}?`,
choices: Object.keys(projects).map(name => ({ title: name, value: name })),
})
selection.names = value ? [value] : []
}
for (const name of selection.names)
getEntryFile(projects[name]!)
return selection
}
+16
View File
@@ -0,0 +1,16 @@
export function createRebuildQueue(build: () => void | Promise<void>): () => Promise<void> {
let pending = false
let active: Promise<void> | undefined
return () => {
pending = true
if (!active) {
active = Promise.resolve().then(async () => {
while (pending) {
pending = false
await build()
}
}).finally(() => { active = undefined })
}
return active
}
}
+22
View File
@@ -0,0 +1,22 @@
// Only the installed APIs used by library tooling; not general SDK declarations.
declare module 'prompts' {
export default function prompts(question: {
type: 'select'
name: 'value'
message: string
choices: { title: string, value: string }[]
}): Promise<{ value?: string }>
}
declare module 'servor' {
export default function servor(options: {
root: string
fallback: string
reload: boolean
port: number
}): Promise<{ url: string }>
}
declare module 'servor/utils/openBrowser.js' {
export default function openBrowser(url: string): true | undefined
}
+4
View File
@@ -29,6 +29,10 @@ export function verifyPerformanceArtifacts(root, mapFile) {
toolchain[file] = digest(path.join(root, file))
assert.equal(digest(path.join(directory, 'build', file)), toolchain[file], `Stale build input: ${file}`)
}
const libraryInputs = location => Object.fromEntries(walk(path.join(location, 'scripts/library')).filter(file => /\.(?:[cm]?[jt]s|json)$/.test(file)).sort().map(file => [path.relative(location, file).replaceAll('\\', '/'), digest(file)]))
const currentLibrary = libraryInputs(root)
assert.deepEqual(libraryInputs(path.join(directory, 'build')), currentLibrary, 'Stale library build inputs; rerun yarn test:package')
Object.assign(toolchain, currentLibrary)
const inputs = []
for (const name of ['artplayer', 'artplayer-plugin-chapter']) {
const pkg = report.packages.find(pkg => pkg.name === name)
+1 -68
View File
@@ -1,68 +1 @@
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import prompts from 'prompts'
export function getProjects(root = process.cwd()) {
const packages = path.join(root, 'packages')
return Object.fromEntries(fs.readdirSync(packages).sort()
.filter(name => name !== 'artplayer-vitepress' && fs.existsSync(path.join(packages, name, 'package.json')))
.map(name => [name, path.join(packages, name)]))
}
export function getEntryFile(projectDir) {
const entries = ['index.js', 'index.ts'].map(name => path.join(projectDir, 'src', name)).filter(file => fs.existsSync(file) && fs.statSync(file).isFile())
if (entries.length !== 1)
throw new Error(`${path.basename(projectDir)} must have exactly one src/index.js or src/index.ts; found ${entries.length}`)
return entries[0]
}
export function parseProjects(args, projects, command) {
const names = []
let open = true
let analyze = false
for (const arg of args) {
if (arg === '--no-open' && command === 'dev')
open = false
else if (arg === '--analyze' && command === 'build')
analyze = true
else if (arg === '--help')
return { help: true, names: [], open }
else if (arg !== '--')
names.push(arg)
}
if (names.includes('all')) {
if (command !== 'build' || names.length !== 1)
throw new Error('Use build all on its own; dev accepts one package')
return { names: Object.keys(projects), open, analyze }
}
for (const name of names) {
if (!Object.hasOwn(projects, name))
throw new Error(`Unknown package: ${name}. Use yarn ${command} --help`)
}
if (command === 'dev' && names.length > 1)
throw new Error('dev accepts one package')
return { names: [...new Set(names)], open, analyze }
}
export async function selectProjects(projects, command) {
const selection = parseProjects(process.argv.slice(2), projects, command)
if (selection.help) {
console.log(command === 'build' ? 'Usage: yarn build [all | package ...] [--analyze]' : 'Usage: yarn dev [package] [--no-open]')
return selection
}
if (!selection.names.length) {
if (!process.stdin.isTTY)
throw new Error(`Select a package explicitly in non-interactive mode: yarn ${command} artplayer`)
const { value } = await prompts({
type: 'select',
name: 'value',
message: `Which project do you want to ${command === 'build' ? 'build' : 'develop'}?`,
choices: Object.keys(projects).map(name => ({ title: name, value: name })),
})
selection.names = value ? [value] : []
}
for (const name of selection.names)
getEntryFile(projects[name])
return selection
}
export { getEntryFile, getProjects, parseProjects, selectProjects } from './library/projects.ts'
+1 -16
View File
@@ -1,16 +1 @@
export function createRebuildQueue(build) {
let pending = false
let active
return () => {
pending = true
if (!active) {
active = Promise.resolve().then(async () => {
while (pending) {
pending = false
await build()
}
}).finally(() => { active = undefined })
}
return active
}
}
export { createRebuildQueue } from './library/rebuild.ts'
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["node"],
"allowImportingTsExtensions": true,
"checkJs": true
},
"include": ["library/**/*.ts", "build.js", "dev.js", "utils.js", "projects.js", "rebuild.js", "build-analysis.mjs"]
}
+3 -114
View File
@@ -1,114 +1,3 @@
export { getProjects } from './projects.js'
// Convert kebab-case to PascalCase: artplayer-plugin-ads -> ArtplayerPluginAds
export function toPascalCase(name) {
return name.replace(/(^|-)([a-z])/g, (_, _p, c) => c.toUpperCase())
}
// Convert kebab-case to camelCase: artplayer-plugin-ads -> artplayerPluginAds
export function toCamelCase(name) {
return name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
}
// Get the global variable name for a package
// - artplayer, artplayer-tool-* -> PascalCase (class)
// - artplayer-plugin-*, artplayer-proxy-* -> camelCase (function)
export function getGlobalName(name) {
if (name === 'artplayer' || name.startsWith('artplayer-tool-')) {
return toPascalCase(name)
}
return toCamelCase(name)
}
// Get Vite build config
export function getViteBuildConfig(options) {
const {
entry,
outDir,
name,
format,
fileName,
minify = 'esbuild',
target = 'es2020',
banner,
emptyOutDir = false,
} = options
const config = {
configFile: false,
publicDir: false,
logLevel: 'warn',
build: {
outDir,
emptyOutDir,
minify,
target,
lib: {
entry,
name,
formats: [format],
fileName: () => fileName,
},
rollupOptions: {
output: {
exports: 'default',
},
plugins: banner
? [{
name: 'add-banner-and-global',
generateBundle(outputOptions, bundle) {
for (const chunk of Object.values(bundle)) {
if (chunk.type === 'chunk') {
// Remove any inline worker banners (inside template literals)
// Match: `/*! ... */\n and replace with just `
let code = chunk.code.replace(/`\/\*![\s\S]*?\*\/\n/g, '`')
// Add main banner if not present
if (!code.startsWith('/*!')) {
code = `${banner}\n${code}`
}
// For UMD format, modify the wrapper to always expose global
// even when AMD loader is present (fix for RequireJS environments)
if (outputOptions.format === 'umd') {
const wrapper = code.replace(/^\/\*![\s\S]*?\*\/\s*/, '').match(/^[!(\s]*function\s*\(\s*([\w$]+)\s*,\s*([\w$]+)\s*\)/)
if (!wrapper)
throw new Error(`Unrecognized UMD wrapper for ${name}`)
const global = wrapper[1]
const amd = /["']function["']\s*===?\s*typeof define\s*&&\s*define\.amd\s*\?\s*define\(([\w$]+)\)\s*:/
if (!amd.test(code))
throw new Error(`Unrecognized AMD branch for ${name}`)
// Keep AMD and the global export identical regardless of minified parameter names.
code = code.replace(
amd,
`"function"==typeof define&&define.amd?(${global}.${name}=$1(),define(function(){return ${global}.${name}})):`,
)
}
chunk.code = code
}
}
},
}]
: [],
},
},
// Disable banner for inline workers
worker: {
format: 'es',
plugins: () => [{
name: 'remove-worker-banner',
generateBundle(_, bundle) {
for (const chunk of Object.values(bundle)) {
if (chunk.type === 'chunk' && chunk.code.startsWith('/*!')) {
// Remove banner from worker code
chunk.code = chunk.code.replace(/^\/\*![\s\S]*?\*\/\s*/, '')
}
}
},
}],
},
define: {
'process.env.NODE_ENV': JSON.stringify(minify ? 'production' : 'development'),
},
}
return config
}
export { getViteBuildConfig } from './library/config.ts'
export { getGlobalName, toCamelCase, toPascalCase } from './library/names.ts'
export { getProjects } from './library/projects.ts'
+5
View File
@@ -1,5 +1,10 @@
# Tests and fixture ownership
`yarn test:library` runs actual library builds, build-input freshness checks and the
scaffold's generated consumers. The new build modules live in `scripts/library/`;
`yarn typecheck:library` checks them and the retained JS/MJS entrypoints. Browser
development/watch/error-recovery coverage is `browser/library-development.spec.js`.
`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;
+6
View File
@@ -1,5 +1,11 @@
# Browser regression entry
`library-development.spec.js` starts the actual typed development runner on an
isolated fixture port. It checks generated TS/Less/SVG/inline-worker assets,
automatic reload on edits and recovery after a compiler error in all three
engines. It owns and stops only its child server, never the user's port 8082
session. This is development-tool validation, not media playback acceptance.
`site-loading.spec.js` covers actual desktop/mobile loading, dependency order,
failure/retry/AMD restoration, example priority, repeated Run and stale responses,
plus Run Code and language routing. External analytics/ads are inert test routes;
+101
View File
@@ -0,0 +1,101 @@
import { spawn } from 'node:child_process'
import fs from 'node:fs'
import net from 'node:net'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { expect, test } from '@playwright/test'
const root = fileURLToPath(new URL('../../', import.meta.url))
const cache = path.join(root, 'refactor/.cache')
function removeFixture(directory) {
if (fs.realpathSync(directory) !== directory || path.dirname(directory) !== fs.realpathSync(cache))
throw new Error(`Refusing redirected fixture cleanup: ${directory}`)
fs.rmSync(directory, { recursive: true, force: true })
}
async function freePort() {
const server = net.createServer()
await new Promise((resolve, reject) => {
server.once('error', reject)
server.listen(0, resolve)
})
const port = server.address().port
await new Promise(resolve => server.close(resolve))
return port
}
test('typed development builds real assets and workers, watches edits and recovers from compilation errors', async ({ page }, testInfo) => {
test.setTimeout(60000)
const directory = fs.mkdtempSync(path.join(cache, 'library-development-'))
const name = 'artplayer-plugin-build-probe'
const source = path.join(directory, 'packages', name, 'src')
fs.mkdirSync(source, { recursive: true })
fs.cpSync(path.join(root, 'refactor/fixtures/build'), source, { recursive: true })
fs.writeFileSync(path.join(source, '../package.json'), JSON.stringify({ name, version: '1.0.0' }))
fs.mkdirSync(path.join(directory, 'docs'))
fs.writeFileSync(path.join(directory, 'docs/index.html'), `<!doctype html><html><body><main></main>
<script src="/uncompiled/${name}/index.js"></script><script>
window.probe = artplayerPluginBuildProbe();
document.querySelector('main').textContent = String(probe.value);
</script></body></html>`)
const port = await freePort()
const url = `http://localhost:${port}`
const module = pathToFileURL(path.join(root, 'scripts/library/development.ts')).href
const code = `import { runDevelop } from ${JSON.stringify(module)};
process.argv = [process.execPath, 'fixture.js', '${name}', '--no-open'];
await runDevelop({port:${port}});`
const launcher = path.join(directory, 'launch.mjs')
fs.writeFileSync(launcher, code)
const child = spawn(process.execPath, [launcher], { cwd: directory, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
let output = ''
child.stdout.on('data', (data) => {
output += data
})
child.stderr.on('data', (data) => {
output += data
})
const exited = new Promise(resolve => child.once('exit', resolve))
try {
await expect.poll(() => output, { timeout: 20000 }).toContain('Watching')
expect(child.exitCode).toBeNull()
await page.goto(url)
await expect(page.locator('main')).toHaveText('42')
expect(await page.evaluate(() => window.probe.css)).toContain('#123abc')
expect(await page.evaluate(() => window.probe.svg)).toContain('<svg')
const workerResult = await page.evaluate(() => new Promise((resolve, reject) => {
const worker = window.probe.createWorker()
worker.onmessage = ({ data }) => {
worker.terminate()
resolve(data)
}
worker.onerror = (error) => {
worker.terminate()
reject(new Error(error.message))
}
worker.postMessage(21)
}))
expect(workerResult).toBe(22)
const entry = path.join(source, 'index.ts')
const original = fs.readFileSync(entry, 'utf8')
const before = output.length
fs.writeFileSync(entry, original.replace('add(20, 22)', 'add(20, 23)'))
await expect.poll(() => output.slice(before), { timeout: 15000 }).toContain('Built in')
await expect(page.locator('main')).toHaveText('43')
const valid = output.length
fs.writeFileSync(entry, 'export default function broken( {')
await expect.poll(() => output.slice(valid), { timeout: 15000 }).toContain('Build error')
expect(child.exitCode).toBeNull()
const failed = output.length
fs.writeFileSync(entry, original.replace('add(20, 22)', 'add(20, 24)'))
await expect.poll(() => output.slice(failed), { timeout: 15000 }).toContain('Built in')
await expect(page.locator('main')).toHaveText('44')
}
finally {
await testInfo.attach('development-server.log', { body: output, contentType: 'text/plain' })
child.kill()
await exited
removeFixture(directory)
}
})
+7
View File
@@ -87,6 +87,7 @@ test('performance refuses stale sources, build tools and altered installed bundl
'scripts/build.js': 'build fixture\n',
'scripts/utils.js': 'utils fixture\n',
'scripts/projects.js': 'projects fixture\n',
'scripts/library/config.ts': 'typed build fixture\n',
}
for (const [file, content] of Object.entries(toolchain)) {
write(path.join(root, file), content)
@@ -115,6 +116,12 @@ test('performance refuses stale sources, build tools and altered installed bundl
write(path.join(root, 'scripts/build.js'), 'changed compiler\n')
assert.throws(() => verifyPerformanceArtifacts(root, map), /Stale build input/)
write(path.join(root, 'scripts/build.js'), toolchain['scripts/build.js'])
write(path.join(root, 'scripts/library/config.ts'), 'changed TS config\n')
assert.throws(() => verifyPerformanceArtifacts(root, map), /Stale library build inputs/)
write(path.join(root, 'scripts/library/config.ts'), toolchain['scripts/library/config.ts'])
write(path.join(snapshot, 'scripts/library/removed.ts'), 'stale module\n')
assert.throws(() => verifyPerformanceArtifacts(root, map), /Stale library build inputs/)
fs.unlinkSync(path.join(snapshot, 'scripts/library/removed.ts'))
write(path.join(output, 'artifacts/artplayer/dist/artplayer.js'), 'changed bundle\n')
assert.throws(() => verifyPerformanceArtifacts(root, map), /Installed performance artifact changed/)
})