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

+12 -11
View File
@@ -13,7 +13,7 @@ The entry passes Artplayer.option to `option/resolve.ts`, which performs the exi
deep merge, restores input.container by reference and invokes the same option-validator
function. Allocation of the instance id still precedes validation; DOM mounting follows it.
`option/types.ts` reuses the public Option as the typed input contract. ResolvedOption
`option/types.ts` reuses the public OptionInput as the typed input contract. ResolvedOption
requires top-level defaults and subtitle/thumbnail defaults, while explicitly allowing
proxy to remain undefined. ResolvedInput preserves extra typed application fields without
adding a global any index signature. The one assertion after validation connects the
@@ -30,12 +30,14 @@ The public Artplayer.validator and scheme references remain available as before.
Merge behavior includes unknown own keys, collection item references, two reads of an
enumerable input container getter, and restoration of an inherited container. Explicit
undefined can still fail the existing schema instead of being silently defaulted. This
task changes neither validation rules nor public consumer declarations.
undefined can still fail the existing schema instead of being silently defaulted.
Runtime validation is unchanged; public input expansions are described below.
Two verified declaration gaps are tracked as BASE-TYPE-06 for CORE-07: existing JS can
omit url and use numeric control HTML, while the current public types reject those forms.
The historical rejection fixtures must become positive consumer checks when reconciled.
CORE-07 adds OptionInput for omitted URL and numeric component HTML. Historical Option
keeps its required URL/read types; the constructor accepts both via overloads. Source
fixtures now accept the expanded inputs, and declaration-legacy.ts protects old reads.
The additive declarations and remaining return conflicts are tracked in
[types/COMPATIBILITY.md](./types/COMPATIBILITY.md).
Other permissive validator cases are not blanket guarantees that later DOM/media code
can consume every accepted value. Preserve failure stages while migrating those consumers.
@@ -257,11 +259,10 @@ documented cast to the pre-existing generic return contract. No file-wide any or
type-check suppression was added. The main public declarations remain a separate
compatibility surface; see [types/README.md](./types/README.md).
Existing declaration discrepancies are still tracked for CORE-07: Utils lacks some
runtime exports, def is declared void, debounce/throttle declare the callback return
instead of void, and debounce's old optional context argument is ignored at runtime.
Sleep's declaration also requires a delay while runtime defaults to zero. These were
not silently tightened during source migration; use the actual source types internally.
Public return discrepancies remain after CORE-07. It adds unescape/ArtPlayerError,
optional sleep and a symbol/PropertyKey def overload; old string def and debounce/throttle return declarations
remain for source compatibility. The old debounce context argument is ignored at runtime.
BASE-TYPE-05 remains open through CORE-21; use the actual source types internally.
Lifecycle-owned timer cancellation is part of CORE-04/17 and the existing BASE-PERF-01
finding, not a claim that a standalone debounce can know when its owner is destroyed.
+2 -2
View File
@@ -1,9 +1,9 @@
import type { Option, ResolvedInput, ResolvedOption } from './types'
import type { OptionInput, ResolvedInput, ResolvedOption } from './types'
import validator from 'option-validator'
import scheme from '../scheme'
import { mergeDeep } from '../utils/property'
export default function resolveOption<Input extends Option>(input: Input, defaults: ResolvedOption): ResolvedInput<Input> {
export default function resolveOption<Input extends OptionInput>(input: Input, defaults: ResolvedOption): ResolvedInput<Input> {
const merged = mergeDeep(defaults, input)
merged.container = input.container
// Keep the existing runtime validator. It checks the known schema, not extension data.
+5 -5
View File
@@ -1,13 +1,13 @@
import type { Option, Thumbnails } from '../../types/option'
import type { OptionInput, Thumbnails } from '../../types/option'
import type { Subtitle } from '../../types/subtitle'
export type { Option } from '../../types/option'
export type { Option, OptionInput } from '../../types/option'
// A successfully merged typed input, not a general guard for arbitrary JS objects.
export interface ResolvedOption extends Omit<Required<Option>, 'proxy' | 'subtitle' | 'thumbnails'> {
proxy: Option['proxy']
export interface ResolvedOption extends Omit<Required<OptionInput>, 'proxy' | 'subtitle' | 'thumbnails'> {
proxy: OptionInput['proxy']
subtitle: Required<Subtitle>
thumbnails: Required<Thumbnails>
}
export type ResolvedInput<Input extends Option> = ResolvedOption & Omit<Input, keyof Option>
export type ResolvedInput<Input extends OptionInput> = ResolvedOption & Omit<Input, keyof OptionInput>
+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