feat(vue): [EX-02] verify installed consumers and preserve lifecycle contracts

This commit is contained in:
Harvey Zhao committed 2026-09-14 06:02:13 +08:00
1 parent 4df9689364
commit d62ab13a35
29 files changed
+2326 -82

No files matched your search

+3
View File
@@ -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
+68 -21
View File
@@ -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<Option>`.
- 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<Option>` 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.
+2
View File
@@ -0,0 +1,2 @@
// Reuse the shared Vue and TypeScript rules.
export { default } from '../../eslint.config.js'
+5 -5
View File
@@ -1,13 +1,13 @@
<!DOCTYPE html>
<!doctype html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>artplayer-vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
-8
View File
@@ -1,8 +0,0 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}
+11 -6
View File
@@ -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"
}
}
+5 -3
View File
@@ -1,12 +1,13 @@
<script setup lang="ts">
import type Artplayer from 'artplayer'
import type { Option } from 'artplayer'
import { reactive } from 'vue'
import Artplayer, { type Option } from 'artplayer'
import Player from './Player.vue'
const option = reactive<Partial<Option>>({
const option = reactive({
url: 'https://artplayer.org/assets/sample/video.mp4',
autoSize: true,
})
} satisfies Partial<Option>)
const style = reactive({
width: '800px',
@@ -15,6 +16,7 @@ const style = reactive({
})
function getInstance(art: Artplayer) {
// eslint-disable-next-line no-console
console.log('Artplayer instance:', art)
}
</script>
+12 -28
View File
@@ -1,44 +1,28 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'
import type { Option } from 'artplayer'
import Artplayer from 'artplayer'
// Test type
import Artplayer, { type Option } from 'artplayer'
// Test i18n
import fr from 'artplayer/i18n/fr'
import id from 'artplayer/i18n/id'
// Test plugins
import artplayerPluginDocumentPip from 'artplayer-plugin-document-pip'
import artplayerPluginDanmuku from 'artplayer-plugin-danmuku'
import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
import { playerOptions } from './player-options'
const props = defineProps<{ option: Partial<Option> }>()
const emit = defineEmits(['getInstance'])
const art = shallowRef<Artplayer | null>(null)
const $container = ref<HTMLDivElement | null>(null)
const $container = shallowRef<HTMLDivElement | null>(null)
onMounted(() => {
art.value = new Artplayer({
...props.option,
url: props.option.url as string,
container: $container.value as HTMLDivElement,
i18n: { id, fr },
lang: 'fr',
plugins: [
artplayerPluginDocumentPip({
//
}),
artplayerPluginDanmuku({
danmuku: 'https://artplayer.org/assets/sample/danmuku.xml',
}),
],
})
const container = $container.value
if (!container)
return
art.value = new Artplayer(playerOptions(props.option, container))
emit('getInstance', art.value)
})
onBeforeUnmount(() => {
art.value?.destroy(false)
const instance = art.value
art.value = null
instance?.destroy(false)
})
</script>
File renamed without changes.
+23
View File
@@ -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<Option>, container: HTMLDivElement): Option {
return {
...option,
container,
// Partial<Option> 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',
}),
],
}
}
+18
View File
@@ -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"]
}
-2
View File
@@ -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: {
+10 -2
View File
@@ -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": {
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,105 @@
# EX-02 Vue 消费者与原有更新/卸载契约
起点 `4df96893649ee0d3ffe1504af0609c3c6e023409`。本任务完成 Vue 示例的自有
TS 入口、配置职责拆分、严格 SFC 检查、实际 tarball 消费和维护说明;不将
插件专项/设备/发布任务一起完成。
## 实现与兼容边界
- `Player.vue` 保留 `option: Partial<Option>`、`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<Option>
造成 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 包的大版本发布。
+2
View File
@@ -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 |
+3 -2
View File
@@ -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<br>构建文档站和验证链接/示例 | SITE-04, EX-01, EX-02 | VitePress 构建、链接与嵌入 demo 检查 | 文档构建、链接、嵌入路径与声明注入通过;真实完整 demo 保留 EX-03 独立门槛;核对 ENG-PM-01 登记的搜索 peer 范围和真实搜索行为 | M | todo |
| SITE-06 | artplayer-vitepress<br>文档站交付验收 | SITE-05 | 维护指南和站点变更记录 | 未手改 generated 目录,旧 URL 可用、部署与检查分离 | M | todo |
| EX-01 | example/react.js, workspace<br>验证 React 消费者与 TS | CORE-22, ENG-07 | React 挂载/卸载/重挂载、引用和插件样例 | 真实 tarball + TS 消费通过,保留既有 React 集成 API | M | done |
| EX-02 | example/vue.js<br>验证 Vue 消费者与更新卸载 | CORE-22, ENG-07 | Vue 实例/ref、参数更新、卸载及插件样例 | 旧 JS 组件用法无需修改,重复挂载不泄漏 | M | todo |
| EX-02 | example/vue.js, workspace<br>验证 Vue 消费者与更新卸载 | CORE-22, ENG-07 | Vue 实例/ref、参数更新、卸载及插件样例 | 旧 JS 组件用法无需修改,重复挂载不泄漏 | M | done |
| EX-03 | workspace<br>验证全部原生 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)
+16
View File
@@ -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 顶层字段
+3
View File
@@ -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 }
+3
View File
@@ -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}`, () => {
+254
View File
@@ -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), `<!doctype html><html><body><div id="root"></div><script type="module" src="/${entry}"></script></body></html>`)
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: '<i><d p="0,1,25,16777215,0,0,0,0">Vue lifecycle sample</d></i>' }))
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.')
+8 -3
View File
@@ -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",
+9
View File
@@ -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
+20
View File
@@ -0,0 +1,20 @@
<script setup>
import { ref } from 'vue'
import Player from './src/Player.vue'
const shown = ref(true)
const option = { url: '/pattern.mp4', muted: true }
const playerStyle = { width: '640px', height: '360px' }
// Plain JavaScript keeps the existing event and prop syntax.
function getInstance(art) {
document.body.dataset.instance = String(art.id)
art.on('destroy', () => document.body.dataset.destroyed = 'yes')
}
</script>
<template>
<button id="toggle" @click="shown = !shown">
Toggle player
</button>
<Player v-if="shown" id="legacy-player" data-consumer="preserved" :option="option" :style="playerStyle" @get-instance="getInstance" />
</template>
+99
View File
@@ -0,0 +1,99 @@
<script setup lang="ts">
import type { Option } from 'artplayer'
import Artplayer from 'artplayer'
import { nextTick, onErrorCaptured, reactive, ref, shallowRef } from 'vue'
import Player from './src/Player.vue'
const shown = ref(true)
const sibling = ref(false)
const bare = ref(false)
const cached = ref(false)
const key = ref(0)
const player = shallowRef<InstanceType<typeof Player> | null>(null)
const option = shallowRef<Partial<Option>>(reactive({ url: '/pattern.mp4', muted: true }))
const records: { art: Artplayer, destroyed: number }[] = []
const errors: { message: string, same: boolean }[] = []
const failure = new Error('vue-consumer-listener-failure')
let callbackMode = 'normal'
function capture(art: Artplayer) {
const record = { art, destroyed: 0 }
records.push(record)
art.on('destroy', () => record.destroyed++)
if (callbackMode === 'throw')
throw failure
if (callbackMode === 'unmount')
shown.value = false
}
onErrorCaptured((error) => {
errors.push({ message: error.message, same: error === failure })
return false
})
const probe = {
async act(action: string) {
if (action === 'mutate')
option.value.url = '/pattern.mp4?mutation'
if (action === 'replace')
option.value = { url: '/pattern.mp4?replacement', muted: true }
if (action === 'key')
key.value++
if (action === 'siblings')
sibling.value = true
if (action === 'remove-sibling')
sibling.value = false
if (action === 'hide')
shown.value = false
if (action === 'show')
shown.value = true
if (action === 'bare') {
shown.value = false
bare.value = true
}
if (action === 'remove-bare')
bare.value = false
if (action === 'cache') {
shown.value = false
cached.value = true
}
if (action === 'deactivate')
cached.value = false
if (action === 'activate')
cached.value = true
if (action === 'throw' || action === 'unmount') {
callbackMode = action
key.value++
shown.value = true
}
if (action === 'normal')
callbackMode = 'normal'
if (action === 'cancel') {
shown.value = true
shown.value = false
}
await nextTick()
},
current: () => Artplayer.instances.find(art => art.template.$container.classList.contains('primary-player')) || Artplayer.instances[0],
snapshot: () => ({
active: Artplayer.instances.length,
created: records.length,
records: records.map(({ art, destroyed }) => ({ id: art.id, isDestroy: art.isDestroy, destroyed })),
errors,
exposesArt: Boolean(player.value && 'art' in player.value),
componentRef: Boolean(player.value?.$el),
players: document.querySelectorAll('.art-video-player').length,
videos: document.querySelectorAll('video').length,
}),
}
declare global {
interface Window { vueProbe: typeof probe, unmountVue: () => void }
}
window.vueProbe = probe
</script>
<template>
<Player v-if="shown" :key="key" ref="player" class="primary-player" :style="{ width: '640px', height: '360px' }" :option="option" @get-instance="capture" />
<Player v-if="sibling" :style="{ width: '320px', height: '180px' }" :option="option" @get-instance="capture" />
<Player v-if="bare" :style="{ width: '320px', height: '180px' }" :option="option" />
<KeepAlive>
<Player v-if="cached" :style="{ width: '320px', height: '180px' }" :option="option" @get-instance="capture" />
</KeepAlive>
</template>
+18
View File
@@ -0,0 +1,18 @@
import type Artplayer from 'artplayer'
import type { Option } from 'artplayer'
import type Player from './src/Player.vue'
type Props = InstanceType<typeof Player>['$props']
const option: Partial<Option> = { 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<typeof Player>
export const publicProps: Props = component.$props
// @ts-expect-error Script setup does not expose its private player ref.
export const privateRef = component.art
+6
View File
@@ -0,0 +1,6 @@
import { createApp } from 'vue'
import Lifecycle from './Lifecycle.vue'
const app = createApp(Lifecycle)
window.unmountVue = () => app.unmount()
app.mount('#root')
+4
View File
@@ -0,0 +1,4 @@
import { createApp } from 'vue'
import LegacyConsumer from './LegacyConsumer.vue'
createApp(LegacyConsumer).mount('#root')
+100 -2
View File
@@ -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==