refactor(core): CORE-08 type plugin registration and guard closed instances

This commit is contained in:
Harvey Zhao committed 2026-09-11 02:09:36 +08:00
1 parent f9707bad71
commit 874dac9f22
36 files changed
+2865 -142

No files matched your search

@@ -0,0 +1,22 @@
import type { BuiltinRegistry, PluginHost } from './types'
import { isClosing } from '../lifecycle/instance'
import { isMobile } from '../utils/compatibility'
import autoOrientation from './autoOrientation'
import autoPlayback from './autoPlayback'
import fastForward from './fastForward'
import lock from './lock'
import miniProgressBar from './miniProgressBar'
export default function installBuiltins<Host extends PluginHost<Host>>(registry: BuiltinRegistry<Host>, option: PluginHost<Host>['option']): void {
// Read each condition at its original turn; earlier plugins may change options.
if (!isClosing(registry.art) && option.miniProgressBar && !option.isLive)
registry.add(miniProgressBar)
if (!isClosing(registry.art) && option.lock && isMobile)
registry.add(lock)
if (!isClosing(registry.art) && option.autoPlayback && !option.isLive)
registry.add(autoPlayback)
if (!isClosing(registry.art) && option.autoOrientation && isMobile)
registry.add(autoOrientation)
if (!isClosing(registry.art) && option.fastForward && isMobile && !option.isLive)
registry.add(fastForward)
}
-59
View File
@@ -1,59 +0,0 @@
import { def, errorHandle, has, isMobile } from '../utils'
import autoOrientation from './autoOrientation'
import autoPlayback from './autoPlayback'
import fastForward from './fastForward'
import lock from './lock'
import miniProgressBar from './miniProgressBar'
export default class Plugins {
constructor(art) {
this.art = art
this.id = 0
const { option } = art
if (option.miniProgressBar && !option.isLive) {
this.add(miniProgressBar)
}
if (option.lock && isMobile) {
this.add(lock)
}
if (option.autoPlayback && !option.isLive) {
this.add(autoPlayback)
}
if (option.autoOrientation && isMobile) {
this.add(autoOrientation)
}
if (option.fastForward && isMobile && !option.isLive) {
this.add(fastForward)
}
for (let index = 0; index < option.plugins.length; index++) {
this.add(option.plugins[index])
}
}
add(plugin) {
this.id += 1
const result = plugin.call(this.art, this.art)
if (result instanceof Promise) {
return result.then(res => this.next(plugin, res))
}
else {
return this.next(plugin, result)
}
}
next(plugin, result) {
const pluginName = (result && result.name) || plugin.name || `plugin${this.id}`
errorHandle(!has(this, pluginName), `Cannot add a plugin that already has the same name: ${pluginName}`)
def(this, pluginName, {
value: result,
})
return this
}
}
+40
View File
@@ -0,0 +1,40 @@
import type { PluginFactory, PluginHost, PluginRegistration } from './types'
import { isClosing } from '../lifecycle/instance'
import { errorHandle } from '../utils/error'
import installBuiltins from './builtins'
import registerPlugin from './registration'
export default class Plugins<Host extends PluginHost<Host>> {
declare art: Host
declare id: number
constructor(art: Host) {
this.art = art
this.id = 0
const { option } = art
installBuiltins(this, option)
for (let index = 0; index < option.plugins.length; index++) {
if (isClosing(art))
return
const registration = this.add(option.plugins[index]!)
if (registration instanceof Promise) {
// Constructor registrations have no caller-owned Promise to observe.
registration.catch(error => console.warn('Failed to initialize ArtPlayer plugin:', error))
}
}
}
add<Result>(plugin: PluginFactory<Host, Result>): PluginRegistration<Result, this>
add(plugin: PluginFactory<Host>): this | Promise<this> {
errorHandle(!isClosing(this.art), 'Cannot add a plugin after ArtPlayer is destroyed')
this.id += 1
const result = plugin.call(this.art, this.art)
if (result instanceof Promise)
return result.then(res => this.next(plugin, res))
return this.next(plugin, result)
}
next(plugin: PluginFactory<Host>, result: unknown): this {
return registerPlugin(this, plugin, result)
}
}
@@ -0,0 +1,27 @@
import { isClosing } from '../lifecycle/instance'
import { errorHandle } from '../utils/error'
import { def, has } from '../utils/property'
export default function registerPlugin<Registry extends { art: object, id: number }>(registry: Registry, plugin: { name: string }, result: unknown): Registry {
if (isClosing(registry.art))
return registry
// Keep the historical duck-typed name lookup and native property-key coercion.
const name = (result && (result as { name?: unknown }).name) || plugin.name || `plugin${registry.id}`
if (isClosing(registry.art))
return registry
let key = name as PropertyKey
const duplicate = has(registry, key)
if (isClosing(registry.art))
return registry
const message = `Cannot add a plugin that already has the same name: ${name}`
if (isClosing(registry.art))
return registry
errorHandle(!duplicate, message)
// Object keys can execute user code during native coercion. Keep that third
// coercion in its historical position, then check closure before defining.
if ((typeof name === 'object' && name !== null) || typeof name === 'function')
key = Reflect.ownKeys({ [key]: undefined })[0]!
if (!isClosing(registry.art))
def(registry, key, { value: result })
return registry
}
+19
View File
@@ -0,0 +1,19 @@
import type { PluginFactory } from '../../types/plugin'
import type { ResolvedOption } from '../option/types'
export type { PluginFactory } from '../../types/plugin'
export type PluginRegistration<Result, Registry>
= unknown extends Result ? Registry | Promise<Registry>
: Result extends Promise<unknown> ? Promise<Registry> : Registry
export interface PluginHost<Host> {
option: Pick<ResolvedOption, 'miniProgressBar' | 'isLive' | 'lock' | 'autoPlayback' | 'autoOrientation' | 'fastForward'> & {
plugins: PluginFactory<Host>[]
}
}
export interface BuiltinRegistry<Host extends PluginHost<Host>> {
art: Host
add: (plugin: PluginFactory<Host>) => unknown
}