refactor(types): CORE-07 reconcile public declarations and preserve legacy consumers

This commit is contained in:
Harvey Zhao committed 2026-09-11 01:42:32 +08:00
1 parent ee859e4804
commit f9707bad71
34 files changed
+2806 -85

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
# Public type compatibility boundaries
The declarations preserve legal historical TypeScript consumers as well as runtime
calls. Some old return and callback declarations were inaccurate. Changing a
return from Promise to a union, or an inferred callback parameter from cue to cue
array, can break old source even when runtime behavior is unchanged. Do not hide
that conflict with `any`, an impossible intersection, or a runtime Promise wrapper.
## Additive declarations
- Option retains its required string URL and existing component read types.
OptionInput accepts omitted URL and numeric component HTML. Constructor overloads
accept both; Component.add/update also accept ComponentInput. Use OptionInput for
new configurations that require these forms. Instance/static option read types
retain their old surface; internal ResolvedOption models the expanded input.
- Artplayer.Emitter now returns the named Emitter interface. It supports optional
typed event maps, correlated payloads, explicit callback context, symbols and
chaining. Constructor arguments remain accepted and ignored at runtime.
- SubtitleUpdateEvents describes the real before/after update cue arrays. Extra
on/once/off/emit overloads accept explicit array callbacks. Historical Events
and the first overload retain old contextual inference for compatibility:
```ts
art.on('subtitleBeforeUpdate', (cues: VTTCue[]) => {
cues.forEach(cue => console.log(cue.text))
})
```
- Utils includes existing unescape and ArtPlayerError exports and optional sleep
delay. A PropertyKey/identity def overload supports symbol keys while the old
string-key signature remains first. These are type additions, not new runtime
functions or exports.
## Retained conflicts and owners
| Surface | Actual runtime | Retained historical declaration | Follow-up |
| ------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------ |
| plugins.add | Registry for a synchronous factory; Promise of registry for a Promise factory | Always Promise | CORE-08/21, BASE-TYPE-04 |
| toggle | Pause result or original play Promise, including rejection | void | CORE-10/21, BASE-TYPE-04 |
| debounce/throttle | Wrapper returns undefined; debounce ignores its old context argument | Callback return type | CORE-21, BASE-TYPE-05 |
| def with string key | Native defineProperty returns the target | void, arbitrary descriptor input accepted | CORE-21, BASE-TYPE-05 |
| setting.find | Item or null | Item or undefined | CORE-14/21, BASE-TYPE-07 |
| setting.add/update/remove | Item/item/undefined | Setting registry | CORE-14/21, BASE-TYPE-07 |
| subtitle update events | VTTCue array | Scalar contextual inference unless explicitly annotated | CORE-15/21, BASE-TYPE-07 |
| notice.show getter | Boolean visibility | Historical message/false union | CORE-18/21, BASE-TYPE-07 |
These remain open declaration work, not release waivers. CORE-21 must resolve the
public/generated facade strategy against both actual returns and historical source
consumers. Migrated implementation modules must use their accurate internal types.
For existing application code, awaiting plugins.add or toggle works for synchronous
and Promise results; ignore timer results, guard setting.find with a truthiness
check, and explicitly annotate cue-array listeners. Do not rely on the inaccurate
return types to call Promise methods on synchronous results or chain settings.
## Evidence and maintenance
declaration-legacy.ts is a compile-only regression fixture, not recommended runtime
usage. It retains the historical assignments that make direct return tightening
incompatible. declaration-inputs.ts covers the additions with old and current TS,
Node10/NodeNext/Bundler, and actual installed packages. emitter-source.ts also checks
the public Emitter shape against the real implementation.
Browser declarations.spec.js compares published/candidate plugin registration,
rejection identity, actual toggle playback, settings return identity, notice state,
static Emitter context and native subtitle cue events. The generated VTT is test
content; explicit Blob URL teardown there does not certify subtitle resource cleanup
(CORE-15). options.spec.js verifies numeric content and omitted URL in the real DOM.
Run yarn typecheck, yarn ci:check, yarn build:ts and yarn test:package:release, then
the installed-artifact browser suite. Keep the exact historical TS 4.3.5 notice
getter/setter error isolated to the frozen 5.4.0 package. Current declarations must
remain free of diagnostics in every supported compiler mode.
+16 -8
View File
@@ -26,16 +26,24 @@ When changing types, run `yarn typecheck`, `yarn test:baseline` and
chapter/legacy factories, language data, invalid inputs, NodeNext/Bundler and old
TS 4.3.5 consumers. Run `yarn build:ts` to refresh editor declarations. Full editor
behavior and other core declaration/runtime mismatches have CORE-07/SITE tasks.
## Configuration migration boundary
CORE-05 keeps the public Option unchanged. Internal src/option/types.ts distinguishes
typed input from defaults-filled ResolvedOption; it is not a new public export. The
existing public url requirement and exclusion of numeric control HTML differ from
verified JS behavior (BASE-TYPE-06, CORE-07). test/types/options-source.ts records the
current rejection; replace those two negative cases with positive consumer coverage
when expanding the public declarations. Do not change runtime validation to match a
narrower historical declaration. Other optional fields and callback signatures remain
subject to the existing public compatibility contract.
Option retains its historical required URL and component read types. OptionInput is
an additive constructor input: URL is optional and numeric component HTML is accepted.
The constructor keeps its old overload and adds OptionInput; Component.add/update
similarly retain their old signatures and add ComponentInput. Internal ResolvedOption
uses the expanded input with defaults filled. It is not a new public runtime export.
This preserves assignments such as `const url: string = option.url` for existing
Option consumers while matching the verified JS input forms (BASE-TYPE-06).
Use OptionInput for newly typed configurations that omit URL or use numeric HTML.
test/types/options-source.ts and declaration-inputs.ts cover those forms positively;
declaration-legacy.ts preserves old reads and historically legal return assumptions.
Do not change runtime validation to match a narrower declaration. The additions,
retained conflicts and follow-up owners are detailed in [COMPATIBILITY.md](./COMPATIBILITY.md).
CORE-07 coordinates the differences; it does not close every conflicting old return
signature. CORE-21 remains responsible for the generated/public declaration strategy.
## Internal media migration
+5
View File
@@ -1,18 +1,23 @@
import type * as Definition from './artplayer.js'
declare const Artplayer: typeof Definition.default
// eslint-disable-next-line ts/no-redeclare -- CJS constructor value and instance type share the export.
type Artplayer = Definition.default
// eslint-disable-next-line ts/no-redeclare -- Namespace merging carries named types on export =.
declare namespace Artplayer {
type Config = Definition.Config
type Emitter<Events extends { [Name in keyof Events]: readonly unknown[] } = Record<PropertyKey, unknown[]>> = Definition.Emitter<Events>
type Events = Definition.Events
type I18n = Definition.I18n
type Icons = Definition.Icons
type Option = Definition.Option
type OptionInput = Definition.OptionInput
type Player = Definition.Player
type Setting = Definition.Setting
type SettingOption = Definition.SettingOption
type Subtitle = Definition.Subtitle
type SubtitleUpdateEvents = Definition.SubtitleUpdateEvents
type Template = Definition.Template
type Utils = Definition.Utils
}
+1 -1
View File
@@ -1,2 +1,2 @@
export { default } from './artplayer.cjs'
export type { Config, Events, I18n, Icons, Option, Player, Setting, SettingOption, Subtitle, Template, Utils } from './artplayer.js'
export type { Config, Emitter, Events, I18n, Icons, Option, OptionInput, Player, Setting, SettingOption, Subtitle, SubtitleUpdateEvents, Template, Utils } from './artplayer.js'
+17 -3
View File
@@ -1,9 +1,10 @@
import type { Component } from './component'
import type { Config } from './config'
import type { Events } from './events'
import type { Emitter } from './emitter'
import type { Events, SubtitleUpdateEvents } from './events'
import type { I18n } from './i18n'
import type { Icons } from './icons'
import type { Option } from './option'
import type { Option, OptionInput } from './option'
import type { Player } from './player'
import type { Setting, SettingOption } from './setting'
import type { Subtitle } from './subtitle'
@@ -12,20 +13,24 @@ import type { Utils } from './utils'
export type {
Config,
Emitter,
Events,
I18n,
Icons,
Option,
OptionInput,
Player,
Setting,
SettingOption,
Subtitle,
SubtitleUpdateEvents,
Template,
Utils,
}
export default class Artplayer extends Player {
constructor(option: Option, readyCallback?: (this: Artplayer, art: Artplayer) => unknown)
constructor(option: OptionInput, readyCallback?: (this: Artplayer, art: Artplayer) => unknown)
static readonly instances: Artplayer[]
static readonly version: string
@@ -34,7 +39,7 @@ export default class Artplayer extends Player {
static readonly config: Config
static readonly utils: Utils
static readonly scheme: Record<keyof Option, unknown>
static readonly Emitter: new (...args: unknown[]) => unknown
static readonly Emitter: new <Events extends { [Name in keyof Events]: readonly unknown[] } = Record<PropertyKey, unknown[]>>(...args: unknown[]) => Emitter<Events>
static readonly validator: <T extends object>(option: T, scheme: object) => T
static readonly kindOf: (item: unknown) => string
static readonly html: Artplayer['template']['html']
@@ -92,15 +97,19 @@ export default class Artplayer extends Player {
torrent?: unknown
on<T extends keyof Events>(name: T, fn: (...args: Events[T]) => unknown, ctx?: object): this
on<T extends keyof SubtitleUpdateEvents>(name: T, fn: (...args: SubtitleUpdateEvents[T]) => unknown, ctx?: object): this
on(name: string, fn: (...args: unknown[]) => unknown, ctx?: object): this
once<T extends keyof Events>(name: T, fn: (...args: Events[T]) => unknown, ctx?: object): this
once<T extends keyof SubtitleUpdateEvents>(name: T, fn: (...args: SubtitleUpdateEvents[T]) => unknown, ctx?: object): this
once(name: string, fn: (...args: unknown[]) => unknown, ctx?: object): this
emit<T extends keyof Events>(name: T, ...args: Events[T]): this
emit<T extends keyof SubtitleUpdateEvents>(name: T, ...args: SubtitleUpdateEvents[T]): this
emit(name: string, ...args: unknown[]): this
off<T extends keyof Events>(name: T, callback?: (...args: Events[T]) => unknown): this
off<T extends keyof SubtitleUpdateEvents>(name: T, callback?: (...args: SubtitleUpdateEvents[T]) => unknown): this
off(name: string, callback?: (...args: unknown[]) => unknown): this
query: Artplayer['template']['query']
@@ -186,13 +195,18 @@ export default class Artplayer extends Player {
readonly setting: {
option: SettingOption[]
updateStyle: (width?: number) => void
/** Legacy return signature; a missing runtime entry is null. */
find: (name: string) => SettingOption | undefined
/** Legacy return signature; runtime returns the formatted input item. */
add: (setting: Setting) => Artplayer['setting']
/** Legacy return signature; runtime returns the updated or added item. */
update: (settings: Setting) => Artplayer['setting']
/** Legacy return signature; runtime returns undefined. */
remove: (name: string) => Artplayer['setting']
} & Component
readonly plugins: {
/** Legacy signature: synchronous factories return this registry directly; Promise factories return a Promise of it. */
add: (
plugin: (this: Artplayer, art: Artplayer) => unknown | Promise<unknown>,
) => Promise<Artplayer['plugins']>
+13 -2
View File
@@ -1,5 +1,10 @@
import type Artplayer from './artplayer'
/** Input accepted by the existing HTML renderer, including numeric content. */
export interface ComponentInput extends Omit<ComponentOption, 'html'> {
html?: string | HTMLElement | number
}
export interface Selector {
/**
* Whether the default is selected
@@ -56,7 +61,10 @@ export interface Component {
/**
* Dynamic add a component
*/
add: (option: ComponentOption | ((art: Artplayer) => ComponentOption)) => HTMLElement | undefined
add: {
(option: ComponentOption | ((art: Artplayer) => ComponentOption)): HTMLElement | undefined
(option: ComponentInput | ((art: Artplayer) => ComponentInput)): HTMLElement | undefined
}
/**
* Dynamic remove a component by name
@@ -66,7 +74,10 @@ export interface Component {
/**
* Dynamic update a component
*/
update: (option: ComponentOption) => HTMLElement | undefined
update: {
(option: ComponentOption): HTMLElement | undefined
(option: ComponentInput): HTMLElement | undefined
}
}
export interface ComponentOption {
+8
View File
@@ -0,0 +1,8 @@
/** The event bus exposed by Artplayer.Emitter; no new runtime export. */
export interface Emitter<Events extends { [Name in keyof Events]: readonly unknown[] } = Record<PropertyKey, unknown[]>> {
e?: { [Name in keyof Events]?: { fn: (...args: [...Events[Name]]) => unknown, ctx: unknown }[] }
on: <Name extends keyof Events, Context>(name: Name, fn: (this: Context, ...args: [...Events[Name]]) => unknown, ctx?: Context) => this
once: <Name extends keyof Events, Context>(name: Name, fn: (this: Context, ...args: [...Events[Name]]) => unknown, ctx?: Context) => this
emit: <Name extends keyof Events>(name: Name, ...args: [...Events[Name]]) => this
off: <Name extends keyof Events>(name: Name, fn?: (...args: [...Events[Name]]) => unknown) => this
}
+8
View File
@@ -3,6 +3,12 @@ import type { Subtitle } from './subtitle'
export type Bar = 'loaded' | 'played' | 'hover'
/** Actual built-in subtitle update payloads; legacy Events keeps its scalar types. */
export interface SubtitleUpdateEvents {
subtitleBeforeUpdate: [cues: VTTCue[]]
subtitleAfterUpdate: [cues: VTTCue[]]
}
export interface Events {
'document:click': [event: Event]
'document:mouseup': [event: Event]
@@ -58,7 +64,9 @@ export interface Events {
'destroy': []
'subtitleOffset': [offset: number]
/** Legacy contextual type; annotate listeners with VTTCue[] for the runtime payload. */
'subtitleBeforeUpdate': [cue: VTTCue]
/** Legacy contextual type; annotate listeners with VTTCue[] for the runtime payload. */
'subtitleAfterUpdate': [cue: VTTCue]
'subtitleLoad': [cues: VTTCue[], option: Subtitle]
+9 -1
View File
@@ -1,5 +1,5 @@
import type Artplayer from './artplayer'
import type { ComponentOption } from './component'
import type { ComponentInput, ComponentOption } from './component'
import type { CssVar } from './cssVar'
import type { I18n } from './i18n'
import type { Icons } from './icons'
@@ -48,6 +48,14 @@ export interface Thumbnails {
scale?: number
}
/** Constructor input; Option retains its historical required URL/read types. */
export interface OptionInput extends Omit<Option, 'url' | 'controls' | 'layers' | 'contextmenu'> {
url?: string
controls?: ComponentInput[]
layers?: ComponentInput[]
contextmenu?: ComponentInput[]
}
export interface Option {
/**
* The player id
+1
View File
@@ -93,6 +93,7 @@ export declare class Player {
pause(): void
play(): Promise<void>
/** Legacy signature; runtime preserves pause's synchronous result or play's Promise. */
toggle(): void
attr(key: string, value?: unknown): unknown
+11 -2
View File
@@ -44,20 +44,29 @@ export interface Utils {
errorHandle: <T extends boolean>(condition: T, msg: string) => T extends true ? T : never
silencePromise: <T>(value: T) => T extends Promise<infer R> ? Promise<R | undefined> : T
def: (obj: object, name: string, value: unknown) => void
def: {
/** Historical string-key signature; runtime returns obj. */
(obj: object, name: string, value: unknown): void
<T>(obj: T, name: PropertyKey, value: PropertyDescriptor & ThisType<T>): T
}
has: (obj: object, name: PropertyKey) => boolean
get: (obj: object, name: PropertyKey) => PropertyDescriptor | undefined
mergeDeep: <T extends object[]>(...args: T) => T[number]
sleep: (ms: number) => Promise<void>
sleep: (ms?: number) => Promise<void>
/** Historical return type; runtime discards the callback result and ignores context. */
debounce: <F extends (...args: any[]) => any>(func: F, wait: number, context?: object) => (...args: Parameters<F>) => ReturnType<F>
/** Historical return type; runtime discards the callback result. */
throttle: <F extends (...args: any[]) => any>(func: F, wait: number) => (...args: Parameters<F>) => ReturnType<F>
clamp: (num: number, a: number, b: number) => number
secondToTime: (second: number) => string
escape: (str: string) => string
unescape: (str: string) => string
capitalize: (str: string) => string
ArtPlayerError: new (message?: string, context?: ((...args: never[]) => unknown) | (abstract new (...args: never[]) => object)) => Error
getIcon: (key?: string, html?: string | HTMLElement) => HTMLElement
getComposedPath: (event: Event) => EventTarget[]
supportsFlex: () => boolean