diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 850e98451..8ba73f8ba 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -133,6 +133,8 @@ jobs:
run: node scripts/package-runtime.mjs --canonical 2>&1 | tee refactor/.cache/ci/consumer-node-canonical.log
- name: Verify React installed consumers
run: yarn test:react-consumer 2>&1 | tee refactor/.cache/ci/react-consumer.log
+ - name: Verify Vue installed consumers
+ run: yarn test:vue-consumer 2>&1 | tee refactor/.cache/ci/vue-consumer.log
- name: Run all three engines
run: yarn test:browser 2>&1 | tee refactor/.cache/ci/browser.log
- name: Verify iframe cached history and interrupted navigation
@@ -149,6 +151,7 @@ jobs:
refactor/.cache/browser/
refactor/.cache/iframe-history/
refactor/.cache/react-consumer-*/
+ refactor/.cache/vue-consumer-*/
refactor/.cache/performance/
refactor/.cache/packages/latest.json
refactor/.cache/packages/run-*/*.json
diff --git a/example/vue.js/README.md b/example/vue.js/README.md
index bf72983ef..ecccd11bd 100644
--- a/example/vue.js/README.md
+++ b/example/vue.js/README.md
@@ -1,29 +1,76 @@
-# vue-project
+# Vue consumer example
-This template should help get you started developing with Vue 3 in Vite.
-
-## Recommended IDE Setup
-
-[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
-
-## Customize configuration
-
-See [Vite Configuration Reference](https://vite.dev/config/).
-
-## Project Setup
+Run the example from the repository root with Node from .node-version and Yarn
+Classic 1.22.22. Use yarn install --frozen-lockfile and only the root yarn.lock.
```sh
-npm install
+yarn dev:vue
+yarn typecheck:vue
+yarn lint:vue
+yarn build:vue
+yarn test:vue-consumer
```
-### Compile and Hot-Reload for Development
+The build output is refactor/.cache/vue-example-dist.
+Rebuild changed library distributions with yarn build followed by the package name.
-```sh
-npm run dev
-```
+## Files and ownership
-### Compile and Minify for Production
+- src/Player.vue owns the player and container through shallow refs. It constructs
+ once on mount, emits getInstance, then releases its reference before calling
+ destroy(false) on owner unmount. Vue owns the outer div. Player state is private:
+ the component ref exposes the normal Vue surface, not an art property.
+- src/player-options.ts holds this example's existing configuration overrides:
+ French language, French/Indonesian dictionaries and fresh Danmuku/Document PiP
+ factories per mount. They still override option.lang, i18n and plugins. The
+ container comes from Vue; other options pass through. The url assertion preserves
+ the historical Partial input while the core performs runtime validation.
+- src/App.vue demonstrates the existing event/prop syntax. Its reactive option
+ object keeps narrow field inference and is checked against `Partial`.
+- src/main.ts is the client entry referenced by index.html. tsconfig.json replaces
+ jsconfig.json, preserves the @ alias and enables strict source/template checking.
+- vite.config.js uses the standard Vue plugin. The unused optional devtools overlay
+ was removed from this example's build dependencies; no player UI was changed.
-```sh
-npm run build
-```
+## Compatibility and updates
+
+The wrapper retains required option: `Partial ` and the getInstance event.
+The array-form event declaration remains intentionally unchanged: historical
+listener type extraction is not narrowed. Annotate event handlers with Artplayer
+as App.vue does. Missing URLs may typecheck through Partial but still fail the
+core's runtime validation.
+
+Changing an option object or its fields does not recreate or update the player.
+Use the received instance's methods, such as switchUrl, for live updates. Change
+the component key to reconstruct using the latest options. No deep option watcher
+or new event/ref contract was introduced. CSS classes and styles still fall through
+to the root div; other native attributes continue to work for JS consumers.
+Strict template checking distinguishes declared props from arbitrary attributes.
+
+Vue captures listener errors through its error handling. A mounted component
+retains its instance after a captured listener error and destroys it on unmount.
+KeepAlive deactivation retains the instance; owner unmount destroys it. The example
+does not silently pause or destroy players when cached. Use player APIs if your
+application needs that behavior.
+
+## Reproducible checks
+
+Root development dependencies pin Vue/compiler-sfc 3.5.28, Vue Vite plugin 6.0.1,
+vue-tsc 3.3.11, TypeScript 5.9.3 and Vite 7.3.6. No player runtime dependency changes.
+The isolated consumer installs actual packed core, Danmuku and Document PiP files,
+frozen-reinstalls them, and verifies every package file. Its Vue compiler and all
+resolved declarations also live in that isolated install; no source aliases or
+workspace type fallbacks are permitted. A plain-JS SFC separately verifies the
+legacy event, attribute and repeated-mount syntax.
+
+Development/production browser runs cover native play/pause/seek and pixels,
+instance API source updates, option/key behavior, refs, siblings, repeated mount,
+listener errors, KeepAlive and real Worker termination. Video/XML responses use
+controlled fixtures. Plugin registration does not certify window PiP interaction,
+full Danmuku scheduling, CDN availability or physical macOS/iOS Safari.
+
+yarn test:vue-consumer --before runs the original Player.vue from the recorded Git
+baseline against the same candidate packages. Both old and new wrappers must pass;
+this is a compatibility control, not an old published npm wrapper test. Evidence
+is retained in unique refactor/.cache/vue-consumer-* folders. GitHub runs the test
+and uploads these reports; local success does not imply verified remote jobs.
diff --git a/example/vue.js/eslint.config.js b/example/vue.js/eslint.config.js
new file mode 100644
index 000000000..d62d06702
--- /dev/null
+++ b/example/vue.js/eslint.config.js
@@ -0,0 +1,2 @@
+// Reuse the shared Vue and TypeScript rules.
+export { default } from '../../eslint.config.js'
diff --git a/example/vue.js/index.html b/example/vue.js/index.html
index fa489296e..b5872b94e 100644
--- a/example/vue.js/index.html
+++ b/example/vue.js/index.html
@@ -1,13 +1,13 @@
-
+
-
-
-
+
+
+
artplayer-vue
-
+
diff --git a/example/vue.js/jsconfig.json b/example/vue.js/jsconfig.json
deleted file mode 100644
index 5a1f2d222..000000000
--- a/example/vue.js/jsconfig.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "compilerOptions": {
- "paths": {
- "@/*": ["./src/*"]
- }
- },
- "exclude": ["node_modules", "dist"]
-}
diff --git a/example/vue.js/package.json b/example/vue.js/package.json
index 1da25d869..ac018af96 100644
--- a/example/vue.js/package.json
+++ b/example/vue.js/package.json
@@ -3,23 +3,28 @@
"type": "module",
"version": "0.0.0",
"private": true,
+ "packageManager": "yarn@1.22.22",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
- "build": "vite build",
- "preview": "vite preview"
+ "build": "yarn typecheck && vite build",
+ "preview": "vite preview",
+ "typecheck": "vue-tsc -p tsconfig.json --noEmit",
+ "lint": "eslint ."
},
"dependencies": {
"artplayer": "^5.3.0-beta.4",
"artplayer-plugin-danmuku": "^5.2.0",
"artplayer-plugin-document-pip": "^1.0.2",
- "vue": "^3.5.21"
+ "vue": "3.5.28"
},
"devDependencies": {
- "@vitejs/plugin-vue": "^6.0.1",
- "vite": "^7.1.4",
- "vite-plugin-vue-devtools": "^8.0.1"
+ "@vitejs/plugin-vue": "6.0.1",
+ "eslint": "9.39.2",
+ "typescript": "5.9.3",
+ "vite": "7.3.6",
+ "vue-tsc": "3.3.11"
}
}
diff --git a/example/vue.js/src/App.vue b/example/vue.js/src/App.vue
index 3e6f2b222..ce0eaf276 100644
--- a/example/vue.js/src/App.vue
+++ b/example/vue.js/src/App.vue
@@ -1,12 +1,13 @@
diff --git a/example/vue.js/src/Player.vue b/example/vue.js/src/Player.vue
index 10d3d5814..4f71473f5 100644
--- a/example/vue.js/src/Player.vue
+++ b/example/vue.js/src/Player.vue
@@ -1,44 +1,28 @@
diff --git a/example/vue.js/src/main.js b/example/vue.js/src/main.ts
similarity index 100%
rename from example/vue.js/src/main.js
rename to example/vue.js/src/main.ts
diff --git a/example/vue.js/src/player-options.ts b/example/vue.js/src/player-options.ts
new file mode 100644
index 000000000..a5f9d3ff3
--- /dev/null
+++ b/example/vue.js/src/player-options.ts
@@ -0,0 +1,23 @@
+import type { Option } from 'artplayer'
+import artplayerPluginDanmuku from 'artplayer-plugin-danmuku'
+import artplayerPluginDocumentPip from 'artplayer-plugin-document-pip'
+import fr from 'artplayer/i18n/fr'
+import id from 'artplayer/i18n/id'
+
+// Preserve this example's existing overrides and create plugins per instance.
+export function playerOptions(option: Partial , container: HTMLDivElement): Option {
+ return {
+ ...option,
+ container,
+ // Partial is the historical wrapper API; Artplayer validates missing URLs.
+ url: option.url as string,
+ i18n: { id, fr },
+ lang: 'fr',
+ plugins: [
+ artplayerPluginDocumentPip({}),
+ artplayerPluginDanmuku({
+ danmuku: 'https://artplayer.org/assets/sample/danmuku.xml',
+ }),
+ ],
+ }
+}
diff --git a/example/vue.js/tsconfig.json b/example/vue.js/tsconfig.json
new file mode 100644
index 000000000..192c390e2
--- /dev/null
+++ b/example/vue.js/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "paths": { "@/*": ["./src/*"] },
+ "types": [],
+ "allowJs": true,
+ "checkJs": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "skipLibCheck": false
+ },
+ "vueCompilerOptions": { "strictTemplates": true },
+ "include": ["src"]
+}
diff --git a/example/vue.js/vite.config.js b/example/vue.js/vite.config.js
index 48e16d2b4..65f5da150 100644
--- a/example/vue.js/vite.config.js
+++ b/example/vue.js/vite.config.js
@@ -2,13 +2,11 @@ import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
-import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
- vueDevTools(),
],
resolve: {
alias: {
diff --git a/package.json b/package.json
index 652d0f784..2484ca6b7 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
"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: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 check:types && yarn typecheck && yarn typecheck:react && yarn lint:react && 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 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: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",
@@ -113,7 +113,12 @@
"typecheck:react": "tsc -p example/react.js/tsconfig.json --noEmit",
"build:react": "yarn typecheck:react && vite build example/react.js --config example/react.js/vite.config.js --outDir ../../refactor/.cache/react-example-dist",
"test:react-consumer": "node refactor/scripts/react-consumer.mjs",
- "lint:react": "eslint example/react.js --no-fix"
+ "lint:react": "eslint example/react.js --no-fix",
+ "dev:vue": "vite example/vue.js --config example/vue.js/vite.config.js",
+ "typecheck:vue": "vue-tsc -p example/vue.js/tsconfig.json --noEmit",
+ "build:vue": "yarn typecheck:vue && vite build example/vue.js --config example/vue.js/vite.config.js --outDir ../../refactor/.cache/vue-example-dist",
+ "test:vue-consumer": "node refactor/scripts/vue-consumer.mjs",
+ "lint:vue": "eslint example/vue.js --no-fix"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
@@ -122,6 +127,7 @@
"@types/react": "19.1.10",
"@types/react-dom": "19.1.7",
"@vitejs/plugin-react": "5.0.0",
+ "@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.28",
"@yarnpkg/lockfile": "1.1.0",
"c8": "12.0.0",
@@ -149,6 +155,8 @@
"typescript-compat": "npm:typescript@4.3.5",
"typescript-runtime-compat": "npm:typescript@5.1.6",
"vite": "7.3.6",
+ "vue": "3.5.28",
+ "vue-tsc": "3.3.11",
"yaml": "2.8.2"
},
"resolutions": {
diff --git a/refactor/baselines/vue-consumer-validation.json b/refactor/baselines/vue-consumer-validation.json
new file mode 100644
index 000000000..214c85e5a
--- /dev/null
+++ b/refactor/baselines/vue-consumer-validation.json
@@ -0,0 +1,1519 @@
+{
+ "task": "EX-02",
+ "environment": {
+ "os": "Windows",
+ "node": "24.21.0",
+ "yarn": "1.22.22"
+ },
+ "reports": [
+ {
+ "file": "refactor/.cache/vue-consumer-jGIWSm/report.json",
+ "sha256": "019d4cac8a69d737b5893d6a0898660e0115a785fd38d4a253d0faa115bad19f"
+ },
+ {
+ "file": "refactor/.cache/vue-consumer-HpwUKY/report.json",
+ "sha256": "f265f75308d78bf93302a53d066446691bd2fcaded36a68fe6522b1ed3163677"
+ },
+ {
+ "file": "refactor/.cache/react-consumer-ZyMc7G/report.json",
+ "sha256": "a97bdf4c014f31608de8147bdf86f55b0393ad4a4a64be19ace31e2477db6bc7"
+ }
+ ],
+ "candidate": {
+ "task": "EX-02",
+ "before": false,
+ "node": "v24.21.0",
+ "source": "4df96893649ee0d3ffe1504af0609c3c6e023409",
+ "packages": [
+ {
+ "name": "artplayer",
+ "archive": "D:\\github\\ArtPlayer\\refactor\\.cache\\vue-consumer-jGIWSm\\artplayer.tgz",
+ "sha256": "0c0ea52d41575261b836aefac68280733b119c75834ecad6d4bc78edb073f4c1"
+ },
+ {
+ "name": "artplayer-plugin-danmuku",
+ "archive": "D:\\github\\ArtPlayer\\refactor\\.cache\\vue-consumer-jGIWSm\\artplayer-plugin-danmuku.tgz",
+ "sha256": "7d1f0d4e3cf4a5579aa8a0f8c850b6205bfdd7e70c46244db6ebc4f7a866a949"
+ },
+ {
+ "name": "artplayer-plugin-document-pip",
+ "archive": "D:\\github\\ArtPlayer\\refactor\\.cache\\vue-consumer-jGIWSm\\artplayer-plugin-document-pip.tgz",
+ "sha256": "4ac0fca9a5a36c1039a34f37caf67479691f153ffda02ed2bc3a96946914a8c2"
+ }
+ ],
+ "browsers": [
+ {
+ "mode": "development",
+ "engine": "chromium",
+ "version": "153.0.8010.12",
+ "checks": [
+ "mount-event-closed-component-ref-and-plugin-registration",
+ "mutate-option-retains-mount-only-contract",
+ "replace-option-retains-mount-only-contract",
+ "native-play-pause-seek-and-decoded-pixels",
+ "instance-api-updates-source-without-remount",
+ "key-change-reconstructs-with-current-props",
+ "sibling-cleanup-retains-other-instance",
+ "optional-listener-and-unmount",
+ "cancel-before-mount-does-not-allocate",
+ "three-mount-unmount-cycles-clean-once",
+ "vue-captured-listener-error-retains-instance-until-unmount",
+ "listener-triggered-unmount-cleans-created-instance",
+ "keepalive-retains-on-deactivation-and-destroys-on-owner-unmount",
+ "plain-js-event-prop-usage-and-remount",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true,
+ "media": {
+ "time": 0.155798,
+ "paused": true,
+ "seek": 1,
+ "width": 320,
+ "rgba": {
+ "sha256": "fc838a7f2b0d4c7cb6482718dfffe6f33cd05cc8d1a5efec70c56a2cc9937a95",
+ "nonDarkChannels": 115,
+ "pixelCount": 64
+ }
+ },
+ "final": {
+ "active": 0,
+ "created": 9,
+ "records": [
+ {
+ "id": 1,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 2,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 3,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 5,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 6,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 7,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 8,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 9,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 10,
+ "isDestroy": true,
+ "destroyed": 1
+ }
+ ],
+ "errors": [
+ {
+ "message": "vue-consumer-listener-failure",
+ "same": true
+ }
+ ],
+ "exposesArt": false,
+ "componentRef": false,
+ "players": 0,
+ "videos": 0
+ },
+ "workers": {
+ "active": 0,
+ "created": 19
+ }
+ },
+ {
+ "mode": "development",
+ "engine": "firefox",
+ "version": "155.0",
+ "checks": [
+ "mount-event-closed-component-ref-and-plugin-registration",
+ "mutate-option-retains-mount-only-contract",
+ "replace-option-retains-mount-only-contract",
+ "native-play-pause-seek-and-decoded-pixels",
+ "instance-api-updates-source-without-remount",
+ "key-change-reconstructs-with-current-props",
+ "sibling-cleanup-retains-other-instance",
+ "optional-listener-and-unmount",
+ "cancel-before-mount-does-not-allocate",
+ "three-mount-unmount-cycles-clean-once",
+ "vue-captured-listener-error-retains-instance-until-unmount",
+ "listener-triggered-unmount-cleans-created-instance",
+ "keepalive-retains-on-deactivation-and-destroys-on-owner-unmount",
+ "plain-js-event-prop-usage-and-remount",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true,
+ "media": {
+ "time": 0.168891,
+ "paused": true,
+ "seek": 1,
+ "width": 320,
+ "rgba": {
+ "sha256": "33fe3ff9f4bcc6d9701ba4d220fdf8f985a84942e95c8e6bd222b94eb5c39ad2",
+ "nonDarkChannels": 104,
+ "pixelCount": 64
+ }
+ },
+ "final": {
+ "active": 0,
+ "created": 9,
+ "records": [
+ {
+ "id": 1,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 2,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 3,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 5,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 6,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 7,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 8,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 9,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 10,
+ "isDestroy": true,
+ "destroyed": 1
+ }
+ ],
+ "errors": [
+ {
+ "message": "vue-consumer-listener-failure",
+ "same": true
+ }
+ ],
+ "exposesArt": false,
+ "componentRef": false,
+ "players": 0,
+ "videos": 0
+ },
+ "workers": {
+ "active": 0,
+ "created": 19
+ }
+ },
+ {
+ "mode": "development",
+ "engine": "webkit",
+ "version": "26.6",
+ "checks": [
+ "mount-event-closed-component-ref-and-plugin-registration",
+ "mutate-option-retains-mount-only-contract",
+ "replace-option-retains-mount-only-contract",
+ "native-play-pause-seek-and-decoded-pixels",
+ "instance-api-updates-source-without-remount",
+ "key-change-reconstructs-with-current-props",
+ "sibling-cleanup-retains-other-instance",
+ "optional-listener-and-unmount",
+ "cancel-before-mount-does-not-allocate",
+ "three-mount-unmount-cycles-clean-once",
+ "vue-captured-listener-error-retains-instance-until-unmount",
+ "listener-triggered-unmount-cleans-created-instance",
+ "keepalive-retains-on-deactivation-and-destroys-on-owner-unmount",
+ "plain-js-event-prop-usage-and-remount",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true,
+ "media": {
+ "time": 0.1590748,
+ "paused": true,
+ "seek": 1.0023504,
+ "width": 640,
+ "rgba": {
+ "sha256": "15b2369b1f3350810e8bbdce3097afeebf63037cba467171e9cce7f239e4e715",
+ "nonDarkChannels": 104,
+ "pixelCount": 64
+ }
+ },
+ "final": {
+ "active": 0,
+ "created": 9,
+ "records": [
+ {
+ "id": 1,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 2,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 3,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 5,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 6,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 7,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 8,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 9,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 10,
+ "isDestroy": true,
+ "destroyed": 1
+ }
+ ],
+ "errors": [
+ {
+ "message": "vue-consumer-listener-failure",
+ "same": true
+ }
+ ],
+ "exposesArt": false,
+ "componentRef": false,
+ "players": 0,
+ "videos": 0
+ },
+ "workers": {
+ "active": 0,
+ "created": 19
+ }
+ },
+ {
+ "mode": "production",
+ "engine": "chromium",
+ "version": "153.0.8010.12",
+ "checks": [
+ "mount-event-closed-component-ref-and-plugin-registration",
+ "mutate-option-retains-mount-only-contract",
+ "replace-option-retains-mount-only-contract",
+ "native-play-pause-seek-and-decoded-pixels",
+ "instance-api-updates-source-without-remount",
+ "key-change-reconstructs-with-current-props",
+ "sibling-cleanup-retains-other-instance",
+ "optional-listener-and-unmount",
+ "cancel-before-mount-does-not-allocate",
+ "three-mount-unmount-cycles-clean-once",
+ "vue-captured-listener-error-retains-instance-until-unmount",
+ "listener-triggered-unmount-cleans-created-instance",
+ "keepalive-retains-on-deactivation-and-destroys-on-owner-unmount",
+ "plain-js-event-prop-usage-and-remount",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true,
+ "media": {
+ "time": 0.158436,
+ "paused": true,
+ "seek": 1,
+ "width": 320,
+ "rgba": {
+ "sha256": "fc838a7f2b0d4c7cb6482718dfffe6f33cd05cc8d1a5efec70c56a2cc9937a95",
+ "nonDarkChannels": 115,
+ "pixelCount": 64
+ }
+ },
+ "final": {
+ "active": 0,
+ "created": 9,
+ "records": [
+ {
+ "id": 1,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 2,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 3,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 5,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 6,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 7,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 8,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 9,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 10,
+ "isDestroy": true,
+ "destroyed": 1
+ }
+ ],
+ "errors": [
+ {
+ "message": "vue-consumer-listener-failure",
+ "same": true
+ }
+ ],
+ "exposesArt": false,
+ "componentRef": false,
+ "players": 0,
+ "videos": 0
+ },
+ "workers": {
+ "active": 0,
+ "created": 19
+ }
+ },
+ {
+ "mode": "production",
+ "engine": "firefox",
+ "version": "155.0",
+ "checks": [
+ "mount-event-closed-component-ref-and-plugin-registration",
+ "mutate-option-retains-mount-only-contract",
+ "replace-option-retains-mount-only-contract",
+ "native-play-pause-seek-and-decoded-pixels",
+ "instance-api-updates-source-without-remount",
+ "key-change-reconstructs-with-current-props",
+ "sibling-cleanup-retains-other-instance",
+ "optional-listener-and-unmount",
+ "cancel-before-mount-does-not-allocate",
+ "three-mount-unmount-cycles-clean-once",
+ "vue-captured-listener-error-retains-instance-until-unmount",
+ "listener-triggered-unmount-cleans-created-instance",
+ "keepalive-retains-on-deactivation-and-destroys-on-owner-unmount",
+ "plain-js-event-prop-usage-and-remount",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true,
+ "media": {
+ "time": 0.169328,
+ "paused": true,
+ "seek": 1,
+ "width": 320,
+ "rgba": {
+ "sha256": "33fe3ff9f4bcc6d9701ba4d220fdf8f985a84942e95c8e6bd222b94eb5c39ad2",
+ "nonDarkChannels": 104,
+ "pixelCount": 64
+ }
+ },
+ "final": {
+ "active": 0,
+ "created": 9,
+ "records": [
+ {
+ "id": 1,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 2,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 3,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 5,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 6,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 7,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 8,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 9,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 10,
+ "isDestroy": true,
+ "destroyed": 1
+ }
+ ],
+ "errors": [
+ {
+ "message": "vue-consumer-listener-failure",
+ "same": true
+ }
+ ],
+ "exposesArt": false,
+ "componentRef": false,
+ "players": 0,
+ "videos": 0
+ },
+ "workers": {
+ "active": 0,
+ "created": 19
+ }
+ },
+ {
+ "mode": "production",
+ "engine": "webkit",
+ "version": "26.6",
+ "checks": [
+ "mount-event-closed-component-ref-and-plugin-registration",
+ "mutate-option-retains-mount-only-contract",
+ "replace-option-retains-mount-only-contract",
+ "native-play-pause-seek-and-decoded-pixels",
+ "instance-api-updates-source-without-remount",
+ "key-change-reconstructs-with-current-props",
+ "sibling-cleanup-retains-other-instance",
+ "optional-listener-and-unmount",
+ "cancel-before-mount-does-not-allocate",
+ "three-mount-unmount-cycles-clean-once",
+ "vue-captured-listener-error-retains-instance-until-unmount",
+ "listener-triggered-unmount-cleans-created-instance",
+ "keepalive-retains-on-deactivation-and-destroys-on-owner-unmount",
+ "plain-js-event-prop-usage-and-remount",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true,
+ "media": {
+ "time": 0.1655602,
+ "paused": true,
+ "seek": 1.0022635,
+ "width": 640,
+ "rgba": {
+ "sha256": "15b2369b1f3350810e8bbdce3097afeebf63037cba467171e9cce7f239e4e715",
+ "nonDarkChannels": 104,
+ "pixelCount": 64
+ }
+ },
+ "final": {
+ "active": 0,
+ "created": 9,
+ "records": [
+ {
+ "id": 1,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 2,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 3,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 5,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 6,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 7,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 8,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 9,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 10,
+ "isDestroy": true,
+ "destroyed": 1
+ }
+ ],
+ "errors": [
+ {
+ "message": "vue-consumer-listener-failure",
+ "same": true
+ }
+ ],
+ "exposesArt": false,
+ "componentRef": false,
+ "players": 0,
+ "videos": 0
+ },
+ "workers": {
+ "active": 0,
+ "created": 19
+ }
+ }
+ ],
+ "passed": true,
+ "media": {
+ "file": "test/browser/media/pattern.mp4",
+ "sha256": "0ad1d7ad286aec6e0fa9e27fc1e487e7277df2e603e30040fb942365763d0233"
+ },
+ "inputs": {
+ "src/Player.vue": "7da72a5ac73f25e26940043f8a27b5b7d18843dbd4e894c68b6e54ea310f7f1f",
+ "src/player-options.ts": "4ac59db434d53d90b8b3068249cdf9a1b71a4789966490e67aea186e8a9ce8a5",
+ "src/App.vue": "a3b5cc939a085b1a511d8fbed0a2b7afac8c90e98cbf1de548c28e3ab9dd63be",
+ "src/main.ts": "50cb9f18b1c7115799ae456f0d0038840278b49ea520b5549d468ec76c248fb9",
+ "index.html": "b5111a3898009c4b9d8c9789e64e5cb4e984520e4777c2ad243e4d4cebe8df82",
+ "Lifecycle.vue": "84b5586b8d47e56b89bdcecda305e0c71418ff3a390b48355f729724f938d5ce",
+ "entry.ts": "d06f34191f1ddd65042476daf086b0d473bf7d8cf7865186a05dedb001e624d2",
+ "LegacyConsumer.vue": "b943f75a302242a205c8a19241cebf6040530c6130293a2db4f4d58c35bc3822",
+ "legacy-entry.js": "182b9fe361f82072d928e2cee736372ba07fa70390c8ffc1be4548dd5e553ba2",
+ "consumer.ts": "b952b6926d478ed0847672597d2951cca016957c88d4e0e2431880f5741e9274"
+ },
+ "types": {
+ "vueTsc": "3.3.11",
+ "typescript": "5.9.3",
+ "vue": "3.5.28",
+ "files": 90,
+ "isolated": true,
+ "negativeCases": 3
+ }
+ },
+ "before": {
+ "task": "EX-02",
+ "before": true,
+ "node": "v24.21.0",
+ "source": "4df96893649ee0d3ffe1504af0609c3c6e023409",
+ "packages": [
+ {
+ "name": "artplayer",
+ "archive": "D:\\github\\ArtPlayer\\refactor\\.cache\\vue-consumer-HpwUKY\\artplayer.tgz",
+ "sha256": "0c0ea52d41575261b836aefac68280733b119c75834ecad6d4bc78edb073f4c1"
+ },
+ {
+ "name": "artplayer-plugin-danmuku",
+ "archive": "D:\\github\\ArtPlayer\\refactor\\.cache\\vue-consumer-HpwUKY\\artplayer-plugin-danmuku.tgz",
+ "sha256": "7d1f0d4e3cf4a5579aa8a0f8c850b6205bfdd7e70c46244db6ebc4f7a866a949"
+ },
+ {
+ "name": "artplayer-plugin-document-pip",
+ "archive": "D:\\github\\ArtPlayer\\refactor\\.cache\\vue-consumer-HpwUKY\\artplayer-plugin-document-pip.tgz",
+ "sha256": "4ac0fca9a5a36c1039a34f37caf67479691f153ffda02ed2bc3a96946914a8c2"
+ }
+ ],
+ "browsers": [
+ {
+ "mode": "development",
+ "engine": "chromium",
+ "version": "153.0.8010.12",
+ "checks": [
+ "mount-event-closed-component-ref-and-plugin-registration",
+ "mutate-option-retains-mount-only-contract",
+ "replace-option-retains-mount-only-contract",
+ "native-play-pause-seek-and-decoded-pixels",
+ "instance-api-updates-source-without-remount",
+ "key-change-reconstructs-with-current-props",
+ "sibling-cleanup-retains-other-instance",
+ "optional-listener-and-unmount",
+ "cancel-before-mount-does-not-allocate",
+ "three-mount-unmount-cycles-clean-once",
+ "vue-captured-listener-error-retains-instance-until-unmount",
+ "listener-triggered-unmount-cleans-created-instance",
+ "keepalive-retains-on-deactivation-and-destroys-on-owner-unmount",
+ "plain-js-event-prop-usage-and-remount",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true,
+ "media": {
+ "time": 0.153314,
+ "paused": true,
+ "seek": 1,
+ "width": 320,
+ "rgba": {
+ "sha256": "fc838a7f2b0d4c7cb6482718dfffe6f33cd05cc8d1a5efec70c56a2cc9937a95",
+ "nonDarkChannels": 115,
+ "pixelCount": 64
+ }
+ },
+ "final": {
+ "active": 0,
+ "created": 9,
+ "records": [
+ {
+ "id": 1,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 2,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 3,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 5,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 6,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 7,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 8,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 9,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 10,
+ "isDestroy": true,
+ "destroyed": 1
+ }
+ ],
+ "errors": [
+ {
+ "message": "vue-consumer-listener-failure",
+ "same": true
+ }
+ ],
+ "exposesArt": false,
+ "componentRef": false,
+ "players": 0,
+ "videos": 0
+ },
+ "workers": {
+ "active": 0,
+ "created": 19
+ }
+ },
+ {
+ "mode": "development",
+ "engine": "firefox",
+ "version": "155.0",
+ "checks": [
+ "mount-event-closed-component-ref-and-plugin-registration",
+ "mutate-option-retains-mount-only-contract",
+ "replace-option-retains-mount-only-contract",
+ "native-play-pause-seek-and-decoded-pixels",
+ "instance-api-updates-source-without-remount",
+ "key-change-reconstructs-with-current-props",
+ "sibling-cleanup-retains-other-instance",
+ "optional-listener-and-unmount",
+ "cancel-before-mount-does-not-allocate",
+ "three-mount-unmount-cycles-clean-once",
+ "vue-captured-listener-error-retains-instance-until-unmount",
+ "listener-triggered-unmount-cleans-created-instance",
+ "keepalive-retains-on-deactivation-and-destroys-on-owner-unmount",
+ "plain-js-event-prop-usage-and-remount",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true,
+ "media": {
+ "time": 0.166666,
+ "paused": true,
+ "seek": 1,
+ "width": 320,
+ "rgba": {
+ "sha256": "33fe3ff9f4bcc6d9701ba4d220fdf8f985a84942e95c8e6bd222b94eb5c39ad2",
+ "nonDarkChannels": 104,
+ "pixelCount": 64
+ }
+ },
+ "final": {
+ "active": 0,
+ "created": 9,
+ "records": [
+ {
+ "id": 1,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 2,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 3,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 5,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 6,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 7,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 8,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 9,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 10,
+ "isDestroy": true,
+ "destroyed": 1
+ }
+ ],
+ "errors": [
+ {
+ "message": "vue-consumer-listener-failure",
+ "same": true
+ }
+ ],
+ "exposesArt": false,
+ "componentRef": false,
+ "players": 0,
+ "videos": 0
+ },
+ "workers": {
+ "active": 0,
+ "created": 19
+ }
+ },
+ {
+ "mode": "development",
+ "engine": "webkit",
+ "version": "26.6",
+ "checks": [
+ "mount-event-closed-component-ref-and-plugin-registration",
+ "mutate-option-retains-mount-only-contract",
+ "replace-option-retains-mount-only-contract",
+ "native-play-pause-seek-and-decoded-pixels",
+ "instance-api-updates-source-without-remount",
+ "key-change-reconstructs-with-current-props",
+ "sibling-cleanup-retains-other-instance",
+ "optional-listener-and-unmount",
+ "cancel-before-mount-does-not-allocate",
+ "three-mount-unmount-cycles-clean-once",
+ "vue-captured-listener-error-retains-instance-until-unmount",
+ "listener-triggered-unmount-cleans-created-instance",
+ "keepalive-retains-on-deactivation-and-destroys-on-owner-unmount",
+ "plain-js-event-prop-usage-and-remount",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true,
+ "media": {
+ "time": 0.1596341,
+ "paused": true,
+ "seek": 1.0022472,
+ "width": 640,
+ "rgba": {
+ "sha256": "15b2369b1f3350810e8bbdce3097afeebf63037cba467171e9cce7f239e4e715",
+ "nonDarkChannels": 104,
+ "pixelCount": 64
+ }
+ },
+ "final": {
+ "active": 0,
+ "created": 9,
+ "records": [
+ {
+ "id": 1,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 2,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 3,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 5,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 6,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 7,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 8,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 9,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 10,
+ "isDestroy": true,
+ "destroyed": 1
+ }
+ ],
+ "errors": [
+ {
+ "message": "vue-consumer-listener-failure",
+ "same": true
+ }
+ ],
+ "exposesArt": false,
+ "componentRef": false,
+ "players": 0,
+ "videos": 0
+ },
+ "workers": {
+ "active": 0,
+ "created": 19
+ }
+ },
+ {
+ "mode": "production",
+ "engine": "chromium",
+ "version": "153.0.8010.12",
+ "checks": [
+ "mount-event-closed-component-ref-and-plugin-registration",
+ "mutate-option-retains-mount-only-contract",
+ "replace-option-retains-mount-only-contract",
+ "native-play-pause-seek-and-decoded-pixels",
+ "instance-api-updates-source-without-remount",
+ "key-change-reconstructs-with-current-props",
+ "sibling-cleanup-retains-other-instance",
+ "optional-listener-and-unmount",
+ "cancel-before-mount-does-not-allocate",
+ "three-mount-unmount-cycles-clean-once",
+ "vue-captured-listener-error-retains-instance-until-unmount",
+ "listener-triggered-unmount-cleans-created-instance",
+ "keepalive-retains-on-deactivation-and-destroys-on-owner-unmount",
+ "plain-js-event-prop-usage-and-remount",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true,
+ "media": {
+ "time": 0.155424,
+ "paused": true,
+ "seek": 1,
+ "width": 320,
+ "rgba": {
+ "sha256": "fc838a7f2b0d4c7cb6482718dfffe6f33cd05cc8d1a5efec70c56a2cc9937a95",
+ "nonDarkChannels": 115,
+ "pixelCount": 64
+ }
+ },
+ "final": {
+ "active": 0,
+ "created": 9,
+ "records": [
+ {
+ "id": 1,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 2,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 3,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 5,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 6,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 7,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 8,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 9,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 10,
+ "isDestroy": true,
+ "destroyed": 1
+ }
+ ],
+ "errors": [
+ {
+ "message": "vue-consumer-listener-failure",
+ "same": true
+ }
+ ],
+ "exposesArt": false,
+ "componentRef": false,
+ "players": 0,
+ "videos": 0
+ },
+ "workers": {
+ "active": 0,
+ "created": 19
+ }
+ },
+ {
+ "mode": "production",
+ "engine": "firefox",
+ "version": "155.0",
+ "checks": [
+ "mount-event-closed-component-ref-and-plugin-registration",
+ "mutate-option-retains-mount-only-contract",
+ "replace-option-retains-mount-only-contract",
+ "native-play-pause-seek-and-decoded-pixels",
+ "instance-api-updates-source-without-remount",
+ "key-change-reconstructs-with-current-props",
+ "sibling-cleanup-retains-other-instance",
+ "optional-listener-and-unmount",
+ "cancel-before-mount-does-not-allocate",
+ "three-mount-unmount-cycles-clean-once",
+ "vue-captured-listener-error-retains-instance-until-unmount",
+ "listener-triggered-unmount-cleans-created-instance",
+ "keepalive-retains-on-deactivation-and-destroys-on-owner-unmount",
+ "plain-js-event-prop-usage-and-remount",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true,
+ "media": {
+ "time": 0.166666,
+ "paused": true,
+ "seek": 1,
+ "width": 320,
+ "rgba": {
+ "sha256": "33fe3ff9f4bcc6d9701ba4d220fdf8f985a84942e95c8e6bd222b94eb5c39ad2",
+ "nonDarkChannels": 104,
+ "pixelCount": 64
+ }
+ },
+ "final": {
+ "active": 0,
+ "created": 9,
+ "records": [
+ {
+ "id": 1,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 2,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 3,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 5,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 6,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 7,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 8,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 9,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 10,
+ "isDestroy": true,
+ "destroyed": 1
+ }
+ ],
+ "errors": [
+ {
+ "message": "vue-consumer-listener-failure",
+ "same": true
+ }
+ ],
+ "exposesArt": false,
+ "componentRef": false,
+ "players": 0,
+ "videos": 0
+ },
+ "workers": {
+ "active": 0,
+ "created": 19
+ }
+ },
+ {
+ "mode": "production",
+ "engine": "webkit",
+ "version": "26.6",
+ "checks": [
+ "mount-event-closed-component-ref-and-plugin-registration",
+ "mutate-option-retains-mount-only-contract",
+ "replace-option-retains-mount-only-contract",
+ "native-play-pause-seek-and-decoded-pixels",
+ "instance-api-updates-source-without-remount",
+ "key-change-reconstructs-with-current-props",
+ "sibling-cleanup-retains-other-instance",
+ "optional-listener-and-unmount",
+ "cancel-before-mount-does-not-allocate",
+ "three-mount-unmount-cycles-clean-once",
+ "vue-captured-listener-error-retains-instance-until-unmount",
+ "listener-triggered-unmount-cleans-created-instance",
+ "keepalive-retains-on-deactivation-and-destroys-on-owner-unmount",
+ "plain-js-event-prop-usage-and-remount",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true,
+ "media": {
+ "time": 0.1591394,
+ "paused": true,
+ "seek": 1.0014247,
+ "width": 640,
+ "rgba": {
+ "sha256": "15b2369b1f3350810e8bbdce3097afeebf63037cba467171e9cce7f239e4e715",
+ "nonDarkChannels": 104,
+ "pixelCount": 64
+ }
+ },
+ "final": {
+ "active": 0,
+ "created": 9,
+ "records": [
+ {
+ "id": 1,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 2,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 3,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 5,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 6,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 7,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 8,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 9,
+ "isDestroy": true,
+ "destroyed": 1
+ },
+ {
+ "id": 10,
+ "isDestroy": true,
+ "destroyed": 1
+ }
+ ],
+ "errors": [
+ {
+ "message": "vue-consumer-listener-failure",
+ "same": true
+ }
+ ],
+ "exposesArt": false,
+ "componentRef": false,
+ "players": 0,
+ "videos": 0
+ },
+ "workers": {
+ "active": 0,
+ "created": 18
+ }
+ }
+ ],
+ "passed": true,
+ "beforeWrapper": "f2f7e34a9510fd21f541afbc71f1f2262a02dfb3dfc5f3ba9e59d32779c28afe",
+ "media": {
+ "file": "test/browser/media/pattern.mp4",
+ "sha256": "0ad1d7ad286aec6e0fa9e27fc1e487e7277df2e603e30040fb942365763d0233"
+ },
+ "inputs": {
+ "src/Player.vue": "f2f7e34a9510fd21f541afbc71f1f2262a02dfb3dfc5f3ba9e59d32779c28afe",
+ "src/player-options.ts": "4ac59db434d53d90b8b3068249cdf9a1b71a4789966490e67aea186e8a9ce8a5",
+ "src/App.vue": "a3b5cc939a085b1a511d8fbed0a2b7afac8c90e98cbf1de548c28e3ab9dd63be",
+ "src/main.ts": "50cb9f18b1c7115799ae456f0d0038840278b49ea520b5549d468ec76c248fb9",
+ "index.html": "b5111a3898009c4b9d8c9789e64e5cb4e984520e4777c2ad243e4d4cebe8df82",
+ "Lifecycle.vue": "84b5586b8d47e56b89bdcecda305e0c71418ff3a390b48355f729724f938d5ce",
+ "entry.ts": "d06f34191f1ddd65042476daf086b0d473bf7d8cf7865186a05dedb001e624d2",
+ "LegacyConsumer.vue": "b943f75a302242a205c8a19241cebf6040530c6130293a2db4f4d58c35bc3822",
+ "legacy-entry.js": "182b9fe361f82072d928e2cee736372ba07fa70390c8ffc1be4548dd5e553ba2",
+ "consumer.ts": "b952b6926d478ed0847672597d2951cca016957c88d4e0e2431880f5741e9274"
+ },
+ "types": {
+ "vueTsc": "3.3.11",
+ "typescript": "5.9.3",
+ "vue": "3.5.28",
+ "files": 90,
+ "isolated": true,
+ "negativeCases": 3
+ }
+ },
+ "reactRegression": {
+ "passed": true,
+ "source": "4df96893649ee0d3ffe1504af0609c3c6e023409",
+ "packages": [
+ {
+ "name": "artplayer",
+ "archive": "D:\\github\\ArtPlayer\\refactor\\.cache\\react-consumer-ZyMc7G\\artplayer.tgz",
+ "sha256": "0c0ea52d41575261b836aefac68280733b119c75834ecad6d4bc78edb073f4c1"
+ },
+ {
+ "name": "artplayer-plugin-danmuku",
+ "archive": "D:\\github\\ArtPlayer\\refactor\\.cache\\react-consumer-ZyMc7G\\artplayer-plugin-danmuku.tgz",
+ "sha256": "7d1f0d4e3cf4a5579aa8a0f8c850b6205bfdd7e70c46244db6ebc4f7a866a949"
+ },
+ {
+ "name": "artplayer-plugin-document-pip",
+ "archive": "D:\\github\\ArtPlayer\\refactor\\.cache\\react-consumer-ZyMc7G\\artplayer-plugin-document-pip.tgz",
+ "sha256": "4ac0fca9a5a36c1039a34f37caf67479691f153ffda02ed2bc3a96946914a8c2"
+ }
+ ],
+ "browsers": [
+ {
+ "mode": "development",
+ "engine": "chromium",
+ "version": "153.0.8010.12",
+ "checks": [
+ "strict-mode-and-plugin-registration",
+ "stable-props-retain-instance",
+ "callback-change-cleans-old-instance",
+ "option-change-cleans-old-instance",
+ "native-play-pause-seek-and-decoded-pixels",
+ "sibling-unmount-preserves-other-instance",
+ "optional-getInstance",
+ "unmount-cleans-recorded-instances-and-dom",
+ "remount-after-empty",
+ "callback-exception-cleanup-and-original-error",
+ "cleanup-error-preserves-primary-callback-error",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true
+ },
+ {
+ "mode": "development",
+ "engine": "firefox",
+ "version": "155.0",
+ "checks": [
+ "strict-mode-and-plugin-registration",
+ "stable-props-retain-instance",
+ "callback-change-cleans-old-instance",
+ "option-change-cleans-old-instance",
+ "native-play-pause-seek-and-decoded-pixels",
+ "sibling-unmount-preserves-other-instance",
+ "optional-getInstance",
+ "unmount-cleans-recorded-instances-and-dom",
+ "remount-after-empty",
+ "callback-exception-cleanup-and-original-error",
+ "cleanup-error-preserves-primary-callback-error",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true
+ },
+ {
+ "mode": "development",
+ "engine": "webkit",
+ "version": "26.6",
+ "checks": [
+ "strict-mode-and-plugin-registration",
+ "stable-props-retain-instance",
+ "callback-change-cleans-old-instance",
+ "option-change-cleans-old-instance",
+ "native-play-pause-seek-and-decoded-pixels",
+ "sibling-unmount-preserves-other-instance",
+ "optional-getInstance",
+ "unmount-cleans-recorded-instances-and-dom",
+ "remount-after-empty",
+ "callback-exception-cleanup-and-original-error",
+ "cleanup-error-preserves-primary-callback-error",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true
+ },
+ {
+ "mode": "production",
+ "engine": "chromium",
+ "version": "153.0.8010.12",
+ "checks": [
+ "strict-mode-and-plugin-registration",
+ "stable-props-retain-instance",
+ "callback-change-cleans-old-instance",
+ "option-change-cleans-old-instance",
+ "native-play-pause-seek-and-decoded-pixels",
+ "sibling-unmount-preserves-other-instance",
+ "optional-getInstance",
+ "unmount-cleans-recorded-instances-and-dom",
+ "remount-after-empty",
+ "callback-exception-cleanup-and-original-error",
+ "cleanup-error-preserves-primary-callback-error",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true
+ },
+ {
+ "mode": "production",
+ "engine": "firefox",
+ "version": "155.0",
+ "checks": [
+ "strict-mode-and-plugin-registration",
+ "stable-props-retain-instance",
+ "callback-change-cleans-old-instance",
+ "option-change-cleans-old-instance",
+ "native-play-pause-seek-and-decoded-pixels",
+ "sibling-unmount-preserves-other-instance",
+ "optional-getInstance",
+ "unmount-cleans-recorded-instances-and-dom",
+ "remount-after-empty",
+ "callback-exception-cleanup-and-original-error",
+ "cleanup-error-preserves-primary-callback-error",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true
+ },
+ {
+ "mode": "production",
+ "engine": "webkit",
+ "version": "26.6",
+ "checks": [
+ "strict-mode-and-plugin-registration",
+ "stable-props-retain-instance",
+ "callback-change-cleans-old-instance",
+ "option-change-cleans-old-instance",
+ "native-play-pause-seek-and-decoded-pixels",
+ "sibling-unmount-preserves-other-instance",
+ "optional-getInstance",
+ "unmount-cleans-recorded-instances-and-dom",
+ "remount-after-empty",
+ "callback-exception-cleanup-and-original-error",
+ "cleanup-error-preserves-primary-callback-error",
+ "original-app-entry-loads-media"
+ ],
+ "passed": true
+ }
+ ]
+ },
+ "validation": {
+ "rootProductionTsFiles": 406,
+ "rootLint": {
+ "errors": 0,
+ "warnings": 1
+ },
+ "ciTestsPassed": 50,
+ "frozenInstall": true,
+ "strictToolchain": true,
+ "exampleBuild": true,
+ "coreDeclarationDrift": false,
+ "workerCountNote": "Candidate profiles created 19 workers; the old production WebKit profile created 18, other old profiles 19. All were terminated. Creation totals are observations, not an API invariant.",
+ "historicalSourceTypes": {
+ "command": "node node_modules/vue-tsc/bin/vue-tsc.js -p refactor/.cache/vue-source-before/tsconfig.json --noEmit --pretty false",
+ "exit": 2,
+ "diagnostic": "refactor/.cache/vue-source-before/src/App.vue(23,12): error TS2322: Type '{ id?: string | undefined; container?: string | { align: string; addEventListener: { (type: K, listener: (this: HTMLDivElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | ... 1 more ... | undefined): void; (type: string, listener: EventListenerOrEventListenerObject, ...' is not assignable to type 'Partial'.\r\n Types of property 'container' are incompatible.\r\n Type 'string | { align: string; addEventListener: { (type: K, listener: (this: HTMLDivElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | ... 1 more ... | undefined): void; (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | ... 1 more ... | un...' is not assignable to type 'string | HTMLDivElement | undefined'.\r\n Type '{ align: string; addEventListener: { (type: K, listener: (this: HTMLDivElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | ... 1 more ... | undefined): void; (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | ... 1 more ... | undefined):...' is not assignable to type 'string | HTMLDivElement | undefined'.\r\n Type '{ align: string; addEventListener: { (type: K, listener: (this: HTMLDivElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | ... 1 more ... | undefined): void; (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | ... 1 more ... | undefined):...' is not assignable to type 'HTMLDivElement'.\r\n Types of property 'offsetParent' are incompatible.\r\n Type '{ readonly attributes: { [x: number]: { readonly localName: string; readonly name: string; readonly namespaceURI: string | null; readonly ownerDocument: { readonly URL: string; alinkColor: string; readonly all: { [x: number]: ...; readonly length: number; item: (nameOrIndex?: string | undefined) => Element | ... 1 m...' is not assignable to type 'Element | null'.\r\n Type '{ readonly attributes: { [x: number]: { readonly localName: string; readonly name: string; readonly namespaceURI: string | null; readonly ownerDocument: { readonly URL: string; alinkColor: string; readonly all: { [x: number]: ...; readonly length: number; item: (nameOrIndex?: string | undefined) => Element | ... 1 m...' is not assignable to type 'Element'.\r\n Types of property 'attributes' are incompatible.\r\n Type '{ [x: number]: { readonly localName: string; readonly name: string; readonly namespaceURI: string | null; readonly ownerDocument: { readonly URL: string; alinkColor: string; readonly all: { [x: number]: { readonly attributes: ...; ... 183 more ...; readonly assignedSlot: { ...; } | null; }; readonly length: number; ...' is not assignable to type 'NamedNodeMap'.\r\n 'number' index signatures are incompatible.\r\n Type '{ readonly localName: string; readonly name: string; readonly namespaceURI: string | null; readonly ownerDocument: { readonly URL: string; alinkColor: string; readonly all: { [x: number]: { readonly attributes: { [x: number]: ...; ... 8 more ...; [Symbol.iterator]: () => ArrayIterator<...>; }; ... 183 more ...; read...' is not assignable to type 'Attr'.\r\n The types of 'ownerDocument.anchors' are incompatible between these types.\r\n Type '{ [x: number]: { charset: string; coords: string; download: string; hreflang: string; name: string; ping: string; referrerPolicy: string; rel: string; relList: { [x: number]: string; readonly length: number; ... 13 more ...; [Symbol.iterator]: () => ArrayIterator<...>; }; ... 339 more ...; username: string; }; item:...' is not assignable to type 'HTMLCollectionOf'.\r\n 'number' index signatures are incompatible.\r\n Type '{ charset: string; coords: string; download: string; hreflang: string; name: string; ping: string; referrerPolicy: string; rel: string; relList: { [x: number]: string; readonly length: number; value: string; ... 12 more ...; [Symbol.iterator]: () => ArrayIterator<...>; }; ... 339 more ...; username: string; }' is not assignable to type 'HTMLAnchorElement'.\r\n Types of property 'shadowRoot' are incompatible.\r\n Type '{ readonly clonable: boolean; readonly delegatesFocus: boolean; readonly host: { readonly attributes: { [x: number]: { readonly localName: string; readonly name: string; readonly namespaceURI: string | null; ... 53 more ...; removeEventListener: (type: string, callback: EventListenerOrEventListenerObject | null, opt...' is not assignable to type 'ShadowRoot | null'.\r\n Type '{ readonly clonable: boolean; readonly delegatesFocus: boolean; readonly host: { readonly attributes: { [x: number]: { readonly localName: string; readonly name: string; readonly namespaceURI: string | null; readonly ownerDocument: { ...; }; ... 52 more ...; removeEventListener: (type: string, callback: EventListene...' is not assignable to type 'ShadowRoot'.\r\n Types of property 'childNodes' are incompatible.\r\n Type '{ [x: number]: { after: (...nodes: (string | Node)[]) => void; before: (...nodes: (string | Node)[]) => void; remove: () => void; replaceWith: (...nodes: (string | Node)[]) => void; readonly baseURI: string; ... 48 more ...; removeEventListener: (type: string, callback: EventListenerOrEventListenerObject | null, opt...' is not assignable to type 'NodeListOf'.\r\n 'number' index signatures are incompatible.\r\n Type '{ after: (...nodes: (string | Node)[]) => void; before: (...nodes: (string | Node)[]) => void; remove: () => void; replaceWith: (...nodes: (string | Node)[]) => void; readonly baseURI: string; ... 48 more ...; removeEventListener: (type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean ...' is not assignable to type 'ChildNode'.\r\n Types of property 'parentElement' are incompatible.\r\n Type '{ accessKey: string; readonly accessKeyLabel: string; autocapitalize: string; autocorrect: boolean; dir: string; draggable: boolean; hidden: boolean; inert: boolean; innerText: string; lang: string; ... 312 more ...; focus: (options?: FocusOptions | undefined) => void; } | null' is not assignable to type 'HTMLElement | null'.\r\n Type '{ accessKey: string; readonly accessKeyLabel: string; autocapitalize: string; autocorrect: boolean; dir: string; draggable: boolean; hidden: boolean; inert: boolean; innerText: string; lang: string; ... 312 more ...; focus: (options?: FocusOptions | undefined) => void; }' is not assignable to type 'HTMLElement'.\r\n Types of property 'assignedSlot' are incompatible.\r\n Type '{ name: string; assign: (...nodes: (Element | Text)[]) => void; assignedElements: (options?: AssignedNodesOptions | undefined) => Element[]; ... 323 more ...; focus: (options?: FocusOptions | undefined) => void; } | null' is not assignable to type 'HTMLSlotElement | null'.\r\n Type '{ name: string; assign: (...nodes: (Element | Text)[]) => void; assignedElements: (options?: AssignedNodesOptions | undefined) => Element[]; ... 323 more ...; focus: (options?: FocusOptions | undefined) => void; }' is not assignable to type 'HTMLSlotElement'.\r\n The types of 'style.parentRule' are incompatible between these types.\r\n Type '{ cssText: string; readonly parentRule: ... | null; readonly parentStyleSheet: { readonly cssRules: { [x: number]: ...; readonly length: number; item: (index: number) => CSSRule | null; [Symbol.iterator]: () => ArrayIterator<...>; }; ... 14 more ...; readonly type: string; } | null; ... 12 more ...; readonly FONT_FE...' is not assignable to type 'CSSRule | null'.\r\n Type '{ cssText: string; readonly parentRule: ... | null; readonly parentStyleSheet: { readonly cssRules: { [x: number]: ...; readonly length: number; item: (index: number) => CSSRule | null; [Symbol.iterator]: () => ArrayIterator<...>; }; ... 14 more ...; readonly type: string; } | null; ... 12 more ...; readonly FONT_FE...' is not assignable to type 'CSSRule'.\r\n Types of property 'parentStyleSheet' are incompatible.\r\n Type '{ readonly cssRules: { [x: number]: { cssText: string; readonly parentRule: ... | null; readonly parentStyleSheet: ... | null; readonly type: number; readonly STYLE_RULE: 1; readonly CHARSET_RULE: 2; ... 9 more ...; readonly FONT_FEATURE_VALUES_RULE: 14; }; readonly length: number; item: (index: number) => CSSRule |...' is not assignable to type 'CSSStyleSheet | null'.\r\n Type '{ readonly cssRules: { [x: number]: { cssText: string; readonly parentRule: ... | null; readonly parentStyleSheet: ... | null; readonly type: number; readonly STYLE_RULE: 1; readonly CHARSET_RULE: 2; ... 9 more ...; readonly FONT_FEATURE_VALUES_RULE: 14; }; readonly length: number; item: (index: number) => CSSRule |...' is not assignable to type 'CSSStyleSheet'.\r\n Types of property 'ownerNode' are incompatible.\r\n Type '{ readonly attributes: { [x: number]: { readonly localName: string; readonly name: string; readonly namespaceURI: string | null; readonly ownerDocument: { readonly URL: string; alinkColor: string; readonly all: { [x: number]: ...; readonly length: number; item: (nameOrIndex?: string | undefined) => Element | ... 1 m...' is not assignable to type 'Element | ProcessingInstruction | null'.\r\n Type '{ readonly attributes: { [x: number]: { readonly localName: string; readonly name: string; readonly namespaceURI: string | null; readonly ownerDocument: { readonly URL: string; alinkColor: string; readonly all: { [x: number]: ...; readonly length: number; item: (nameOrIndex?: string | undefined) => Element | ... 1 m...' is not assignable to type 'Element | ProcessingInstruction | null'.\r\n",
+ "note": "Old App and Player checked in the current workspace dependency tree; not a claim about all historical Vue environments."
+ }
+ },
+ "inAppBrowser": {
+ "name": "Codex In-app Browser",
+ "url": "http://127.0.0.1:5188/",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36",
+ "fullVersion": "Unavailable: Browser.getVersion is unsupported",
+ "source": "https://artplayer.org/assets/sample/video.mp4",
+ "mockedRequests": false,
+ "playing": {
+ "time": 74.60414,
+ "duration": 90.045011,
+ "paused": false,
+ "readyState": 4,
+ "width": 640,
+ "height": 360,
+ "error": null
+ },
+ "afterPauseAndHome": {
+ "time": 0,
+ "paused": true,
+ "readyState": 4,
+ "error": null
+ },
+ "capturedWarningsAndErrors": [],
+ "temporaryTabClosed": true,
+ "devServerStopped": true
+ },
+ "logs": {
+ "candidate": "refactor/.cache/vue-verified.log",
+ "before": "refactor/.cache/vue-before.log",
+ "react": "refactor/.cache/vue-react-regression.log",
+ "types": "refactor/.cache/vue-root-types.log",
+ "vueTypes": "refactor/.cache/vue-final-types.log",
+ "lint": "refactor/.cache/vue-root-lint.log",
+ "ci": "refactor/.cache/vue-ci-tests.log",
+ "frozen": "refactor/.cache/vue-frozen.log",
+ "toolchain": "refactor/.cache/vue-toolchain.log",
+ "declarations": "refactor/.cache/vue-declaration-drift.log",
+ "exampleBuild": "refactor/.cache/vue-example-build.log"
+ },
+ "limitations": [
+ "Historical wrapper source with current candidate packages is not every published core/plugin combination.",
+ "Automated media/XML are controlled; in-app development check separately used original public samples.",
+ "Worker termination counters do not establish process memory usage.",
+ "Real PiP windows, full Danmuku behavior, SSR/HMR and physical Safari are not covered.",
+ "Remote GitHub runs and publication are not verified or authorized by local success."
+ ]
+}
diff --git a/refactor/changes/2026-09-14-EX-02-vue-consumer.md b/refactor/changes/2026-09-14-EX-02-vue-consumer.md
new file mode 100644
index 000000000..4d645a5d3
--- /dev/null
+++ b/refactor/changes/2026-09-14-EX-02-vue-consumer.md
@@ -0,0 +1,105 @@
+# EX-02 Vue 消费者与原有更新/卸载契约
+
+起点 `4df96893649ee0d3ffe1504af0609c3c6e023409`。本任务完成 Vue 示例的自有
+TS 入口、配置职责拆分、严格 SFC 检查、实际 tarball 消费和维护说明;不将
+插件专项/设备/发布任务一起完成。
+
+## 实现与兼容边界
+
+- `Player.vue` 保留 `option: Partial`、`getInstance` 事件和
+ `onMounted` 初始化。将示例配置抽到 `player-options.ts`,维持法语、词典、
+ Danmuku/Document PiP 的覆盖顺序及每实例创建插件工厂。
+- DOM 容器和播放器均由 shallowRef 持有;去掉容器类型断言。卸载先取出并
+ 清空内部引用,再 `destroy(false)`,保留 Vue 对外层 div 的所有权。
+- 保留数组式 `defineEmits(['getInstance'])`,没有收窄旧 `$emit`/监听器的
+ 类型形状。内部交付的实例有准确 Artplayer 类型,App 也显式标注回调参数。
+- 保留仅挂载时读取 options:字段变化或替换对象不自动切源或重建;用户通过
+ 收到的实例 `switchUrl` 更新,通过组件 key 重建。没有增加深度 watch。
+- 保留 script-setup 的私有状态边界:组件 ref 有 Vue `$el/$props`,不公开 art。
+ class/style 保持透传;旧 JS 中的 id/data 属性透传单独实际验证。
+- Vue 捕获事件监听器错误后,仍挂载的组件保留播放器,到卸载时释放。此处
+ 没有照搬 React 的异常处理。KeepAlive 停用也不销毁/暂停,所有者卸载才清理。
+ 依据 [Vue 生命周期](https://vuejs.org/api/composition-api-lifecycle.html) 和
+ [script-setup 公开边界](https://vuejs.org/api/sfc-script-setup.html#defineexpose),
+ 再用旧/新包装的实际浏览器结果验证。
+
+`main.js` 迁移到 `main.ts` 并同步 HTML;`tsconfig.json` 替代 jsconfig,保留 @
+别名,启用 strict、strictTemplates、checkJs、skipLibCheck:false。App 对选项
+使用窄字段推导和 satisfies 校验,避免直接给 reactive 传入全部 Partial
+造成 DOM 字段类型展开。包内核心/插件公开声明、运行源码和产物均未修改。
+
+## 类型问题与测试修正
+
+原 App/Player 的固定 Git 内容在当前工作区、新 vue-tsc 下,App 第 23 行复现
+一个 TS2322。当前树可见 @vue/reactivity 3.5.13 与 Vue/runtime-dom 3.5.28 共存,
+诊断涉及 DOM 深层类型展开;不据此断言所有 Vue 环境均有该问题。新示例严格
+检查通过,隔离安装中的旧包装也通过,工作区类型问题与运行兼容分开记录。
+
+初版测试的两个假设已纠正:具体 Vue 组件实例不能随意赋给无 props/emits
+泛型的 ComponentPublicInstance;strictTemplates 不会自动把任意原生属性
+识别为组件声明的 props。保留真实 `$props`/私有 ref 正反例,任意属性透传在
+未改成 TS 的 JS SFC 中验证,没有扩大组件 props 或关闭严格模板检查。
+
+初版隔离校验拒绝了工作区 vue-tsc 引用的 language-core 模板 helper。最终连
+vue-tsc/TypeScript 也安装在隔离消费者中,全部 90 个编译输入都须位于该目录,
+没有给工作区类型增加例外。三个 @ts-expect-error 分别拒绝错误 URL、缺失
+option 和读取私有 art;原数组式 event 监听器形状仍可赋值。
+
+## 工具与所有权
+
+根 devDependencies 新增固定 Vue 3.5.28、@vitejs/plugin-vue 6.0.1、
+vue-tsc 3.3.11。Vue 版本与已固定 compiler-sfc 一致;vue-tsc 的实际 registry
+元数据要求 TS >=5.0,复用 TS 5.9.3、Vite 7.3.6。只更新根 yarn.lock,示例
+manifest 同步版本/用途,移除未使用的可选 devtools overlay 依赖。
+这些是示例/测试工具,不改变 Artplayer 包的生产依赖。
+
+新增 `dev:vue`、`typecheck:vue`、`lint:vue`、`build:vue`、`test:vue-consumer`。
+`test/vue/` 分离实际 SFC 场景、纯 JS 调用和只编译类型反例;编排脚本复用
+已有 package-consumer 的隔离目录/命令/安全清理,避免引入另一套包安装逻辑。
+
+消费者测试顺序:pack 三包并记录哈希;仓库外 offline 安装和 frozen force
+重装;逐文件对照 tarball;隔离 vue-tsc 严格编译;Vite 构建实际 App、生命周期
+页和 JS 页;三引擎逐个测试。浏览器、预览服务和临时安装均在 finally 释放。
+观察 Worker 的包装实际调用原生构造/terminate,没有替换 worker 执行逻辑。
+
+## 实际结果
+
+见 [机器证据](../baselines/vue-consumer-validation.json)。Windows、Node
+24.21.0、Yarn 1.22.22,Chromium 153.0.8010.12 / Firefox 155.0 / WebKit 26.6。
+
+- 新包装和固定旧 Player.vue:分别 development/production × 3 引擎,共
+ 12 组通过,每组 15 类检查,无 retry/skip。覆盖原事件/组件 ref、选项修改与
+ 替换、播放/暂停/seek/像素、实例切源、key 重建、兄弟实例、可选监听器、
+ mount 前取消、连续三次挂卸载、捕获错误、监听器触发卸载、KeepAlive、旧 JS
+ 用法、原 App 入口。候选每组创建 19 个 Worker,旧包装生产 WebKit 为 18 个、
+ 其余为 19 个;创建总数不是兼容断言,全部最终存活 0,实例/DOM 清理。
+- 新依赖下 React 实际消费六组回归通过;全仓 406 个生产 TS 文件及兼容类型
+ 矩阵通过;50 项 CI 回归通过;根/示例 lint、frozen 安装、严格工具链、核心
+ 声明无漂移检查和示例生产构建通过。根 lint 保留 1 条既有生成声明注释警告。
+- GitHub 三系统 browser-smoke 加入 Vue 消费及 always 证据上传;验证器会拒绝
+ 删除、跳过该步骤或遗漏其证据。这里只证明本地配置/测试,没有远端运行证据。
+
+## 内置浏览器补充验证
+
+真实启动 `yarn dev:vue --host 127.0.0.1 --port 5188 --strictPort`,Vite 7.3.6
+在 322ms 就绪。通过已授权 Codex 内置浏览器打开实际开发页,不拦截样例请求:
+公网 video.mp4、XML 弹幕正常加载;点击 Lire 后播放推进至 74.60414s,原生
+duration 90.045011s、640×360、readyState 4、error null。Pause 后操作进度条
+Home,UI 和原生 currentTime 回到 0,paused=true,查询到的 error/warn 为空。
+
+User-Agent 报告 Chrome/152.0.0.0(Windows);接口不支持 Browser.getVersion,
+不虚构完整补丁版本。该次开发页与 headless 自动化分开记账,没有据此声称 Chrome
+扩展已恢复或 Safari/真机通过。UI 截图已在工具会话查看,未作为仓库图片保存。
+临时页已关闭,开发服务器已主动终止。暂停 seek 后旧弹幕仍显示符合已记录的
+Danmuku 历史池语义,未将其误报为新缺陷,见该包 ARCHITECTURE.md 和原基线测试。
+
+## 边界与回退
+
+自动化媒体/XML 是受控夹具;内置浏览器只补充一次公网样例/开发页实播,不证明
+普遍 CDN 可用性。Worker 终止计数不等于进程内存测量。未验证真实 PiP 窗口、
+完整弹幕调度、SSR/HMR、macOS/iOS Safari、旧核心/旧插件全部组合;对应已有
+包/工程/发布门槛继续保留。`--before` 是历史包装源码对照,不是旧 npm 包矩阵。
+
+回退 EX-02 提交恢复旧示例、配置和工具锁;不涉及数据迁移。将来生产包内容或
+工具链变化后重新打包验证,不复用本次 tarball 哈希作 npm 发布证明。无版本
+升级、推送或发布;Vue 示例的私有 0.0.0 不属于 22 workspace 包的大版本发布。
diff --git a/refactor/ci-setup.md b/refactor/ci-setup.md
index d3fb4aaa3..4c96e30ca 100644
--- a/refactor/ci-setup.md
+++ b/refactor/ci-setup.md
@@ -9,6 +9,8 @@
| `yarn lint` | 只读 ESLint,覆盖包源码/声明、JS/MJS 工具、测试和编辑器声明 |
| `yarn lint:fix` | 显式自动修复相同范围 |
| `yarn typecheck` | 根/迁移包严格检查、当前与兼容 TS 消费;历史 NodeNext ESM 错误单独核对,见 typechecking.md |
+| `yarn typecheck:react` / `yarn typecheck:vue` | 原 React TSX / Vue SFC 示例严格检查,ci:check 同时执行对应 lint |
+| `yarn test:react-consumer` / `yarn test:vue-consumer` | 仓库外 tarball 安装、原框架示例、开发/生产三引擎;browser-smoke 执行并上传独立目录,本地证据不代替远端矩阵 |
| `yarn test:unit` | 原播放/DASH 回归、同夹具的新旧公共契约与 JS/TS loader 验证 |
| `yarn test:node` | test:unit 加工具链/文档构建回归,保留原 test:playback/test:dash-control 入口 |
| `yarn test` | 统一执行 Node 与基线测试,源码/夹具维护入口见仓库 test/README.md |
diff --git a/refactor/plan.md b/refactor/plan.md
index e3e79dce8..1074be7a7 100644
--- a/refactor/plan.md
+++ b/refactor/plan.md
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 238 项,范围 22 个包及工作区/示例。
-状态:todo 61 / doing 15 / blocked 0 / done 162 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
+状态:todo 60 / doing 15 / blocked 0 / done 163 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -387,7 +387,7 @@
| SITE-05 | artplayer-vitepress 构建文档站和验证链接/示例 | SITE-04, EX-01, EX-02 | VitePress 构建、链接与嵌入 demo 检查 | 文档构建、链接、嵌入路径与声明注入通过;真实完整 demo 保留 EX-03 独立门槛;核对 ENG-PM-01 登记的搜索 peer 范围和真实搜索行为 | M | todo |
| SITE-06 | artplayer-vitepress 文档站交付验收 | SITE-05 | 维护指南和站点变更记录 | 未手改 generated 目录,旧 URL 可用、部署与检查分离 | M | todo |
| EX-01 | example/react.js, workspace 验证 React 消费者与 TS | CORE-22, ENG-07 | React 挂载/卸载/重挂载、引用和插件样例 | 真实 tarball + TS 消费通过,保留既有 React 集成 API | M | done |
-| EX-02 | example/vue.js 验证 Vue 消费者与更新卸载 | CORE-22, ENG-07 | Vue 实例/ref、参数更新、卸载及插件样例 | 旧 JS 组件用法无需修改,重复挂载不泄漏 | M | todo |
+| EX-02 | example/vue.js, workspace 验证 Vue 消费者与更新卸载 | CORE-22, ENG-07 | Vue 实例/ref、参数更新、卸载及插件样例 | 旧 JS 组件用法无需修改,重复挂载不泄漏 | M | done |
| EX-03 | workspace 验证全部原生 demo 与外部播放集成 | EX-01, EX-02, PKG-CHAPTER-06, PKG-AMBILIGHT-06, PKG-AUDIO-06, PKG-AUTO-THUMB-06, PKG-VTT-THUMB-06, PKG-HLS-06, PKG-DASH-06, PKG-MULTI-SUB-06, PKG-JASSUB-06, PKG-MASK-06, PKG-ASR-06, PKG-ADS-06, PKG-VAST-06, PKG-CAST-06, PKG-DPIP-06, PKG-DANMUKU-09, PKG-CANVAS-06, PKG-MB-10, PKG-IFRAME-06, PKG-TOOL-THUMB-06 | 8082 全 demo、HLS/DASH/FLV/MPEGTS/WebTorrent 集成记录 | 旧 URL/参数/脚本加载保持;网络/SDK 限制明确,不静默跳过;覆盖编辑器重复 Run 和 ESM/i18n/mobile/iframe,记录实际候选脚本及状态隔离证据;依据 BASE-04 路径台账逐项消除 not-run,处理 BASE-DEMO-01 | H | todo |
## 7 工具链与性能
@@ -605,6 +605,7 @@
- PKG-TOOL-THUMB-03: [记录](changes/2026-09-13-PKG-TOOL-THUMB-03-input-checkpoint.md) [记录](baselines/thumbnail-input-checkpoint.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md) [记录](baselines/thumbnail-lifecycle-validation.json)
- PKG-TOOL-THUMB-04: [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-runtime-types.md) [记录](baselines/thumbnail-runtime-types-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-public-types.md) [记录](baselines/thumbnail-public-types-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-emitter.md) [记录](baselines/thumbnail-emitter-validation.json)
- EX-01: [记录](changes/2026-09-14-EX-01-react-consumer.md) [记录](baselines/react-consumer-validation.json) [记录](scripts/react-consumer.mjs)
+- EX-02: [记录](changes/2026-09-14-EX-02-vue-consumer.md) [记录](baselines/vue-consumer-validation.json) [记录](scripts/vue-consumer.mjs)
- 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)
diff --git a/refactor/progress.md b/refactor/progress.md
index 67795ec6f..b7cf53a0e 100644
--- a/refactor/progress.md
+++ b/refactor/progress.md
@@ -1,5 +1,21 @@
# 进度与证据
+## EX-02 Vue 消费者完成
+
+Vue 示例入口和配置职责完成 TS 整理;保留挂载时读取 option、原 getInstance
+事件/类型和私有 ref 边界。严格检查实际 SFC/模板,原 JS 调用单独验证。候选
+与旧包装分别开发/生产 × 三引擎,各 15 类检查共 12 组通过,实际 Worker、
+实例和 DOM 均清理。新依赖下 React 六组回归、全仓 406 个生产 TS 文件、50 项
+CI 回归、lint(0 error/1 既有 warning)、frozen 安装、工具链、声明漂移和
+Vue 构建检查通过。内置浏览器另验证真实开发页公网样例播放/暂停/Home,UA
+报告 Chrome/152.0.0.0,补丁版本未提供;临时页和服务已关闭。见
+[变更](changes/2026-09-14-EX-02-vue-consumer.md)及
+[证据](baselines/vue-consumer-validation.json)。未关闭 PiP/Danmuku 等专项和
+真实设备/发布门槛;远端 CI 未验证,无推送/发布。当前 238 项:163 done、
+15 doing、60 todo。下一步推进 SITE-01 文档站入口/资产核对,VAST 兼容选择和
+Auto Thumbnail 首帧仍未关闭。
+
+
## ENG-LINT-01 全仓 lint 恢复
在 EX-01 独立提交 e0d785662 并通过提交审计后,只调整 Mask manifest 顶层字段
diff --git a/refactor/scripts/ci-workflow.mjs b/refactor/scripts/ci-workflow.mjs
index 0a2e7a922..bd71adcfd 100644
--- a/refactor/scripts/ci-workflow.mjs
+++ b/refactor/scripts/ci-workflow.mjs
@@ -87,6 +87,9 @@ export function validateCIWorkflow(source) {
const reactIndex = browser.steps.findIndex(step => step.run === 'yarn test:react-consumer 2>&1 | tee refactor/.cache/ci/react-consumer.log')
assert(reactIndex > consumerIndex && !Object.hasOwn(browser.steps[reactIndex], 'if'), 'Run React installed consumers after restoring canonical Node')
assert(browser.steps.some(step => step.uses?.startsWith('actions/upload-artifact@') && step.if === 'always()' && step.with.path.split('\n').includes('refactor/.cache/react-consumer-*/')), 'Retain React consumer failure evidence')
+ const vueIndex = browser.steps.findIndex(step => step.run === 'yarn test:vue-consumer 2>&1 | tee refactor/.cache/ci/vue-consumer.log')
+ assert(vueIndex > consumerIndex && !Object.hasOwn(browser.steps[vueIndex], 'if'), 'Run Vue installed consumers after restoring canonical Node')
+ assert(browser.steps.some(step => step.uses?.startsWith('actions/upload-artifact@') && step.if === 'always()' && step.with.path.split('\n').includes('refactor/.cache/vue-consumer-*/')), 'Retain Vue consumer failure evidence')
const pages = workflow.jobs.checks.steps.find(step => step.uses?.startsWith('actions/upload-pages-artifact@'))
assert.equal(pages?.if, 'inputs.pages-artifact && github.ref == \'refs/heads/master\' && matrix.os == \'ubuntu-latest\'', 'Only one trusted matrix leg can prepare Pages')
return { jobs: requiredJobs, systems, summary: summary.name }
diff --git a/refactor/scripts/ci-workflow.test.mjs b/refactor/scripts/ci-workflow.test.mjs
index a9a9ec8d7..dffbd130b 100644
--- a/refactor/scripts/ci-workflow.test.mjs
+++ b/refactor/scripts/ci-workflow.test.mjs
@@ -37,6 +37,9 @@ for (const [name, mutate] of [
['skipped React consumer', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:react-consumer')).if = 'false'],
['removed React consumer', w => w.jobs['browser-smoke'].steps = w.jobs['browser-smoke'].steps.filter(s => !s.run?.startsWith('yarn test:react-consumer'))],
['missing React evidence', w => w.jobs['browser-smoke'].steps.find(s => s.uses?.startsWith('actions/upload-artifact@')).with.path = 'refactor/.cache/ci/'],
+ ['skipped Vue consumer', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:vue-consumer')).if = 'false'],
+ ['removed Vue consumer', w => w.jobs['browser-smoke'].steps = w.jobs['browser-smoke'].steps.filter(s => !s.run?.startsWith('yarn test:vue-consumer'))],
+ ['missing Vue evidence', w => w.jobs['browser-smoke'].steps.find(s => s.uses?.startsWith('actions/upload-artifact@')).with.path = w.jobs['browser-smoke'].steps.find(s => s.uses?.startsWith('actions/upload-artifact@')).with.path.replace('refactor/.cache/vue-consumer-*/', '')],
['Pages uploaded by both matrix legs', w => w.jobs.checks.steps.find(s => s.uses?.startsWith('actions/upload-pages-artifact@')).if = 'inputs.pages-artifact && github.ref == \'refs/heads/master\''],
]) {
test(`CI rejects ${name}`, () => {
diff --git a/refactor/scripts/vue-consumer.mjs b/refactor/scripts/vue-consumer.mjs
new file mode 100644
index 000000000..10b8fcba7
--- /dev/null
+++ b/refactor/scripts/vue-consumer.mjs
@@ -0,0 +1,254 @@
+import assert from 'node:assert/strict'
+import { execFileSync } from 'node:child_process'
+import fs from 'node:fs'
+import path from 'node:path'
+import process from 'node:process'
+import { chromium, firefox, webkit } from '@playwright/test'
+import vue from '@vitejs/plugin-vue'
+import { build, preview } from 'vite'
+import { packedFiles } from '../../scripts/package-check.mjs'
+import { consumerDirectory, removeConsumer, run, workspace, writeJson } from '../../scripts/package-consumer.mjs'
+import { hash, readMember } from './releases.mjs'
+
+assert.equal(process.version.slice(1), fs.readFileSync(path.join(workspace, '.node-version'), 'utf8').trim())
+assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22')
+assert(process.argv.slice(2).every(arg => arg === '--before'))
+const yarn = process.env.npm_execpath
+assert(yarn && fs.existsSync(yarn))
+const before = process.argv.includes('--before')
+const output = fs.mkdtempSync(path.join(workspace, 'refactor/.cache/vue-consumer-'))
+const consumer = consumerDirectory()
+const report = { task: 'EX-02', before, node: process.version, source: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: workspace, encoding: 'utf8' }).trim(), packages: [], browsers: [], passed: false }
+console.log(`Vue consumer evidence: ${output}`)
+try {
+ const dependencies = {}
+ for (const name of ['artplayer', 'artplayer-plugin-danmuku', 'artplayer-plugin-document-pip']) {
+ const archive = path.join(output, `${name}.tgz`)
+ fs.writeFileSync(path.join(output, `${name}-pack.log`), run([yarn, 'pack', '--filename', archive], path.join(workspace, 'packages', name)))
+ dependencies[name] = `file:${archive.replaceAll('\\', '/')}`
+ report.packages.push({ name, archive, sha256: hash(fs.readFileSync(archive)) })
+ }
+ const manifest = JSON.parse(fs.readFileSync(path.join(workspace, 'package.json'), 'utf8'))
+ dependencies.vue = manifest.devDependencies.vue
+ const devDependencies = { 'vue-tsc': manifest.devDependencies['vue-tsc'], 'typescript': manifest.devDependencies.typescript }
+ writeJson(path.join(consumer, 'package.json'), { name: 'vue-artplayer-isolated-consumer', private: true, type: 'module', dependencies, devDependencies })
+ fs.writeFileSync(path.join(output, 'install.log'), run([yarn, 'install', '--offline', '--ignore-scripts', '--non-interactive'], consumer))
+ const lock = fs.readFileSync(path.join(consumer, 'yarn.lock'))
+ fs.writeFileSync(path.join(output, 'consumer-yarn.lock'), lock)
+ fs.writeFileSync(path.join(output, 'frozen.log'), run([yarn, 'install', '--offline', '--frozen-lockfile', '--force', '--ignore-scripts', '--non-interactive'], consumer))
+ assert.deepEqual(fs.readFileSync(path.join(consumer, 'yarn.lock')), lock)
+ for (const pkg of report.packages) {
+ for (const member of packedFiles(pkg.archive)) assert.deepEqual(fs.readFileSync(path.join(consumer, 'node_modules', pkg.name, member.replace(/^package\//, ''))), readMember(pkg.archive, member))
+ }
+ const example = path.join(workspace, 'example/vue.js')
+ fs.cpSync(path.join(example, 'src'), path.join(consumer, 'src'), { recursive: true })
+ for (const file of ['index.html', 'tsconfig.json']) fs.copyFileSync(path.join(example, file), path.join(consumer, file))
+ if (before) {
+ const old = execFileSync('git', ['show', '4df96893649ee0d3ffe1504af0609c3c6e023409:example/vue.js/src/Player.vue'], { cwd: workspace })
+ fs.writeFileSync(path.join(consumer, 'src/Player.vue'), old)
+ report.beforeWrapper = hash(old)
+ }
+ for (const file of ['Lifecycle.vue', 'entry.ts', 'LegacyConsumer.vue', 'legacy-entry.js', 'consumer.ts']) fs.copyFileSync(path.join(workspace, 'test/vue', file), path.join(consumer, file))
+ for (const [file, entry] of [['harness.html', 'entry.ts'], ['legacy.html', 'legacy-entry.js']]) fs.writeFileSync(path.join(consumer, file), `
`)
+ fs.mkdirSync(path.join(consumer, 'public'))
+ const media = fs.readFileSync(path.join(workspace, 'test/browser/media/pattern.mp4'))
+ fs.writeFileSync(path.join(consumer, 'public/pattern.mp4'), media)
+ fs.copyFileSync(path.join(example, 'public/favicon.ico'), path.join(consumer, 'public/favicon.ico'))
+ report.media = { file: 'test/browser/media/pattern.mp4', sha256: hash(media) }
+ report.inputs = Object.fromEntries(['src/Player.vue', 'src/player-options.ts', 'src/App.vue', 'src/main.ts', 'index.html', 'Lifecycle.vue', 'entry.ts', 'LegacyConsumer.vue', 'legacy-entry.js', 'consumer.ts'].map(file => [file, hash(fs.readFileSync(path.join(consumer, file)))]))
+ const config = JSON.parse(fs.readFileSync(path.join(consumer, 'tsconfig.json'), 'utf8'))
+ config.include = ['src', 'Lifecycle.vue', 'entry.ts', 'consumer.ts']
+ writeJson(path.join(consumer, 'tsconfig.json'), config)
+ const typeLog = run([path.join(consumer, 'node_modules/vue-tsc/bin/vue-tsc.js'), '-p', 'tsconfig.json', '--noEmit', '--listFiles', '--pretty', 'false'], consumer)
+ fs.writeFileSync(path.join(output, 'types.log'), typeLog)
+ const files = typeLog.trim().split(/\r?\n/)
+ for (const file of files) {
+ const actual = fs.realpathSync(file)
+ assert(actual.startsWith(fs.realpathSync(consumer) + path.sep), `Types escaped consumer: ${actual}`)
+ }
+ report.types = { vueTsc: manifest.devDependencies['vue-tsc'], typescript: manifest.devDependencies.typescript, vue: dependencies.vue, files: files.length, isolated: true, negativeCases: 3 }
+ for (const mode of ['development', 'production']) {
+ const outDir = path.join(output, mode)
+ await build({ root: consumer, configFile: false, plugins: [vue({ isProduction: mode === 'production' })], mode, define: { 'process.env.NODE_ENV': JSON.stringify(mode) }, build: { outDir, emptyOutDir: false, rollupOptions: { input: { app: path.join(consumer, 'index.html'), harness: path.join(consumer, 'harness.html'), legacy: path.join(consumer, 'legacy.html') } } } })
+ const server = await preview({ root: consumer, configFile: false, build: { outDir }, preview: { host: '127.0.0.1', port: 0, open: false } })
+ const base = `http://127.0.0.1:${server.httpServer.address().port}`
+ try {
+ for (const [engine, type] of Object.entries({ chromium, firefox, webkit })) {
+ const browser = await type.launch()
+ const row = { mode, engine, version: browser.version(), checks: [], passed: false }
+ report.browsers.push(row)
+ const page = await browser.newPage()
+ await page.addInitScript(() => {
+ const active = new Set()
+ let created = 0
+ const NativeWorker = window.Worker
+ window.Worker = class extends NativeWorker {
+ constructor(url, options) {
+ super(url, options)
+ active.add(this)
+ created++
+ }
+
+ terminate() {
+ active.delete(this)
+ return super.terminate()
+ }
+ }
+ window.workerProbe = () => ({ active: active.size, created })
+ })
+ const unexpected = []
+ page.on('pageerror', error => unexpected.push(error.message))
+ await page.route('https://artplayer.org/assets/sample/danmuku.xml', route => route.fulfill({ contentType: 'text/xml', body: 'Vue lifecycle sample ' }))
+ await page.route('https://artplayer.org/assets/sample/video.mp4', route => route.fulfill({ contentType: 'video/mp4', body: media }))
+ const snapshot = () => page.evaluate(() => window.vueProbe.snapshot())
+ const act = action => page.evaluate(action => window.vueProbe.act(action), action)
+ const wait = async (count) => {
+ await page.waitForFunction(count => window.vueProbe?.snapshot().active === count, count)
+ return snapshot()
+ }
+ try {
+ await page.goto(`${base}/harness.html`)
+ let state = await wait(1)
+ assert.equal(state.created, 1)
+ assert.equal(state.componentRef, true)
+ assert.equal(state.exposesArt, false)
+ assert.equal(await page.locator('.primary-player').count(), 1)
+ await page.waitForFunction(() => window.vueProbe.current().plugins.artplayerPluginDanmuku && window.vueProbe.current().plugins.artplayerPluginDocumentPip)
+ row.checks.push('mount-event-closed-component-ref-and-plugin-registration')
+ const id = await page.evaluate(() => window.vueProbe.current().id)
+ for (const mode of ['mutate', 'replace']) {
+ await act(mode)
+ assert.equal((await snapshot()).created, 1)
+ assert.equal(await page.evaluate(() => window.vueProbe.current().id), id)
+ assert.equal(await page.evaluate(() => window.vueProbe.current().option.url), '/pattern.mp4')
+ row.checks.push(`${mode}-option-retains-mount-only-contract`)
+ }
+ await page.waitForFunction(() => window.vueProbe.current().duration > 1)
+ await page.evaluate(() => window.vueProbe.current().play())
+ await page.waitForFunction(() => window.vueProbe.current().currentTime > 0.15)
+ row.media = await page.evaluate(async () => {
+ const art = window.vueProbe.current()
+ art.pause()
+ const time = art.currentTime
+ const seeked = new Promise(resolve => art.video.addEventListener('seeked', resolve, { once: true }))
+ art.currentTime = 1
+ await seeked
+ const canvas = document.createElement('canvas')
+ canvas.width = 8
+ canvas.height = 8
+ const context = canvas.getContext('2d')
+ context.drawImage(art.video, 0, 0, 8, 8)
+ return { time, paused: art.playing === false, seek: art.currentTime, width: art.video.videoWidth, rgba: Array.from(context.getImageData(0, 0, 8, 8).data) }
+ })
+ assert(row.media.time > 0.15 && row.media.paused && Math.abs(row.media.seek - 1) < 0.1 && row.media.width > 0)
+ assert(row.media.rgba.some((value, index) => index % 4 !== 3 && value > 50))
+ row.checks.push('native-play-pause-seek-and-decoded-pixels')
+ await page.evaluate(() => window.vueProbe.current().switchUrl('/pattern.mp4?api'))
+ assert.equal(await page.evaluate(() => window.vueProbe.current().id), id)
+ assert((await page.evaluate(() => window.vueProbe.current().url)).includes('?api'))
+ row.checks.push('instance-api-updates-source-without-remount')
+ await act('key')
+ state = await wait(1)
+ assert.equal(state.created, 2)
+ assert.equal(state.records[0].destroyed, 1)
+ assert.equal(await page.evaluate(() => window.vueProbe.current().option.url), '/pattern.mp4?replacement')
+ row.checks.push('key-change-reconstructs-with-current-props')
+ const retainedId = await page.evaluate(() => window.vueProbe.current().id)
+ await act('siblings')
+ await wait(2)
+ await act('remove-sibling')
+ await wait(1)
+ assert.equal(await page.evaluate(() => window.vueProbe.current().id), retainedId)
+ row.checks.push('sibling-cleanup-retains-other-instance')
+ await act('bare')
+ await wait(1)
+ await act('remove-bare')
+ state = await wait(0)
+ assert.equal(state.players, 0)
+ row.checks.push('optional-listener-and-unmount')
+ const prior = state.created
+ await act('cancel')
+ assert.equal((await snapshot()).created, prior)
+ row.checks.push('cancel-before-mount-does-not-allocate')
+ for (let index = 0; index < 3; index++) {
+ await act('show')
+ await wait(1)
+ await act('hide')
+ state = await wait(0)
+ assert.equal(state.players, 0)
+ assert(state.records.every(record => record.isDestroy && record.destroyed === 1))
+ assert.equal((await page.evaluate(() => window.workerProbe())).active, 0)
+ }
+ row.checks.push('three-mount-unmount-cycles-clean-once')
+ await act('throw')
+ state = await wait(1)
+ assert.equal(state.errors.length, 1)
+ assert.equal(state.errors[0].same, true)
+ await act('hide')
+ await wait(0)
+ row.checks.push('vue-captured-listener-error-retains-instance-until-unmount')
+ await act('unmount')
+ await wait(0)
+ await act('normal')
+ row.checks.push('listener-triggered-unmount-cleans-created-instance')
+ await act('cache')
+ await wait(1)
+ const cachedId = await page.evaluate(() => window.vueProbe.current().id)
+ await act('deactivate')
+ state = await wait(1)
+ assert.equal(state.players, 0)
+ await act('activate')
+ assert.equal(await page.evaluate(() => window.vueProbe.current().id), cachedId)
+ await page.evaluate(() => window.unmountVue())
+ state = await wait(0)
+ assert.equal(state.videos, 0)
+ assert(state.records.every(record => record.isDestroy && record.destroyed === 1))
+ row.final = state
+ row.workers = await page.evaluate(() => window.workerProbe())
+ assert(row.workers.created > 0)
+ assert.equal(row.workers.active, 0)
+ row.checks.push('keepalive-retains-on-deactivation-and-destroys-on-owner-unmount')
+ assert.deepEqual(unexpected, [])
+ await page.goto(`${base}/legacy.html`)
+ await page.waitForFunction(() => Boolean(document.body.dataset.instance))
+ assert.equal(await page.locator('#legacy-player').getAttribute('data-consumer'), 'preserved')
+ await page.locator('#toggle').click()
+ await page.waitForFunction(() => document.body.dataset.destroyed === 'yes')
+ assert.equal(await page.locator('.art-video-player').count(), 0)
+ await page.locator('#toggle').click()
+ await page.locator('.art-video-player').waitFor()
+ row.checks.push('plain-js-event-prop-usage-and-remount')
+ await page.goto(base)
+ await page.locator('.art-video-player').waitFor()
+ await page.waitForFunction(() => document.querySelector('video')?.readyState >= 2)
+ row.checks.push('original-app-entry-loads-media')
+ assert.deepEqual(unexpected, [])
+ row.passed = true
+ }
+ catch (error) {
+ row.error = error.stack
+ await page.screenshot({ path: path.join(output, `${mode}-${engine}-failure.png`) })
+ throw error
+ }
+ finally {
+ await browser.close()
+ writeJson(path.join(output, 'report.json'), report)
+ }
+ }
+ }
+ finally {
+ await new Promise(resolve => server.httpServer.close(resolve))
+ }
+ }
+ report.passed = true
+}
+catch (error) {
+ report.error = error.stack
+ fs.writeFileSync(path.join(output, 'failure.log'), `${error.stack}\n${error.stdout || ''}\n${error.stderr || ''}`)
+ throw error
+}
+finally {
+ writeJson(path.join(output, 'report.json'), report)
+ removeConsumer(consumer)
+}
+console.log('Vue installed consumer types and six native browser profiles passed.')
diff --git a/refactor/tasks.json b/refactor/tasks.json
index 587dd1773..645d09b33 100644
--- a/refactor/tasks.json
+++ b/refactor/tasks.json
@@ -4228,17 +4228,22 @@
"phase": "6 文档与消费者",
"title": "验证 Vue 消费者与更新卸载",
"scope": [
- "example/vue.js"
+ "example/vue.js",
+ "workspace"
],
"dependsOn": [
"CORE-22",
"ENG-07"
],
- "status": "todo",
+ "status": "done",
"risk": "M",
"deliverable": "Vue 实例/ref、参数更新、卸载及插件样例",
"acceptance": "旧 JS 组件用法无需修改,重复挂载不泄漏",
- "evidence": []
+ "evidence": [
+ "changes/2026-09-14-EX-02-vue-consumer.md",
+ "baselines/vue-consumer-validation.json",
+ "scripts/vue-consumer.mjs"
+ ]
},
{
"id": "EX-03",
diff --git a/test/README.md b/test/README.md
index 13c59636e..6fe18a65a 100644
--- a/test/README.md
+++ b/test/README.md
@@ -2,6 +2,15 @@
Use the pinned Node/Yarn toolchain from `../refactor/toolchain-setup.md`.
+`yarn test:vue-consumer` checks the actual Vue example with packed core/Danmuku/
+Document PiP in an outside-workspace install. It also installs its pinned Vue
+compiler there, checks all declaration paths for isolation, and runs development
+and production builds in three engines. `test/vue/` contains strict SFC/type
+fixtures and a separate plain-JS consumer. Reports under
+`refactor/.cache/vue-consumer-*` cover updates, remount, KeepAlive, captured errors,
+native media and real Worker cleanup. `--before` substitutes the recorded old
+wrapper and must preserve the same lifecycle behavior; it is not expected to fail.
+
`yarn test:react-consumer` installs packed core/Danmuku/Document PiP outside the
workspace and checks the actual React example's strict TSX, development and
production builds, and native lifecycle/playback in three engines. Its fixture
diff --git a/test/vue/LegacyConsumer.vue b/test/vue/LegacyConsumer.vue
new file mode 100644
index 000000000..bc3b70b0c
--- /dev/null
+++ b/test/vue/LegacyConsumer.vue
@@ -0,0 +1,20 @@
+
+
+
+
+ Toggle player
+
+
+
diff --git a/test/vue/Lifecycle.vue b/test/vue/Lifecycle.vue
new file mode 100644
index 000000000..217ce016c
--- /dev/null
+++ b/test/vue/Lifecycle.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/test/vue/consumer.ts b/test/vue/consumer.ts
new file mode 100644
index 000000000..ba6af7865
--- /dev/null
+++ b/test/vue/consumer.ts
@@ -0,0 +1,18 @@
+import type Artplayer from 'artplayer'
+import type { Option } from 'artplayer'
+import type Player from './src/Player.vue'
+
+type Props = InstanceType['$props']
+const option: Partial = { url: '/pattern.mp4' }
+export const valid: Props = { option, onGetInstance: (art: Artplayer) => art.pause() }
+export const optional: Props = { option: {} }
+// Preserve the historical array-form emits type; do not silently narrow its listener shape.
+export const oldListener: Props = { option, onGetInstance: (...args: unknown[]) => args.length }
+// @ts-expect-error Option fields retain the original public types.
+export const wrongOption: Props = { option: { url: 123 } }
+// @ts-expect-error The wrapper requires an option object.
+export const missingOption: Props = {}
+declare const component: InstanceType
+export const publicProps: Props = component.$props
+// @ts-expect-error Script setup does not expose its private player ref.
+export const privateRef = component.art
diff --git a/test/vue/entry.ts b/test/vue/entry.ts
new file mode 100644
index 000000000..e277c8606
--- /dev/null
+++ b/test/vue/entry.ts
@@ -0,0 +1,6 @@
+import { createApp } from 'vue'
+import Lifecycle from './Lifecycle.vue'
+
+const app = createApp(Lifecycle)
+window.unmountVue = () => app.unmount()
+app.mount('#root')
diff --git a/test/vue/legacy-entry.js b/test/vue/legacy-entry.js
new file mode 100644
index 000000000..0afe8178a
--- /dev/null
+++ b/test/vue/legacy-entry.js
@@ -0,0 +1,4 @@
+import { createApp } from 'vue'
+import LegacyConsumer from './LegacyConsumer.vue'
+
+createApp(LegacyConsumer).mount('#root')
diff --git a/yarn.lock b/yarn.lock
index c96830be4..d8e32fb5f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1549,6 +1549,11 @@
dependencies:
playwright "1.63.0"
+"@rolldown/pluginutils@1.0.0-beta.29":
+ version "1.0.0-beta.29"
+ resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz#f8fc9a8788757dccba0d3b7fee93183621773d4c"
+ integrity sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==
+
"@rolldown/pluginutils@1.0.0-beta.30":
version "1.0.0-beta.30"
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.30.tgz#ac55a2215c78a54ba67276524ac3dc8c195316d0"
@@ -2163,6 +2168,13 @@
"@types/babel__core" "^7.20.5"
react-refresh "^0.17.0"
+"@vitejs/plugin-vue@6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz#4c7f559621af104a22255c6ace5626e6d8349689"
+ integrity sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==
+ dependencies:
+ "@rolldown/pluginutils" "1.0.0-beta.29"
+
"@vitejs/plugin-vue@^5.2.1":
version "5.2.4"
resolved "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz"
@@ -2176,6 +2188,27 @@
"@typescript-eslint/scope-manager" "^8.55.0"
"@typescript-eslint/utils" "^8.55.0"
+"@volar/language-core@2.4.28":
+ version "2.4.28"
+ resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.28.tgz#c21f365a91c1dffe8bd7264fd491770c8d74fef3"
+ integrity sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==
+ dependencies:
+ "@volar/source-map" "2.4.28"
+
+"@volar/source-map@2.4.28":
+ version "2.4.28"
+ resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.4.28.tgz#b40254e8c96199e5f1e0796777c593c617ad270e"
+ integrity sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==
+
+"@volar/typescript@2.4.28":
+ version "2.4.28"
+ resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.4.28.tgz#83f86356e84eb101b8081a44c104f2f2ced8411f"
+ integrity sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==
+ dependencies:
+ "@volar/language-core" "2.4.28"
+ path-browserify "^1.0.1"
+ vscode-uri "^3.0.8"
+
"@vue/compiler-core@3.5.13":
version "3.5.13"
resolved "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz"
@@ -2198,6 +2231,17 @@
estree-walker "^2.0.2"
source-map-js "^1.2.1"
+"@vue/compiler-core@3.5.42":
+ version "3.5.42"
+ resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.42.tgz#9d159ca5b1e7d792a42e9b1ddf2fc31edc9333dc"
+ integrity sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==
+ dependencies:
+ "@babel/parser" "^7.29.8"
+ "@vue/shared" "3.5.42"
+ entities "^7.0.1"
+ estree-walker "^2.0.2"
+ source-map-js "^1.2.1"
+
"@vue/compiler-dom@3.5.13":
version "3.5.13"
resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz"
@@ -2214,6 +2258,14 @@
"@vue/compiler-core" "3.5.28"
"@vue/shared" "3.5.28"
+"@vue/compiler-dom@^3.5.0":
+ version "3.5.42"
+ resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz#2ac9570010bbffb7c08d8a904a956ecbabcdf6c6"
+ integrity sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==
+ dependencies:
+ "@vue/compiler-core" "3.5.42"
+ "@vue/shared" "3.5.42"
+
"@vue/compiler-sfc@3.5.13":
version "3.5.13"
resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz"
@@ -2287,6 +2339,19 @@
dependencies:
rfdc "^1.4.1"
+"@vue/language-core@3.3.11":
+ version "3.3.11"
+ resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-3.3.11.tgz#e5b6b5ad1fa1a37e032cf7668458cda7a5566b37"
+ integrity sha512-QJmpliwAVpC/OxubIByPAhNzsQPRc8/gxlN2qnVzVfIMjMDz/9RnXRFoetjz5yEgXVXyp4LqhXq3V53PjmNzFw==
+ dependencies:
+ "@volar/language-core" "2.4.28"
+ "@vue/compiler-dom" "^3.5.0"
+ "@vue/shared" "^3.5.0"
+ alien-signals "^3.2.1"
+ muggle-string "^0.4.1"
+ path-browserify "^1.0.1"
+ picomatch "^4.0.4"
+
"@vue/reactivity@3.5.13":
version "3.5.13"
resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz"
@@ -2363,6 +2428,11 @@
resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz"
integrity sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==
+"@vue/shared@3.5.42", "@vue/shared@^3.5.0":
+ version "3.5.42"
+ resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.42.tgz#a413cf21262502662354cda3b65ab31bca979202"
+ integrity sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==
+
"@vueuse/core@12.8.2", "@vueuse/core@^12.4.0":
version "12.8.2"
resolved "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz"
@@ -2490,6 +2560,11 @@ algoliasearch@^5.14.2:
"@algolia/requester-fetch" "5.48.0"
"@algolia/requester-node-http" "5.48.0"
+alien-signals@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/alien-signals/-/alien-signals-3.2.1.tgz#eb66256949bce90b7d30d055e2752e62d6930c7c"
+ integrity sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==
+
ansi-colors@^4.1.1:
version "4.1.3"
resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz"
@@ -6259,6 +6334,11 @@ ms@^2.1.3:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+muggle-string@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/muggle-string/-/muggle-string-0.4.1.tgz#3b366bd43b32f809dc20659534dd30e7c8a0d328"
+ integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==
+
multimatch@5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz"
@@ -6883,6 +6963,11 @@ parse-url@^8.1.0:
dependencies:
parse-path "^7.0.0"
+path-browserify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
+ integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
+
path-exists@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz"
@@ -6951,7 +7036,7 @@ picomatch@^2.3.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601"
integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==
-picomatch@^4.0.2, picomatch@^4.0.3:
+picomatch@^4.0.2, picomatch@^4.0.3, picomatch@^4.0.4:
version "4.0.7"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.7.tgz#6313360034ccb36b3dc61ecbdff78121f90fe21f"
integrity sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==
@@ -8344,6 +8429,11 @@ vitepress@1.6.3:
vite "^5.4.14"
vue "^3.5.13"
+vscode-uri@^3.0.8:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.2.0.tgz#1cb1fd3cee7426b66732afc3fb86343d596d5fb4"
+ integrity sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==
+
vue-eslint-parser@^10.2.0:
version "10.2.0"
resolved "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.2.0.tgz"
@@ -8356,6 +8446,14 @@ vue-eslint-parser@^10.2.0:
esquery "^1.6.0"
semver "^7.6.3"
+vue-tsc@3.3.11:
+ version "3.3.11"
+ resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-3.3.11.tgz#26e7df7b6c1d66793d68caaa4ab96caa28002d9c"
+ integrity sha512-gOb0B9rtU2+f1dszwPqSH5kAieIF9ReeLhD3kSRNHv5WZZUQz/JdVXW0RTdqhNTMlQkqKzrTTviqKr/4FYZraQ==
+ dependencies:
+ "@volar/typescript" "2.4.28"
+ "@vue/language-core" "3.3.11"
+
vue@3.5.13:
version "3.5.13"
resolved "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz"
@@ -8367,7 +8465,7 @@ vue@3.5.13:
"@vue/server-renderer" "3.5.13"
"@vue/shared" "3.5.13"
-vue@^3.5.13:
+vue@3.5.28, vue@^3.5.13:
version "3.5.28"
resolved "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz"
integrity sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==