fix(react): [EX-01] verify installed consumers and clean up callback failures

This commit is contained in:
Harvey Zhao committed 2026-09-14 05:17:51 +08:00
1 parent 3e39fc745f
commit e0d785662d
25 files changed
+3539 -98

No files matched your search

+55 -7
View File
@@ -1,12 +1,60 @@
# React + Vite
# React consumer example
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Run from the repository root with Node from .node-version and Yarn Classic
1.22.22. Install once with yarn install --frozen-lockfile; keep only the root
yarn.lock in version control.
Currently, two official plugins are available:
```sh
yarn dev:react
yarn typecheck:react
yarn lint:react
yarn build:react
yarn test:react-consumer
```
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
build:react writes to refactor/.cache/react-example-dist. It uses built
workspace package entrypoints. Rebuild a changed library with `yarn build <package>` before testing its integration. The consumer test packs the current
core, Danmuku and Document PiP distributions, installs them outside the workspace,
verifies their bytes, typechecks and builds the copied example, and exercises it
in Chromium, Firefox and WebKit. No package aliases to source files are used.
Reports and the consumer lock are in unique refactor/.cache/react-consumer-* folders.
## Expanding the ESLint configuration
## Component contract and ownership
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
- src/Player.tsx owns one Artplayer instance per effect. Existing props remain
option: `Partial<Option>`, optional getInstance(art), and native div attributes.
Stable option/callback identities retain the instance. Changing either destroys
the old instance before creating one.
- getInstance runs after construction, not media readiness. Subscribe to player
events for readiness. React StrictMode may call it again during development
setup/cleanup. Callback errors destroy the instance before propagating the
original error to React, including when cleanup also throws.
- Cleanup uses destroy(false): React owns the outer div. Retained player references
expose isDestroy; the callback does not receive a new null argument on unmount.
- src/player-options.ts retains this example's existing overrides: French language,
French/Indonesian dictionaries, and new Danmuku/Document PiP factories per instance.
These still override option.lang, i18n and plugins. Other options pass through.
React supplies the container. The url assertion preserves the historical Partial
input; the core validates it. Missing URLs are not promised to construct successfully.
- src/App.tsx supplies stable options and a callback. src/main.tsx mounts StrictMode;
index.html loads the TSX entry. tsconfig.json checks the actual TSX and declarations
with strict checks and skipLibCheck: false.
- Root development dependencies pin React/React DOM 19.1.1, their types
19.1.10/19.1.7, Vite 7.3.6, React Vite plugin 5.0.0 and TS 5.9.3.
These are example/test tooling, not Artplayer runtime dependencies.
## Test scope
Installed development and production builds cover StrictMode, stable/changed
props, sibling instances, optional callbacks, unmount/remount, callback exceptions,
native play/pause/seek, decoded canvas pixels, plugin registration and the App entry.
Video and XML responses are controlled fixtures; CDN availability is not asserted.
Window PiP interaction, full Danmuku scheduling, macOS/iOS Safari and device
capabilities remain in package/release acceptance tasks.
yarn test:react-consumer --before substitutes recorded pre-fix Player source
against the same installed candidate packages. It must fail at callback exception
cleanup. This is a wrapper-source regression control, not an old npm wrapper test:
this example is not a published wrapper package.
Maintenance details and frozen evidence belong to EX-01 in refactor/.
+2 -29
View File
@@ -1,29 +1,2 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])
// Reuse the repository's TypeScript-aware configuration for this TSX example.
export { default } from '../../eslint.config.js'
+1 -1
View File
@@ -8,6 +8,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+14 -15
View File
@@ -1,30 +1,29 @@
{
"name": "artplayer-react",
"private": true,
"version": "0.0.0",
"type": "module",
"version": "0.0.0",
"private": true,
"packageManager": "yarn@1.22.22",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "yarn typecheck && vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"artplayer": "^5.3.0-beta.4",
"artplayer-plugin-danmuku": "^5.2.0",
"artplayer-plugin-document-pip": "^1.0.2",
"react": "^19.1.1",
"react-dom": "^19.1.1"
"react": "19.1.1",
"react-dom": "19.1.1"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"vite": "^7.1.2"
"@types/react": "19.1.10",
"@types/react-dom": "19.1.7",
"@vitejs/plugin-react": "5.0.0",
"eslint": "9.39.2",
"typescript": "5.9.3",
"vite": "7.3.6"
}
}
+3 -1
View File
@@ -1,7 +1,9 @@
import Artplayer, { type Option } from 'artplayer'
import type Artplayer from 'artplayer'
import type { Option } from 'artplayer'
import Player from './Player'
function getInstance(art: Artplayer) {
// eslint-disable-next-line no-console
console.log(art)
}
+23 -27
View File
@@ -1,16 +1,11 @@
import type { Option } from 'artplayer'
import type { HTMLAttributes } from 'react'
import Artplayer from 'artplayer'
import { useEffect, useRef } from 'react'
import Artplayer, { type Option } from 'artplayer'
import { playerOptions } from './player-options'
// Test i18n
import fr from 'artplayer/i18n/fr'
import id from 'artplayer/i18n/id'
// Test plugins
import artplayerPluginDanmuku from 'artplayer-plugin-danmuku'
import artplayerPluginDocumentPip from 'artplayer-plugin-document-pip'
interface PlayerProps extends React.HTMLAttributes<HTMLDivElement> {
option: Partial<Option>,
export interface PlayerProps extends HTMLAttributes<HTMLDivElement> {
option: Partial<Option>
getInstance?: (art: Artplayer) => void
}
@@ -18,24 +13,25 @@ export default function Player({ option, getInstance, ...rest }: PlayerProps) {
const containerRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
if (!containerRef.current) return
if (!containerRef.current)
return
const art = new Artplayer({
...option,
container: containerRef.current,
url: option.url as string,
i18n: { id, fr },
lang: 'fr',
plugins: [
artplayerPluginDocumentPip({}),
artplayerPluginDanmuku({
danmuku: 'https://artplayer.org/assets/sample/danmuku.xml',
}),
],
})
const art = new Artplayer(playerOptions(option, containerRef.current))
if (typeof getInstance === 'function') {
getInstance(art)
try {
if (typeof getInstance === 'function') {
getInstance(art)
}
}
catch (error) {
try {
art.destroy(false)
}
finally {
// Keep the consumer's error even if teardown also fails.
// eslint-disable-next-line no-unsafe-finally
throw error
}
}
return () => {
+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',
}),
],
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"jsx": "react-jsx",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["react", "react-dom"],
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"skipLibCheck": false
},
"include": ["src"]
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
// https://vite.dev/config/
export default defineConfig({