refactor(core): [CORE-21] generate compatible public and editor declarations

This commit is contained in:
Harvey Zhao committed 2026-09-11 17:56:05 +08:00
1 parent ee895a558f
commit faaa3e8c2d
123 files changed
+31305 -1712

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
import { expect, test } from './fixtures.js'
test('docs Monaco worker consumes generated core types and runs a typed player example', async ({ page }) => {
await page.goto('/test/player.html?core=candidate&chapter=published')
await page.addScriptTag({ url: '/assets/js/vs/loader.js' })
const result = await page.evaluate(async () => {
window.require.config({ paths: { vs: '/assets/js/vs' } })
await new Promise((resolve, reject) => window.require(['vs/editor/editor.main'], resolve, reject))
const api = window.monaco
const types = api.languages.typescript
types.typescriptDefaults.setCompilerOptions({ target: types.ScriptTarget.ES2020, strict: true, skipLibCheck: false, noEmit: false, types: [], allowNonTsExtensions: true })
const source = await (await fetch('/assets/ts/artplayer.d.ts')).text()
const libraryUri = 'file:///artplayer.d.ts'
const library = types.typescriptDefaults.addExtraLib(source, libraryUri)
const model = api.editor.createModel(`
const editorOption: Artplayer.OptionInput = { container: '.player', url: '/test/pattern.mp4', muted: true }
const editorArt: Artplayer = new Artplayer(editorOption)
const editorEvents: Artplayer.Emitter<{ value: [number] }> = new Artplayer.Emitter()
editorEvents.emit('value', 1)
`, 'typescript', api.Uri.parse('file:///consumer.ts'))
const invalid = api.editor.createModel('new Artplayer({ container: 123, url: "" })', 'typescript', api.Uri.parse('file:///invalid.ts'))
try {
const getWorker = await types.getTypeScriptWorker()
const worker = await getWorker(model.uri, invalid.uri)
const syntax = await worker.getSyntacticDiagnostics(model.uri.toString())
const semantic = await worker.getSemanticDiagnostics(model.uri.toString())
const declaration = await worker.getSemanticDiagnostics(libraryUri)
const bad = await worker.getSemanticDiagnostics(invalid.uri.toString())
const output = await worker.getEmitOutput(model.uri.toString())
if (syntax.length || semantic.length || declaration.length)
return { syntax, semantic, declaration, invalid: bad.map(item => item.code), ready: false }
const script = output.outputFiles.find(file => file.name.endsWith('.js'))
if (!script)
throw new Error('Monaco did not emit the typed example')
// eslint-disable-next-line no-new-func -- Execute the locally compiled editor fixture, as the demo's Run action does.
const art = new Function(`${script.text}\nreturn editorArt`)()
try {
if (!art.isReady)
await new Promise(resolve => art.on('ready', resolve))
return { syntax, semantic, declaration, invalid: bad.map(item => item.code), ready: art.isReady && art.video.videoWidth > 0 }
}
finally {
art.destroy()
}
}
finally {
model.dispose()
invalid.dispose()
library.dispose()
}
})
expect(result.syntax).toEqual([])
expect(result.semantic).toEqual([])
expect(result.declaration).toEqual([])
expect(result.invalid.length).toBeGreaterThan(0)
expect(result.ready).toBe(true)
})
+169
View File
@@ -0,0 +1,169 @@
import { expect, test } from './fixtures.js'
for (const core of ['published', 'candidate']) {
test(`${core}: constructor callback hosts expose only initialized components`, async ({ page }) => {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
const result = await page.evaluate(async () => {
const observations = []
const receivers = []
const names = ['template', 'events', 'storage', 'icons', 'i18n', 'notice', 'player', 'layers', 'controls', 'contextmenu', 'subtitle', 'info', 'loading', 'hotkey', 'mask', 'setting', 'plugins']
const record = (phase, receiver, instance) => {
receivers.push(receiver)
observations.push({ phase, receiver: receiver === instance, fields: names.filter(name => Object.hasOwn(instance, name)) })
}
let mounted
const settingMounted = new Promise(resolve => mounted = resolve)
let ready
const loaded = new Promise(resolve => ready = resolve)
const art = new window.Artplayer({
container: '.player',
url: '/test/pattern.mp4',
type: 'fixture',
muted: true,
setting: true,
proxy(instance) {
record('proxy', this, instance)
return document.createElement('video')
},
layers: [{ html: 'Layer', mounted() { record('layer', this, this) } }],
controls: [{ html: 'Control', position: 'left', mounted() { record('control', this, this) } }],
contextmenu: [{ html: 'Menu', mounted() { record('contextmenu', this, this) } }],
settings: [{ html: 'Setting', mounted() {
record('setting', this, this)
mounted()
} }],
plugins: [function (instance) {
record('plugin', this, instance)
return { name: 'phases' }
}],
customType: { fixture(video, url, instance) {
record('customType', this, instance)
video.src = url
} },
}, function (instance) {
record('ready', this, instance)
ready()
})
try {
await Promise.all([settingMounted, loaded])
return { names, observations, complete: window.Artplayer.instances.includes(art), receivers: receivers.every(receiver => receiver === art) }
}
finally {
art.destroy()
}
})
expect(result.complete).toBe(true)
expect(result.receivers).toBe(true)
const expected = { proxy: 0, layer: 7, control: 8, contextmenu: 9, plugin: 16, setting: 17, customType: 17, ready: 17 }
expect(result.observations.map(item => item.phase).sort()).toEqual(Object.keys(expected).sort())
for (const item of result.observations) {
expect(item.receiver).toBe(true)
expect(item.fields, item.phase).toEqual(result.names.slice(0, expected[item.phase]))
}
})
test(`${core}: container validation rejects non-div elements`, async ({ page }) => {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
const result = await page.evaluate(() => {
const container = document.createElement('section')
document.body.append(container)
try {
const player = new window.Artplayer({ container, url: '' })
player.destroy()
return 'accepted'
}
catch (error) {
return error.message
}
finally {
container.remove()
}
})
expect(result).toBe('Unsupported container element type, only support \'div\' but got \'section\'')
})
}
test('precise declarations: actual return values and callback receivers agree with the installed view', async ({ page }) => {
await page.goto('/test/player.html?core=candidate&chapter=published')
const result = await page.evaluate(async () => {
const Player = window.Artplayer
const container = document.querySelector('.player')
const callbacks = []
let ready
const loaded = new Promise(resolve => ready = resolve)
const factory = function (instance) {
callbacks.push({ same: this === instance, registryAbsent: this.plugins === undefined })
return { name: 'precise', update(value) {
return value
} }
}
const art = new Player({
container,
url: '/test/pattern.mp4',
muted: true,
plugins: [factory, window.artplayerPluginChapter({ chapters: [{ start: 0, end: Infinity, title: 'Existing plugin' }] })],
}, function (instance) {
ready({ same: this === instance, inInstances: Player.instances.includes(instance) })
})
try {
const readyResult = await loaded
const sync = art.plugins.add(function (instance) {
callbacks.push({ same: this === instance, registry: this.plugins.art === instance })
return { name: 'sync' }
})
const pending = art.plugins.add(async function (instance) {
callbacks.push({ same: this === instance, registry: this.plugins.art === instance })
return { name: 'async' }
})
const asyncResult = await pending
const item = { name: 'typed-setting', html: 'Setting' }
const added = art.setting.add(item)
const updated = art.setting.update({ name: item.name, html: 'Updated' })
const control = art.controls.add({ name: 'typed-control', html: 'Control', position: 'left' })
const style = art.subtitle.style({ color: 'white' })
art.notice.show = 'Visible'
const notice = art.notice.show
art.notice.show = false
const hidden = art.notice.show
const pause = art.pause()
const removed = art.setting.remove(item.name)
return {
ready: readyResult,
callbacks,
constructor: art.constructor === Player,
optionFactory: art.option.plugins[0] === factory,
registry: sync === art.plugins && asyncResult === art.plugins,
promiseBranch: pending instanceof Promise && !(sync instanceof Promise),
commandReads: ['seek', 'forward', 'backward', 'switch', 'quality'].every(name => art[name] === undefined),
setting: added === item && updated === item && removed === undefined && art.setting.find(item.name) === null,
control: control === undefined,
subtitle: style === art.template.$subtitle && Array.isArray(art.subtitle.cues) && Array.isArray(art.subtitle.activeCues),
notice: notice === true && hidden === false,
template: art.template.$container === container && !('html' in art.template) && typeof Player.html === 'string',
pause: pause === undefined,
absentStatics: !('env' in Player) && !('build' in Player),
chapter: typeof art.plugins.artplayerPluginChapter?.update === 'function',
}
}
finally {
art.destroy()
}
})
expect(result).toEqual({
ready: { same: true, inInstances: true },
callbacks: [{ same: true, registryAbsent: true }, { same: true, registry: true }, { same: true, registry: true }],
constructor: true,
optionFactory: true,
registry: true,
promiseBranch: true,
commandReads: true,
setting: true,
control: true,
subtitle: true,
notice: true,
template: true,
pause: true,
absentStatics: true,
chapter: true,
})
})
+45
View File
@@ -0,0 +1,45 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
// eslint-disable-next-line test/no-import-node-test -- Declaration generation must reject drift and invalid public sources.
import { test } from 'node:test'
import { generateCoreDeclarations } from '../scripts/build-types.mjs'
test('declaration checks reject stale output without overwriting it and recover through explicit generation', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'artplayer-declarations-'))
const folder = path.join(root, 'packages/artplayer')
fs.mkdirSync(path.join(folder, 'public'), { recursive: true })
fs.mkdirSync(path.join(folder, 'types'), { recursive: true })
const source = path.join(folder, 'public/contract.ts')
const output = path.join(folder, 'types/contract.d.ts')
try {
fs.writeFileSync(source, 'export interface Contract { name: string }\n')
await assert.rejects(generateCoreDeclarations({ root }), /stale/)
assert.equal(fs.existsSync(output), false)
await generateCoreDeclarations({ root, write: true })
const generated = fs.readFileSync(output, 'utf8')
assert.match(generated, /Generated from public\/contract.ts/)
assert.deepEqual((await generateCoreDeclarations({ root })).changed, [])
fs.writeFileSync(source, 'export interface Contract { name: string, enabled: boolean }\n')
await assert.rejects(generateCoreDeclarations({ root }), /stale/)
assert.equal(fs.readFileSync(output, 'utf8'), generated)
await generateCoreDeclarations({ root, write: true })
assert.match(fs.readFileSync(output, 'utf8'), /enabled: boolean/)
assert.equal(fs.existsSync(path.join(folder, 'types/contract.js')), false)
fs.writeFileSync(source, 'export interface Contract { name: MissingPublicType }\n')
const valid = fs.readFileSync(output, 'utf8')
await assert.rejects(generateCoreDeclarations({ root, write: true }), /MissingPublicType/)
assert.equal(fs.readFileSync(output, 'utf8'), valid)
fs.mkdirSync(path.join(folder, 'src'))
fs.writeFileSync(path.join(folder, 'src/private.ts'), 'export interface Private { internal: boolean }\n')
fs.writeFileSync(source, 'export type { Private } from "../src/private.js"\n')
await assert.rejects(generateCoreDeclarations({ root, write: true }), /rootDir|non-public/)
assert.equal(fs.readFileSync(output, 'utf8'), valid)
}
finally {
assert.equal(path.dirname(path.resolve(root)), path.resolve(os.tmpdir()))
assert(path.basename(root).startsWith('artplayer-declarations-'))
fs.rmSync(root, { recursive: true, force: true })
}
})
+21
View File
@@ -0,0 +1,21 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
// eslint-disable-next-line test/no-import-node-test -- Exercise the real standalone editor compiler and generator.
import { test } from 'node:test'
import { ESLint } from 'eslint'
import compat from 'typescript-compat'
import { asGlobalDeclaration, checkCoreEditorDeclaration, generateCoreEditorDeclaration } from '../scripts/editor-types.mjs'
test('editor declarations are reproducible, standalone and preserve constructor/named types in both compilers', async () => {
const generated = generateCoreEditorDeclaration()
const file = 'docs/assets/ts/artplayer.d.ts'
const eslint = new ESLint({ fix: true, fixTypes: ['layout'] })
const [formatted] = await eslint.lintText(generated, { filePath: file })
assert.equal(formatted.errorCount, 0)
assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), formatted.output || generated, 'Editor declarations are stale; run yarn build:ts')
assert.deepEqual(checkCoreEditorDeclaration(formatted.output || generated, compat), [])
const corrupted = generated.replace('export type OptionInput = ArtplayerDefinitions.OptionInput;', 'export type OptionInput = MissingEditorType;')
assert.notEqual(corrupted, generated, 'Missing corruption target')
assert(checkCoreEditorDeclaration(corrupted).some(diagnostic => diagnostic.code === 2304), 'Invalid output must not be hidden by skipLibCheck')
assert.throws(() => asGlobalDeclaration('export { default } from "./missing"', 'Artplayer'), /unresolved exports/)
})
+2
View File
@@ -14,6 +14,8 @@ test('Package checks reject missing files, missing wildcard exports and internal
for (const missing of files)
assert.throws(() => checkFiles(manifest, files.filter(file => file !== missing)), /Missing/)
assert.throws(() => checkFiles(manifest, [...files, 'package/tsconfig.json']), /tsconfig/)
for (const source of ['src/internal.ts', 'public/artplayer.ts', 'node_modules/private/index.js'])
assert.throws(() => checkFiles(manifest, [...files, `package/${source}`]), /Source\/dependencies/)
assert.throws(() => checkFiles(manifest, files, ['package/dist/old.js']), /Historical/)
})
+10
View File
@@ -38,6 +38,16 @@ Remove a known case when its owning task fixes it; keep frozen release evidence
`yarn test:package:release` additionally rejects any remaining known type errors.
Passing this command alone is not authorization or sufficient evidence to publish.
The optional core `artplayer/runtime` entry has eight additional strict consumer
groups: TS 5.1.6 and 5.9.3 each check Node10 CommonJS, NodeNext CJS/ESM and Bundler.
These fixtures cover accurate returns and getter/setter types, construction-stage
callback hosts, both old and new module augmentations, old chapter factories and
the shared modern/legacy constructor. The old root remains isolated from modern
accessor syntax and retains its five original consumer groups. Runtime checks
verify the added entries reuse existing JS module and constructor identities;
`runtime/types` remains type-only. All generated core declarations are packaged,
while their authored `public/` source graph and internal implementation are excluded.
Reports, archives, installed artifact copies and build/install logs are under
`refactor/.cache/packages/run-*`; `latest.json` points to the last successful
compatibility run (a strict release rejection still leaves its report). For browser
+13
View File
@@ -27,6 +27,19 @@ const vm = require('node:vm');
const legacy = await import('artplayer/legacy')
const pluginLegacy = await import('artplayer-plugin-chapter/legacy')
check('DIST.esm-legacy', legacy.default.version === core.version && typeof pluginLegacy.default({}) === 'function')
if (!expected.baseline) {
const precise = require('artplayer/runtime')
const preciseEsm = await import('artplayer/runtime')
const preciseLegacy = require('artplayer/runtime/legacy')
const preciseLegacyEsm = await import('artplayer/runtime/legacy')
check('DIST.runtime-cjs-identity', precise === core && require.resolve('artplayer/runtime') === require.resolve('artplayer'))
check('DIST.runtime-esm-identity', preciseEsm === esm && preciseEsm.default === esm.default)
check('DIST.runtime-legacy-identity', preciseLegacy === require('artplayer/legacy') && preciseLegacyEsm === legacy)
check('DIST.runtime-shared-statics', precise.instances === core.instances && preciseEsm.default.instances === esm.default.instances && preciseLegacy.instances === legacy.default.instances)
assert.throws(() => require('artplayer/runtime/types'), error => error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED')
await assert.rejects(import('artplayer/runtime/types'), error => error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED')
check('DIST.runtime-types-only', true)
}
for (const [name, filename, globalName] of [
['core', 'artplayer/dist/artplayer.js', 'Artplayer'],
['core-legacy', 'artplayer/dist/artplayer.legacy.js', 'Artplayer'],
+17
View File
@@ -0,0 +1,17 @@
/* eslint-disable ts/no-require-imports, ts/no-this-alias -- Exercise actual CJS syntax and callback receiver types. */
import Artplayer = require('artplayer/runtime')
import LegacyRuntime = require('artplayer/runtime/legacy')
const option: Artplayer.OptionInput = { container: '#player' }
const art: Artplayer = new Artplayer(option)
const legacy: Artplayer = new LegacyRuntime(option)
const factory: Artplayer.PluginFactory<{ name: string }> = function (instance) {
const receiver: Artplayer.PluginHost = this
const value: undefined = instance.seek
void [receiver, value]
return { name: 'cjs' }
}
const registry: Artplayer.Plugins = art.plugins.add(factory)
// @ts-expect-error CJS must not reintroduce legacy false getter values.
const falseSeek: number = legacy.seek
void [registry, falseSeek]
+28
View File
@@ -0,0 +1,28 @@
import type { Component, Controls } from '../../packages/artplayer/public/runtime/component'
import type { Setting, SettingItem } from '../../packages/artplayer/public/runtime/setting'
import type Artplayer from '../../packages/artplayer/src'
import type { ControlHost } from '../../packages/artplayer/src/control/types'
import type { SettingHost } from '../../packages/artplayer/src/setting/types'
declare const source: Artplayer
const controls: Controls<ControlHost> = source.controls
const component: Component<ControlHost> = source.controls
const setting: Setting<SettingHost> = source.setting
const added: undefined = controls.add({ name: 'sample', html: 1, position: 'left' })
const updated: undefined = controls.update({ name: 'sample', html: 2 })
const nullable: SettingItem<SettingHost> | null = setting.find('absent')
const item = { name: 'setting', html: 'Item', extra: 1 }
const same: typeof item = setting.add(item)
const changed: SettingItem<SettingHost> = setting.update({ name: 'setting', html: 'Updated' })
const removed: void = setting.remove('setting')
setting.add({ name: 'callback', html: 'Callback', onClick(entry) {
const offset: number = this.subtitleOffset
return [offset, entry.name]
} })
// @ts-expect-error The updated item is not the whole setting registry.
const registry: Setting<SettingHost> = setting.update({ name: 'setting' })
// @ts-expect-error A missing setting result is null, not undefined.
const absent: undefined = setting.find('absent')
// @ts-expect-error Control add does not return a mounted element.
const element: HTMLElement = controls.add({ html: 'Button', position: 'left' })
export { absent, added, changed, component, element, nullable, registry, removed, same, updated }
+72
View File
@@ -0,0 +1,72 @@
/* eslint-disable ts/no-this-alias -- Assignments verify callback initialization views. */
import type { ContextmenuHost, ControlHost, CustomTypeHost, LayerHost, OptionInput, PluginHost, ProxyHost } from 'artplayer/runtime'
import Artplayer from 'artplayer/runtime'
const option: OptionInput = {
container: document.createElement('div'),
proxy(art) {
const host: ProxyHost = art
const id: number = host.id
art.on('ready', () => {})
// @ts-expect-error Template has not returned during proxy construction.
void art.template
// @ts-expect-error Accessing the video getter before Template returns would throw.
void art.video
// @ts-expect-error Player descriptors have not been installed yet.
art.seek = 1
void id
return document.createElement('video')
},
customType: { custom(video, url, art) {
const host: CustomTypeHost = art
host.seek = '1'
const seek: undefined = host.seek
// URL assignment defers customType until construction has finished.
const player: object = art.player
art.controls.show = true
void [video, url, seek, player]
} },
layers: [art => ({ html: String(art.id), mounted() {
const host: LayerHost = this
host.seek = 1
// @ts-expect-error Layer has not returned when its mounted callback runs.
host.layers.show = true
} })],
controls: [{ position: 'left', mounted() {
const host: ControlHost = this
host.layers.show = true
// @ts-expect-error Control has not returned when its mounted callback runs.
host.controls.show = true
} }],
contextmenu: [{ mounted() {
const host: ContextmenuHost = this
host.controls.show = true
// @ts-expect-error Contextmenu has not returned when its mounted callback runs.
host.contextmenu.show = true
} }],
setting: true,
settings: [{ html: 'Setting', mounted() {
// Setting mounted uses a deferred callback after construction completes.
const host: Artplayer = this
host.setting.show = true
host.plugins.add(() => ({ name: 'from-setting' }))
} }],
plugins: [function (art) {
const host: PluginHost = art
host.setting.show = true
host.seek = 1
// @ts-expect-error Registry is assigned only after constructor factories return.
host.plugins.add(() => ({ name: 'too-early' }))
return { name: 'construction' }
}],
}
const art = new Artplayer(option, function (art) {
art.seek = 2
this.controls.show = true
this.plugins.add(() => ({ name: 'ready' }))
})
// @ts-expect-error Only div containers satisfy the runtime element validation.
const invalidContainer: OptionInput = { container: document.createElement('section') }
// @ts-expect-error A configured proxy must return a video or media-like canvas.
const invalidProxy: OptionInput = { container: '#player', proxy: () => undefined }
void [art, invalidContainer, invalidProxy]
+30
View File
@@ -0,0 +1,30 @@
import type { Emitter } from '../../packages/artplayer/public/emitter'
import type { Events } from '../../packages/artplayer/public/runtime/events'
import type { Notice, Subtitle, SubtitleCue } from '../../packages/artplayer/public/runtime/subtitle'
import type Artplayer from '../../packages/artplayer/src'
declare const source: Artplayer
const events: Emitter<Events> = source
const subtitle: Subtitle = source.subtitle
const notice: Notice = source.notice
const visible: boolean = notice.show
notice.show = 'message'
const switched: Promise<string | null | undefined> = subtitle.switch('/subtitle.vtt')
const styled: HTMLDivElement = subtitle.style('font-size', 20)
events.on('subtitleBeforeUpdate', (cues) => {
const actual: SubtitleCue[] = cues
return actual.map(cue => cue.text)
})
events.on('blur', event => event.type)
source.on('subtitleBeforeUpdate', cues => cues.map(cue => cue.text))
events.emit('subtitleAfterUpdate', [])
events.emit('seek', 1.5, '1.5')
// @ts-expect-error The corrected payload cannot retain historical scalar cue inference.
events.on('subtitleBeforeUpdate', (cue: VTTCue) => cue.text)
// @ts-expect-error Native focus loss carries its original Event.
events.emit('blur')
// @ts-expect-error The actual source map must also reject a scalar update payload.
source.emit('subtitleAfterUpdate', 1)
// @ts-expect-error A visibility getter does not return the last message.
const message: string = notice.show
export { events, message, styled, switched, visible }
+31
View File
@@ -0,0 +1,31 @@
import type SourceArtplayer from '../../packages/artplayer/src'
import type { I18nHost } from '../../packages/artplayer/src/i18n'
import type RuntimeArtplayer from '../../packages/artplayer/types/runtime'
import type { ArtplayerConstructor } from '../../packages/artplayer/types/runtime'
import type { Config, EventRegistry, Hotkey, I18n, Storage } from '../../packages/artplayer/types/runtime/services'
declare const source: SourceArtplayer
declare const constructor: typeof SourceArtplayer
const events: EventRegistry = source.events
const storage: Storage = source.storage
const hotkey: Hotkey<SourceArtplayer> = source.hotkey
const language: I18n<I18nHost> = source.i18n
const config: Config = constructor.config
const validator: ArtplayerConstructor['validator'] = constructor.validator
const kindOf: ArtplayerConstructor['kindOf'] = constructor.kindOf
const scheme: ArtplayerConstructor['scheme'] = constructor.scheme
// The facade covers every implementation member without whole-instance assertions.
const missingInstance: Exclude<keyof SourceArtplayer, keyof RuntimeArtplayer> extends never ? true : false = true
const extraInstance: Exclude<keyof RuntimeArtplayer, keyof SourceArtplayer | 'flv' | 'm3u8' | 'hls' | 'ts' | 'mpd' | 'torrent'> extends never ? true : false = true
const missingStatic: Exclude<keyof typeof SourceArtplayer, keyof ArtplayerConstructor> extends never ? true : false = true
const extraStatic: Exclude<keyof ArtplayerConstructor, keyof typeof SourceArtplayer> extends never ? true : false = true
type ValueMembers = 'id' | 'isLock' | 'isReady' | 'isFocus' | 'isInput' | 'isRotate' | 'isDestroy' | 'video' | 'player' | 'reset' | 'destroy'
const values: Pick<RuntimeArtplayer, ValueMembers> = source
type SharedOptions = Exclude<keyof RuntimeArtplayer['option'], 'proxy' | 'plugins' | 'customType' | 'layers' | 'controls' | 'contextmenu' | 'settings'>
const options: Pick<RuntimeArtplayer['option'], SharedOptions> = source.option
const storageValue: unknown = storage.get()
// @ts-expect-error Persisted JSON can be a primitive, not just a settings object.
const falseSettings: Record<string, unknown> = storage.get()
void [events, storage, hotkey, language, config, validator, kindOf, scheme, missingInstance, extraInstance, missingStatic, extraStatic, values, options, storageValue, falseSettings]
+54
View File
@@ -0,0 +1,54 @@
import type { Emitter } from 'artplayer'
import type { Controls } from '../../packages/artplayer/types/runtime/component.js'
import type { Events } from '../../packages/artplayer/types/runtime/events.js'
import type { NativeMedia } from '../../packages/artplayer/types/runtime/media.js'
import type { Player } from '../../packages/artplayer/types/runtime/player.js'
import type { Plugins } from '../../packages/artplayer/types/runtime/plugin.js'
import type { Setting, SettingItem } from '../../packages/artplayer/types/runtime/setting.js'
import type { Notice, Subtitle } from '../../packages/artplayer/types/runtime/subtitle.js'
import type { Icons, Template } from '../../packages/artplayer/types/runtime/template.js'
import type { Utils } from '../../packages/artplayer/types/runtime/utils.js'
declare const player: Player<NativeMedia>
declare const registry: Plugins<Player<NativeMedia>>
declare const events: Emitter<Events>
declare const notice: Notice
declare const subtitle: Subtitle
declare const template: Template<Player<NativeMedia>, NativeMedia>
declare const icons: Icons
declare const utils: Utils
declare const controls: Controls<Player<NativeMedia>>
declare const setting: Setting<Player<NativeMedia>>
const read: undefined = player.seek
player.seek = '2'
const play: Promise<void> = player.play()
const pip: Element | null | boolean = player.pip
const immediate: typeof registry = registry.add(art => ({ name: 'plain', value: art.currentTime }))
const pending: Promise<typeof registry> = registry.add(async art => ({ name: 'async', value: art.currentTime }))
events.on('subtitleBeforeUpdate', cues => cues.map(cue => cue.text))
events.on('video:error', event => event.type)
const visible: boolean = notice.show
notice.show = new Error('test')
const changed: Promise<string | null | undefined> = subtitle.switch('/subtitle.vtt')
const native: NativeMedia = template.$video
const wrapper: HTMLElement = icons.play
const custom: HTMLElement | undefined = icons.custom
const target = { id: 1 }
const same: typeof target = utils.def(target, 'name', { value: 'name' })
const delayed: void = utils.debounce(() => 1, 0, {})()
const mounted: undefined = controls.add({ name: 'button', html: 'Button', position: 'left' })
const settingResult: SettingItem<Player<NativeMedia>> | null = setting.find('entry')
const input = { name: 'entry', html: 'Entry', custom: 123 }
const settingIdentity: typeof input = setting.add(input)
const updated: SettingItem<Player<NativeMedia>> = setting.update(input)
// @ts-expect-error Reading a setter-only command is not a numeric result.
const number: number = player.seek
// @ts-expect-error Native error forwarding preserves Event, not an Error object.
events.emit('video:error', new Error('wrong payload'))
// @ts-expect-error Update callbacks receive arrays.
events.on('subtitleBeforeUpdate', (cue: VTTCue) => cue.text)
// @ts-expect-error Deferred callbacks have no immediate numeric result.
const immediateNumber: number = utils.throttle(() => 1, 0)()
// @ts-expect-error A setting update returns the item, not the registry.
const settingRegistry: typeof setting = setting.update(input)
export { changed, custom, delayed, immediate, immediateNumber, mounted, native, number, pending, pip, play, read, same, settingIdentity, settingRegistry, settingResult, updated, visible, wrapper }
+36
View File
@@ -0,0 +1,36 @@
import type { CanvasMedia, MediaSurface, NativeMedia } from '../../packages/artplayer/public/runtime/media'
import type { Player } from '../../packages/artplayer/public/runtime/player'
import type { MediaSurface as SourceMedia } from '../../packages/artplayer/src/media/types'
import type { PlayerProperties } from '../../packages/artplayer/src/player/properties'
declare const actual: PlayerProperties
declare const media: SourceMedia
declare const native: Player<NativeMedia>
declare const canvas: Player<CanvasMedia>
const publicPlayer: Player = actual
const publicMedia: MediaSurface = media
const internalMedia: SourceMedia = publicMedia
const commands: undefined[] = [publicPlayer.seek, publicPlayer.forward, publicPlayer.backward, publicPlayer.switch, publicPlayer.quality]
const pip: Element | null | boolean = publicPlayer.pip
const url: string | null = publicPlayer.url
const nativePlay: Promise<void> = native.play()
const nativePause: void = native.pause()
const nativeToggle: void | Promise<void> = native.toggle()
const proxyPlay: Promise<unknown> = canvas.play()
publicPlayer.seek = '1.5'
publicPlayer.forward = 2
publicPlayer.quality = [{ html: document.createElement('span'), url: '/video.mp4' }]
publicPlayer.pip = true
// @ts-expect-error Commands do not have the historical fictitious numeric getters.
const position: number = publicPlayer.seek
// @ts-expect-error Accurate write types are not widened just to satisfy TS 4.3's accessor restriction.
publicPlayer.quality = undefined
// @ts-expect-error A native PiP element is a result, not a supported setter input.
publicPlayer.pip = document.createElement('video')
// @ts-expect-error The proxy play result cannot be assumed to be a native void result.
const proxyNative: Promise<void> = canvas.play()
type Assert<Condition extends true> = Condition
type Complete = Assert<Exclude<keyof PlayerProperties, keyof Player> extends never ? true : false>
type NoInventedProperties = Assert<Exclude<keyof Player, keyof PlayerProperties> extends never ? true : false>
export type { Complete, NoInventedProperties }
export { commands, internalMedia, nativePause, nativePlay, nativeToggle, pip, position, proxyNative, proxyPlay, url }
+165
View File
@@ -0,0 +1,165 @@
/* eslint-disable ts/no-this-alias -- Assignments prove contextual callback receiver types. */
import type { PluginFactory as LegacyPluginFactory } from 'artplayer'
import type { CustomTypeHost, Events, OptionInput, PluginFactory, PluginHost, Plugins } from 'artplayer/runtime'
import chapter from 'artplayer-plugin-chapter'
import Artplayer from 'artplayer/runtime'
import LegacyRuntime from 'artplayer/runtime/legacy'
declare module 'artplayer/types' {
interface Plugins {
legacyRuntimeExtension?: { set: (count: number) => void }
}
interface Events {
'legacy-runtime:update': [count: number]
}
}
declare module 'artplayer/runtime/types' {
interface Plugins {
runtimeExtension?: { update: (count: number) => void }
}
interface Events {
'runtime-extension:update': [count: number]
}
}
const factory: PluginFactory<{ name: string, update: (count: number) => void }> = function (art) {
const seek: undefined = this.seek
void seek
// @ts-expect-error A constructor factory cannot assume the registry has been assigned.
void art.plugins.art
return { name: 'runtimeExtension', update(count) {
art.emit('runtime-extension:update', count)
} }
}
const option: OptionInput = {
container: document.createElement('div'),
plugins: [factory, function (art) {
const receiver: PluginHost = this
const value: undefined = art.seek
void [receiver, value]
return { name: 'inline' }
}],
controls: [{ name: 'test', position: 'left', mounted(element) {
const value: undefined = this.seek
element.textContent = String(value)
} }],
settings: [{ html: 'Custom', onClick(item) {
const visible: boolean = this.notice.show
return `${item.html}: ${visible}`
} }],
customType: { hls(video, url, art) {
const receiver: CustomTypeHost = this
const value: undefined = art.forward
void [receiver, value, video, url]
} },
}
const art = new Artplayer(option, function (instance) {
const receiver: Artplayer = this
instance.plugins.runtimeExtension?.update(1)
void receiver
})
const sync: Plugins = art.plugins.add(factory)
const async: Promise<Plugins> = art.plugins.add(async () => ({ name: 'async' }))
const inline = art.plugins.add(function (instance) {
const receiver: Artplayer = this
const value: undefined = instance.backward
void [receiver, value]
return { name: 'inlineAdd' }
})
const same: Plugins = inline
const seek: undefined = art.seek
art.seek = 2
art.seek = '2'
art.notice.show = 'Message'
const shown: boolean = art.notice.show
const pip: Element | null | boolean = art.pip
art.pip = true
const icon: HTMLElement = art.icons.play
const customIcon: HTMLElement | undefined = art.icons.customIcon
const queried: HTMLDivElement | null = art.template.query<HTMLDivElement>('.optional')
const settingItem = { name: 'precise-item', html: 'Item', marker: 1 }
const added: typeof settingItem = art.setting.add(settingItem)
const removed: void = art.setting.remove(settingItem.name)
const subtitleStyle: HTMLDivElement = art.subtitle.style({ color: 'red' })
const target = { count: 1 }
const defined: typeof target = Artplayer.utils.def(target, Symbol('count'), {
get() { return this.count },
})
const debounced = Artplayer.utils.debounce((value: number) => value + 1, 10)
const throttled = Artplayer.utils.throttle((value: number) => value + 1, 10)
const delayed: void = debounced(1)
const limited: void = throttled(1)
const missing = art.setting.find('missing')
if (missing)
missing.html = 'Found'
art.on('runtime-extension:update', count => count.toFixed())
art.on('subtitleAfterUpdate', cues => cues.map(cue => cue.text))
art.plugins.runtimeExtension?.update(2)
const payload: Events['runtime-extension:update'] = [1]
Artplayer.config.events.push('custom-event')
Artplayer.DEBUG = false
// @ts-expect-error defineProperty requires an object target in an installed consumer too.
Artplayer.utils.def(123, 'value', { value: 1 })
declare const legacyFactory: LegacyPluginFactory
const legacyResult: Plugins | Promise<Plugins> = art.plugins.add(legacyFactory)
art.plugins.add(chapter())
art.plugins.legacyRuntimeExtension?.set(2)
art.on('legacy-runtime:update', count => count.toFixed())
const legacyRuntime: Artplayer = new LegacyRuntime(option)
const legacyOptions = new Artplayer({ container: '#legacy', plugins: [legacyFactory] }, function (instance) {
const value: undefined = this.seek
const other: undefined = instance.seek
void [value, other]
})
class Derived extends Artplayer {
marker = true
}
const derived = new Derived(option)
const chained: Derived = derived.on('ready', () => {})
// @ts-expect-error Command properties have no getter value.
const falseSeek: number = art.seek
// @ts-expect-error New control entries require one of the three supported positions.
art.controls.add({ html: 'Missing position' })
// @ts-expect-error Unknown control positions fail runtime validation.
art.controls.add({ html: 'Wrong position', position: 'middle' })
// @ts-expect-error Synchronous registration is not a Promise.
const falsePromise: Promise<Plugins> = sync
// @ts-expect-error Getter visibility is boolean.
const falseNotice: string = art.notice.show
// @ts-expect-error Native PiP returns an element or null, while WebKit returns boolean.
const falsePip: boolean = pip
// @ts-expect-error The PiP setter takes a boolean, not the native getter's element.
art.pip = document.createElement('video')
// @ts-expect-error Icons expose wrapper elements, not their SVG input strings.
const falseIcon: string = art.icons.play
// @ts-expect-error SSR and custom query results may be absent.
const falseQuery: HTMLDivElement = art.template.query<HTMLDivElement>('.optional')
// @ts-expect-error A delayed wrapper does not return the callback's number.
const falseDelayed: number = debounced(1)
// @ts-expect-error A throttled wrapper does not return the callback's number.
const falseLimited: number = throttled(1)
// @ts-expect-error defineProperty returns its target, not a descriptor.
const falseDescriptor: PropertyDescriptor = defined
// @ts-expect-error Precise built-in events reject scalar subtitle payloads.
art.emit('subtitleAfterUpdate', 'bad')
// @ts-expect-error Augmented payloads stay checked.
art.emit('runtime-extension:update', 'bad')
// @ts-expect-error Augmented plugin methods stay checked.
art.plugins.runtimeExtension?.update('bad')
// @ts-expect-error Legacy plugin augmentation keeps its method types on the precise view.
art.plugins.legacyRuntimeExtension?.set('bad')
// @ts-expect-error Legacy event augmentation keeps its payload type on the precise view.
art.emit('legacy-runtime:update', 'bad')
// @ts-expect-error Static env was never exposed by the runtime.
void Artplayer.env
// @ts-expect-error Template html belongs to the constructor.
void art.template.html
// @ts-expect-error Missing setting entries are null, not undefined.
const falseMissing: undefined = art.setting.find('missing')
void [sync, async, same, seek, shown, payload, legacyResult, legacyRuntime, legacyOptions, chained, falseSeek, falsePromise, falseNotice, falseMissing]
void [pip, icon, customIcon, queried, added, removed, subtitleStyle, defined, delayed, limited, falsePip, falseIcon, falseQuery, falseDelayed, falseLimited, falseDescriptor]
+34
View File
@@ -0,0 +1,34 @@
import type { PluginFactory, Plugins } from '../../packages/artplayer/public/runtime/plugin'
import type { IconName, Icons, Template } from '../../packages/artplayer/public/runtime/template'
import type Artplayer from '../../packages/artplayer/src'
import type iconDefaults from '../../packages/artplayer/src/icons/defaults'
declare const source: Artplayer
const plugins: Plugins<Artplayer> = source.plugins
const sync: Plugins<Artplayer> = plugins.add(function (art) {
// eslint-disable-next-line ts/no-this-alias -- Check the public factory's inferred receiver against the actual source instance.
const same: Artplayer = this
return { name: 'sync', same, art }
})
const asyncRegistry: Promise<Plugins<Artplayer>> = plugins.add(async () => ({ name: 'async' }))
declare const unknownFactory: PluginFactory<Artplayer>
const uncertain: Plugins<Artplayer> | Promise<Plugins<Artplayer>> = plugins.add(unknownFactory)
const template: Template<Artplayer> = source.template
const icons: Icons = source.icons
const icon: HTMLElement = icons.play
const customIcon: HTMLElement | undefined = icons.custom
const subtitle: HTMLDivElement | null = template.$subtitle
const query: HTMLVideoElement | null = template.query<HTMLVideoElement>('video')
// @ts-expect-error Sync registration is not a Promise.
const syncPromise: Promise<Plugins<Artplayer>> = plugins.add(() => ({ name: 'plain' }))
// @ts-expect-error Template.html belongs to the constructor, not its instance.
const instanceHtml = template.html
// @ts-expect-error Icon wrappers are not promised to be div elements.
const div: HTMLDivElement = icons.play
// @ts-expect-error Missing custom icon keys require a presence guard.
const present: HTMLElement = icons.custom
type Assert<Condition extends true> = Condition
type AllIcons = Assert<Exclude<keyof typeof iconDefaults, IconName> extends never ? true : false>
type NoInventedIcons = Assert<Exclude<IconName, keyof typeof iconDefaults> extends never ? true : false>
export type { AllIcons, NoInventedIcons }
export { asyncRegistry, customIcon, div, icon, instanceHtml, present, query, subtitle, sync, syncPromise, uncertain }
+31
View File
@@ -0,0 +1,31 @@
import type { Utils } from '../../packages/artplayer/public/runtime/utils'
import * as source from '../../packages/artplayer/src/utils'
const utils: Utils = source
const target = { enabled: true }
const same: typeof target = utils.def(target, 'value', { value: 1 })
const delayed: void = utils.debounce(() => 123, 0, {})()
const throttled: void = utils.throttle(() => 456, 0)()
const receiver = { count: 0 }
const callback = utils.debounce(function (this: typeof receiver, increment: number) {
this.count += increment
}, 0)
callback.call(receiver, 2)
const append: Element | ChildNode | null = utils.append(document.createElement('div'), '')
const style: number | string = utils.getStyle(document.body, 'width', Math.random() > 0.5)
const queried: HTMLVideoElement | null = utils.query<HTMLVideoElement>('video', document.createDocumentFragment())
const removed: Text = utils.remove(document.createTextNode('text'))
utils.sleep()
// @ts-expect-error The callback result is not returned by the debounce wrapper.
const immediate: number = utils.debounce(() => 1, 0)()
// @ts-expect-error Native defineProperty requires a descriptor object.
utils.def(target, 'value', 123)
// @ts-expect-error Native defineProperty also requires an object target.
utils.def(123, 'value', { value: 1 })
// @ts-expect-error Optional event targets do not make composed paths optional arrays.
const nullable: null = utils.getComposedPath({ target: null })
type Assert<Condition extends true> = Condition
type Complete = Assert<Exclude<keyof typeof source, keyof Utils> extends never ? true : false>
type NoInventedExports = Assert<Exclude<keyof Utils, keyof typeof source> extends never ? true : false>
export type { Complete, NoInventedExports }
export { append, delayed, immediate, nullable, queried, removed, same, style, throttled }