fix(thumbnail): [PKG-TOOL-THUMB-04] supply public types and installed module entries

This commit is contained in:
Harvey Zhao committed 2026-09-13 05:16:36 +08:00
1 parent 15485b6420
commit 3bff236145
33 files changed
+2466 -164

No files matched your search

+1
View File
@@ -70,6 +70,7 @@
'./assets/ts/artplayer-proxy-canvas.d.ts',
'./assets/ts/artplayer-proxy-mediabunny.d.ts',
'./assets/ts/artplayer-tool-iframe.d.ts',
'./assets/ts/artplayer-tool-thumbnail.d.ts',
'./assets/ts/artplayer.d.ts',
'./assets/ts/artplayer-i18n.d.ts',
]
+210 -104
View File
@@ -1,109 +1,215 @@
interface Option {
m3u8?: {
quality?: {
control?: boolean
setting?: boolean
title?: string
auto?: string
getName?: (level: {
id: number
index: number
name: null | string
height: number
bitrate: number
}) => string
}
audio?: {
control?: boolean
setting?: boolean
title?: string
auto?: string
getName?: (track: {
id: number
index: number
name: null | string
lang: string
language: string
bitrate: number
}) => string
// Generated from the package public declaration by yarn build:ts. Do not edit.
/* eslint-disable ts/no-redeclare -- Callable and public type namespace intentionally merge. */
declare namespace artplayerProxyMediabunnyDefinitions {
export interface Option {
m3u8?: {
quality?: {
control?: boolean
setting?: boolean
title?: string
auto?: string
getName?: (level: {
id: number
index: number
name: null | string
height: number
bitrate: number
}) => string
}
audio?: {
control?: boolean
setting?: boolean
title?: string
auto?: string
getName?: (track: {
id: number
index: number
name: null | string
lang: string
language: string
bitrate: number
}) => string
}
}
/**
* Timeout for loading media in milliseconds
* @default 0
*/
loadTimeout?: number
/**
* Interval for timeupdate events in milliseconds
* @default 250
*/
timeupdateInterval?: number
/**
* Audio-video synchronization tolerance in seconds
* @default 0.12
*/
avSyncTolerance?: number
/**
* Whether to drop late video frames
* @default false
*/
dropLateFrames?: boolean
/**
* Poster image URL
*/
poster?: string
/**
* Media source (URL, Blob, or ReadableStream)
*/
source?: string | Blob | ReadableStream<Uint8Array>
/**
* Check if server supports range requests before loading
* @default false
*/
preflightRange?: boolean
/**
* Initial volume (0-1)
* @default 0.7
*/
volume?: number
/**
* Initial muted state
* @default false
*/
muted?: boolean
/**
* Autoplay
* @default false
*/
autoplay?: boolean
/**
* Loop playback
* @default false
*/
loop?: boolean
/**
* Cross-origin setting
*/
crossOrigin?: string
}
/**
* Timeout for loading media in milliseconds
* @default 0
*/
loadTimeout?: number
/**
* Interval for timeupdate events in milliseconds
* @default 250
*/
timeupdateInterval?: number
/**
* Audio-video synchronization tolerance in seconds
* @default 0.12
*/
avSyncTolerance?: number
/**
* Whether to drop late video frames
* @default false
*/
dropLateFrames?: boolean
/**
* Poster image URL
*/
poster?: string
/**
* Media source (URL, Blob, or ReadableStream)
*/
source?: string | Blob | ReadableStream<Uint8Array>
/**
* Check if server supports range requests before loading
* @default false
*/
preflightRange?: boolean
/**
* Initial volume (0-1)
* @default 0.7
*/
volume?: number
/**
* Initial muted state
* @default false
*/
muted?: boolean
/**
* Autoplay
* @default false
*/
autoplay?: boolean
/**
* Loop playback
* @default false
*/
loop?: boolean
/**
* Cross-origin setting
*/
crossOrigin?: string
export type Result = HTMLCanvasElement
export interface HlsLevel {
id: number
index: number
name: string | null
height: number
bitrate: number
/** SDK object; narrow with the SDK version used by your application. */
track: unknown
}
export interface HlsAudio {
id: number
index: number
name: string | null
lang: string
language: string
bitrate: number
/** SDK object; narrow with the SDK version used by your application. */
track: unknown
}
export interface HlsState {
levels: HlsLevel[]
audios: HlsAudio[]
currentLevel: HlsLevel | null
currentAudio: HlsAudio | null
videoMode: 'auto' | 'manual'
audioMode: 'auto' | 'manual'
}
/** Historical RAF estimates, not decoded-frame or network timing measurements. */
export interface SyntheticFrameMetadata {
/** Historical media time in seconds, unlike the native video API. */
presentationTime: number
expectedDisplayTime: number
width: number
height: number
mediaTime: number
presentedFrames: number
processingDuration: number
captureTime: number
receiveTime: number
rtpTimestamp: number
}
export type SyntheticFrameCallback = (now: number, metadata: SyntheticFrameMetadata) => void
export type MediaListener = (event: Event & {
detail: unknown
}) => unknown
/** Opt-in media surface of art.mediabunny; decoder internals are not part of this view. */
export interface MediaBunnyShim {
canvas: HTMLCanvasElement
/** Runtime also accepts SDK sources; the default Option keeps its historical input union. */
src: unknown
readonly currentSrc: unknown
currentTime: number
readonly duration: number
/** Synthetic full-duration/current-time ranges, not measured network buffers. */
readonly buffered: TimeRanges
readonly played: TimeRanges
readonly seekable: TimeRanges
readonly paused: boolean
readonly playing: boolean
readonly ended: boolean
readonly seeking: boolean
readonly readyState: number
readonly networkState: number
readonly error: {
code: number
message: string
} | null
volume: number
muted: boolean
playbackRate: number
readonly videoWidth: number
readonly videoHeight: number
poster: string
/** The following setters are inert; use Option for autoplay/loop/crossOrigin. */
autoplay: boolean
loop: boolean
controls: boolean
playsInline: boolean
crossOrigin: string
preload: string
defaultMuted: boolean
defaultPlaybackRate: number
play: () => Promise<void>
pause: () => void
load: () => void
/** Always returns "maybe"; it does not probe codec/browser support. */
canPlayType: (type: string) => 'maybe'
getM3u8State: () => Promise<HlsState | null>
switchM3u8Quality: (value: unknown) => Promise<void>
switchM3u8Audio: (value: unknown) => Promise<void>
createTimeRanges: (start: number, end: number) => TimeRanges
requestVideoFrameCallback: (callback: SyntheticFrameCallback) => number
cancelVideoFrameCallback: (id: number) => void
addEventListener: (type: string, listener: MediaListener) => void
removeEventListener: (type: string, listener: MediaListener) => void
getBoundingClientRect: () => DOMRect
setAttribute: (name: string, value: unknown) => void
destroy: () => void
}
/** Native Canvas methods win collisions, including DOM event and attribute methods. */
export type MediaBunnyCanvas = HTMLCanvasElement & Omit<MediaBunnyShim, keyof HTMLCanvasElement>
/** The alias is installed by the proxy and removed on player destruction. */
export type MediaBunnyPlayer = Artplayer & {
mediabunny?: MediaBunnyShim
}
export const artplayerProxyMediabunny: (option?: Option) => (art: Artplayer) => Result
}
declare const artplayerProxyMediabunny: typeof artplayerProxyMediabunnyDefinitions.artplayerProxyMediabunny
declare namespace artplayerProxyMediabunny {
export type Option = artplayerProxyMediabunnyDefinitions.Option
export type Result = artplayerProxyMediabunnyDefinitions.Result
export type HlsLevel = artplayerProxyMediabunnyDefinitions.HlsLevel
export type HlsAudio = artplayerProxyMediabunnyDefinitions.HlsAudio
export type HlsState = artplayerProxyMediabunnyDefinitions.HlsState
export type SyntheticFrameMetadata = artplayerProxyMediabunnyDefinitions.SyntheticFrameMetadata
export type SyntheticFrameCallback = artplayerProxyMediabunnyDefinitions.SyntheticFrameCallback
export type MediaListener = artplayerProxyMediabunnyDefinitions.MediaListener
export type MediaBunnyShim = artplayerProxyMediabunnyDefinitions.MediaBunnyShim
export type MediaBunnyCanvas = artplayerProxyMediabunnyDefinitions.MediaBunnyCanvas
export type MediaBunnyPlayer = artplayerProxyMediabunnyDefinitions.MediaBunnyPlayer
}
type Result = HTMLCanvasElement
declare const artplayerProxyMediabunny: (option?: Option) => (art: Artplayer) => Result
export default artplayerProxyMediabunny
export = artplayerProxyMediabunny
export as namespace artplayerProxyMediabunny;
+95
View File
@@ -0,0 +1,95 @@
// Generated from the package public declaration by yarn build:ts. Do not edit.
/** Extract PNG thumbnail sheets from a locally selected video file. */
declare class ArtplayerToolThumbnail {
constructor(option?: ArtplayerToolThumbnail.Option)
static readonly DEFAULTS: ArtplayerToolThumbnail.SheetOptions
static ondragover(event: DragEvent): void
static creatVideo(): HTMLVideoElement
processing: boolean
option: ArtplayerToolThumbnail.ResolvedOption
video: HTMLVideoElement
duration: number
density: number | undefined
file: File | undefined
videoUrl: string | undefined
thumbnailUrl: string | undefined
e?: ArtplayerToolThumbnail.EventRegistry
setup(option?: ArtplayerToolThumbnail.Option): this
inputChange(event: Event): void
ondrop(event: DragEvent): void
loadVideo(file?: File | null): void
/** Ready-metadata preflight may throw synchronously. Cancellation rejects with AbortError. */
start(): Promise<void>
creatScreenshotDate(): ArtplayerToolThumbnail.ScreenshotPoint[]
creatCanvas(): HTMLCanvasElement
download(): this
errorHandle(condition: unknown, message: string): void
destroy(): void
on<Name extends PropertyKey, Custom extends unknown[], Context>(name: Name, callback: (this: Context, ...args: ArtplayerToolThumbnail.EventArgs<Name, Custom>) => unknown, ctx?: Context): this
once<Name extends PropertyKey, Custom extends unknown[], Context>(name: Name, callback: (this: Context, ...args: ArtplayerToolThumbnail.EventArgs<Name, Custom>) => unknown, ctx?: Context): this
emit<Name extends PropertyKey, Custom extends unknown[]>(name: Name, ...args: ArtplayerToolThumbnail.EventArgs<Name, Custom>): this
off<Name extends PropertyKey, Custom extends unknown[]>(name: Name, callback?: ArtplayerToolThumbnail.Listener<ArtplayerToolThumbnail.EventArgs<Name, Custom>>): this
}
declare namespace ArtplayerToolThumbnail {
interface SheetOptions {
number: number
width: number
height: number
column: number
begin: number
end: number
}
/** fileInput must be a file input or an Element wrapper when constructing. */
interface Option extends Partial<SheetOptions> {
fileInput?: Element
[name: string]: unknown
}
interface ResolvedOption extends SheetOptions {
fileInput: HTMLInputElement
[name: string]: unknown
}
interface ScreenshotPoint {
time: number
x: number
y: number
}
interface Events {
file: [
file: File,
]
video: [
video: HTMLVideoElement,
]
canvas: [
canvas: HTMLCanvasElement,
]
update: [
url: string,
progress: number,
]
done: [
]
download: [
name: string,
]
/** Built-in failures are strings; user callbacks may throw any message value. */
error: [
message: unknown,
]
destroy: [
]
}
type EventArgs<Name extends PropertyKey, Custom extends unknown[] = unknown[]> = Name extends keyof Events ? [
...Events[Name],
] : Custom
type Listener<Args extends unknown[]> = ((...args: Args) => unknown) & {
_?: (...args: Args) => unknown
}
/** Heterogeneous listener storage; dispatch through emit to retain event argument checks. */
type EventRegistry = Partial<Record<PropertyKey, {
fn: Listener<never[]>
ctx: unknown
}[]>>
}
export = ArtplayerToolThumbnail
export as namespace ArtplayerToolThumbnail;
+2 -1
View File
@@ -83,7 +83,8 @@
"generate:media:hls": "node scripts/generate-hls-fixture.mjs",
"test:iframe": "node --test test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe.test.js test/iframe-lifecycle.test.js refactor/scripts/iframe-contract.test.mjs refactor/scripts/iframe-types.test.mjs",
"test:iframe-types-package": "node refactor/scripts/iframe-package-types.mjs",
"test:thumbnail": "node --test test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js refactor/scripts/thumbnail-contract.test.mjs"
"test:thumbnail": "node --test test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js refactor/scripts/thumbnail-contract.test.mjs refactor/scripts/thumbnail-types.test.mjs",
"test:thumbnail-types-package": "node refactor/scripts/thumbnail-package-types.mjs"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
@@ -1,8 +1,9 @@
# Thumbnail tool maintenance map
The runtime responsibilities were separated under PKG-TOOL-THUMB-03 and are now
strict TypeScript under PKG-TOOL-THUMB-04. Published declarations and installed
entrypoint checks are still in progress; runtime migration is not release readiness.
strict TypeScript under PKG-TOOL-THUMB-04. Public declarations and isolated installed
entrypoints now have checks; the historical default-policy decision and final
integration/release gates remain open.
| File | Responsibility |
| --- | --- |
@@ -15,6 +16,7 @@ entrypoint checks are still in progress; runtime migration is not release readin
| src/emitter.ts | Typed local adaptation of tiny-emitter; preserves on/once/emit/off behavior |
| src/utils.ts | Pure clamp and filename helpers; unused sleep/serial helpers removed |
| src/types.ts | Internal option, frame, event tuple, job and lifecycle contracts; no runtime output |
| types/artplayer-tool-thumbnail.d.ts | Public class/namespace; d.cts/d.mts wrappers share its identity |
The entry delegates to input/source/extraction/sheet. Helpers import its type only;
the imports are erased and do not create a runtime dependency on the entry.
@@ -100,7 +102,8 @@ Safari/WebKit evidence in 05. The independent tool example is
callback failures, reentrancy and private resources. Browser tests hold real PNG
callbacks and replace a selected file during encoding; only the latest job may
complete. Continue published declarations and installed consumers in 04; preserve
old tests and the default delay/height boundary.
old tests and the default delay/height boundary. The [public type guide](types/README.md)
documents declaration ownership, module entries, events and installed consumers.
See [task plan](../../refactor/plan.md) and [risk ledger](../../refactor/risks.json).
## Type and provenance boundaries
@@ -110,9 +113,12 @@ strict, noUncheckedIndexedAccess, noImplicitOverride, skipLibCheck=false and no
ambient Node/test globals. Public fields use declare so TypeScript does not add
early undefined properties or change constructor property order. File/URL/density
fields retain their absence until the corresponding operation publishes them.
Unknown option fields remain unknown, and custom string/number/symbol events keep
an open payload boundary; built-in events have precise tuples. This source typing
is not yet the missing public npm declaration entrypoint.
Unknown option fields remain unknown. Custom string/number/symbol events infer
caller-defined callback tuples; built-in events have precise tuples. Their
heterogeneous registry erases tuple types at storage and applies a local assertion
when dispatching. Public and source constructors are checked in both directions.
Error payloads remain unknown because callbacks can throw arbitrary message
values. This does not change runtime error delivery.
Assertions are limited to existing runtime boundaries: a temporary empty option
object before setup validation; the input/wrapper conversion; event target and
@@ -129,7 +135,8 @@ The emitter structurally corresponds to tiny-emitter 2.1.0, which the recovered
The pinned [reference files](../../refactor/baselines/thumbnail-vendor/sources.json)
and [MIT notice](THIRD_PARTY_NOTICES) retain upstream attribution. The repository
build includes the complete notice in main, legacy and ESM bundle headers.
Final packed contents remain a distribution gate. Do not replace this local
Current packed contents include the complete notice and no implementation source;
new release candidates must repeat the installed check. Do not replace this local
emitter with the core emitter as part of a type-only change: dispatch semantics
and historical prototype-key handling need their own behavior review.
+35 -6
View File
@@ -19,11 +19,40 @@
"player"
],
"exports": {
"import": "./dist/artplayer-tool-thumbnail.esm.js",
"require": "./dist/artplayer-tool-thumbnail.js",
"default": "./dist/artplayer-tool-thumbnail.js"
".": {
"import": {
"types": "./types/artplayer-tool-thumbnail.d.mts",
"default": "./dist/artplayer-tool-thumbnail.mjs"
},
"require": {
"types": "./types/artplayer-tool-thumbnail.d.cts",
"default": "./dist/artplayer-tool-thumbnail.js"
},
"default": "./dist/artplayer-tool-thumbnail.js"
},
"./legacy": {
"types": "./types/artplayer-tool-thumbnail.d.cts",
"import": "./dist/artplayer-tool-thumbnail.legacy.js",
"require": "./dist/artplayer-tool-thumbnail.legacy.js",
"default": "./dist/artplayer-tool-thumbnail.legacy.js"
}
},
"module": "dist/artplayer-tool-thumbnail.esm.js",
"types": "types/artplayer-tool-thumbnail.d.ts",
"browserslist": "last 1 Chrome version"
"main": "./dist/artplayer-tool-thumbnail.js",
"module": "./dist/artplayer-tool-thumbnail.mjs",
"types": "./types/artplayer-tool-thumbnail.d.ts",
"browserslist": "last 1 Chrome version",
"typesVersions": {
"*": {
"legacy": [
"types/artplayer-tool-thumbnail.d.ts"
]
}
},
"files": [
"ARCHITECTURE.md",
"README.md",
"THIRD_PARTY_NOTICES",
"dist",
"types"
]
}
@@ -1,26 +1,27 @@
// Locally adapted from tiny-emitter; upstream reference 2.1.0. See THIRD_PARTY_NOTICES.
type EventMap<Events> = { [Name in keyof Events]: readonly unknown[] }
type EventArgs<Events extends EventMap<Events>, Name extends PropertyKey, Custom extends unknown[]> = Name extends keyof Events ? [...Events[Name]] : Custom
type Listener<Args extends unknown[]> = ((...args: Args) => unknown) & { _?: (...args: Args) => unknown }
interface Registration<Args extends unknown[]> {
fn: Listener<Args>
ctx: unknown
}
type Registry<Events extends EventMap<Events>> = { [Name in keyof Events]?: Registration<[...Events[Name]]>[] }
type Registry = Partial<Record<PropertyKey, Registration<never[]>[]>>
export default class Emitter<Events extends EventMap<Events> = Record<PropertyKey, unknown[]>> {
declare e?: Registry<Events>
declare e?: Registry
on<Name extends keyof Events, Context>(name: Name, fn: (this: Context, ...args: [...Events[Name]]) => unknown, ctx?: Context): this {
const e: Registry<Events> = this.e || (this.e = {});
(e[name] || (e[name] = [])).push({ fn, ctx })
on<Name extends PropertyKey, Custom extends unknown[], Context>(name: Name, fn: (this: Context, ...args: EventArgs<Events, Name, Custom>) => unknown, ctx?: Context): this {
const e: Registry = this.e || (this.e = {});
(e[name] || (e[name] = [] as Registration<never[]>[])).push({ fn, ctx })
return this
}
once<Name extends keyof Events, Context>(name: Name, fn: (this: Context, ...args: [...Events[Name]]) => unknown, ctx?: Context): this {
once<Name extends PropertyKey, Custom extends unknown[], Context>(name: Name, fn: (this: Context, ...args: EventArgs<Events, Name, Custom>) => unknown, ctx?: Context): this {
// eslint-disable-next-line ts/no-this-alias -- Preserve the owning emitter independently of callback ctx.
const self = this
const callback: Listener<[...Events[Name]]> = fn
function listener(...args: [...Events[Name]]) {
const callback: Listener<EventArgs<Events, Name, Custom>> = fn
function listener(...args: EventArgs<Events, Name, Custom>) {
self.off(name, listener)
callback.apply(ctx, args)
}
@@ -28,19 +29,20 @@ export default class Emitter<Events extends EventMap<Events> = Record<PropertyKe
return this.on(name, listener, ctx)
}
emit<Name extends keyof Events>(name: Name, ...data: [...Events[Name]]): this {
const e: Registry<Events> = this.e || (this.e = {})
emit<Name extends PropertyKey, Custom extends unknown[]>(name: Name, ...data: EventArgs<Events, Name, Custom>): this {
const e: Registry = this.e || (this.e = {})
const evtArr = (e[name] || []).slice()
for (let i = 0; i < evtArr.length; i += 1) {
evtArr[i]!.fn.apply(evtArr[i]!.ctx, data)
// The registry erases heterogeneous listener tuples; emit checks its own payload.
(evtArr[i]!.fn as Listener<typeof data>).apply(evtArr[i]!.ctx, data)
}
return this
}
off<Name extends keyof Events>(name: Name, callback?: Listener<[...Events[Name]]>): this {
const e: Registry<Events> = this.e || (this.e = {})
off<Name extends PropertyKey, Custom extends unknown[]>(name: Name, callback?: Listener<EventArgs<Events, Name, Custom>>): this {
const e: Registry = this.e || (this.e = {})
const evts = e[name]
const liveEvents: Registration<[...Events[Name]]>[] = []
const liveEvents: Registration<never[]>[] = []
if (evts && callback) {
for (let i = 0, len = evts.length; i < len; i += 1) {
if (evts[i]!.fn !== callback && evts[i]!.fn._ !== callback)
@@ -75,7 +75,7 @@ function createJob(tool: ArtplayerToolThumbnail, state: LifecycleState) {
}
if (report) {
try {
tool.emit('error', (error as { message?: string } | null | undefined)?.message)
tool.emit('error', (error as { message?: unknown } | null | undefined)?.message)
}
catch (listenerError) {
error = listenerError
@@ -94,7 +94,7 @@ function createJob(tool: ArtplayerToolThumbnail, state: LifecycleState) {
catch (error) {
let failure = error
try {
tool.emit('error', (error as { message?: string } | null | undefined)?.message)
tool.emit('error', (error as { message?: unknown } | null | undefined)?.message)
}
catch (listenerError) {
failure = listenerError
@@ -23,14 +23,14 @@ export interface ScreenshotPoint {
y: number
}
export interface ThumbnailEvents extends Record<PropertyKey, unknown[]> {
export interface ThumbnailEvents {
file: [file: File]
video: [video: HTMLVideoElement]
canvas: [canvas: HTMLCanvasElement]
update: [url: string, progress: number]
done: []
download: [name: string]
error: [message: string | undefined]
error: [message: unknown]
destroy: []
}
@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "allowJs": false },
"include": ["src/**/*.ts"]
"include": ["src/**/*.ts", "types/**/*.d.ts"]
}
@@ -0,0 +1,60 @@
# Public thumbnail types
The declaration describes the existing tool class, not an ArtPlayer plugin factory.
The previous workspace manifest pointed to a missing declaration. There is no
recovered legacy declaration to claim as an exact TypeScript baseline.
| Entry | Declaration | Runtime |
| --- | --- | --- |
| Root ESM import | artplayer-tool-thumbnail.d.mts | Default constructor in dist/artplayer-tool-thumbnail.mjs |
| Root require | artplayer-tool-thumbnail.d.cts | Direct constructor in dist/artplayer-tool-thumbnail.js |
| Older Node resolution / TS 4.3 | artplayer-tool-thumbnail.d.ts | main points to the direct CommonJS constructor |
| artplayer-tool-thumbnail/legacy | d.cts or typesVersions fallback | Direct legacy constructor; native ESM gets its default |
| Browser script / editor | Generated docs/assets/ts declaration | ArtplayerToolThumbnail global |
The d.ts file is the public source of truth. d.cts forwards its export assignment;
d.mts exposes the default constructor and type-only named exports. No runtime
named exports or .default self-alias exist. Keep the wrappers sharing one class.
```ts
import Thumbnail, { Option } from 'artplayer-tool-thumbnail'
const input = document.createElement('input')
input.type = 'file'
const options: Option = { fileInput: input, number: 10 }
const tool = new Thumbnail(options)
tool.on('update', (url, progress) => console.log(url, progress))
tool.on('custom', (value: number) => console.log(value))
tool.emit('custom', 1)
```
CommonJS TS can use `import Thumbnail = require('artplayer-tool-thumbnail')` without
esModuleInterop. The optional constructor argument mirrors the JS signature, but
successful initialization requires fileInput to be an actual file input or Element
wrapper. Missing input throws synchronously. setup accepts partial options and
retains unknown fields. Numeric range/clamp validation remains a runtime condition.
Known event tuples are precise. Custom string/number/symbol events infer annotated
callback arguments; those protocols remain the caller's responsibility. ctx is
inferred from the optional third argument. Dispatch through emit to retain event
argument checks rather than directly invoking the heterogeneous e registry.
file/videoUrl/thumbnailUrl/density may be absent before their operation.
start returns Promise<void>; ready-metadata preflight may throw synchronously,
and source replacement/destroy reject with AbortError. Error events usually contain
strings, but callback throws can supply any message value, so narrow unknown first.
Source/declaration constructors are mutually checked in test/types/thumbnail-source.ts.
Current and TS 4.3.5 consumers include ten invalid uses. Installed checks pack/install
outside the workspace, offline with an unchanged frozen lock, verify every file
and exercise native Node require/import without a DOM.
```sh
yarn test:thumbnail
yarn test:thumbnail-types-package
yarn build:ts
yarn test:browser test/browser/thumbnail-editor-types.spec.js
```
build:ts selects package declaration entries rather than auxiliary types and emits
the uppercase global. Actual file extraction, old-core composition and the full
demo remain separate checks. The 3.5.31 versus 4.4.0 default policy remains pending.
@@ -0,0 +1,4 @@
// eslint-disable-next-line ts/no-require-imports -- Match the direct CommonJS constructor without a self-default alias.
import ArtplayerToolThumbnail = require('./artplayer-tool-thumbnail.js')
export = ArtplayerToolThumbnail
@@ -0,0 +1,2 @@
export { default } from './artplayer-tool-thumbnail.cjs'
export type { EventArgs, EventRegistry, Events, Listener, Option, ResolvedOption, ScreenshotPoint, SheetOptions } from './artplayer-tool-thumbnail.js'
@@ -0,0 +1,83 @@
/** Extract PNG thumbnail sheets from a locally selected video file. */
declare class ArtplayerToolThumbnail {
constructor(option?: ArtplayerToolThumbnail.Option)
static readonly DEFAULTS: ArtplayerToolThumbnail.SheetOptions
static ondragover(event: DragEvent): void
static creatVideo(): HTMLVideoElement
processing: boolean
option: ArtplayerToolThumbnail.ResolvedOption
video: HTMLVideoElement
duration: number
density: number | undefined
file: File | undefined
videoUrl: string | undefined
thumbnailUrl: string | undefined
e?: ArtplayerToolThumbnail.EventRegistry
setup(option?: ArtplayerToolThumbnail.Option): this
inputChange(event: Event): void
ondrop(event: DragEvent): void
loadVideo(file?: File | null): void
/** Ready-metadata preflight may throw synchronously. Cancellation rejects with AbortError. */
start(): Promise<void>
creatScreenshotDate(): ArtplayerToolThumbnail.ScreenshotPoint[]
creatCanvas(): HTMLCanvasElement
download(): this
errorHandle(condition: unknown, message: string): void
destroy(): void
on<Name extends PropertyKey, Custom extends unknown[], Context>(name: Name, callback: (this: Context, ...args: ArtplayerToolThumbnail.EventArgs<Name, Custom>) => unknown, ctx?: Context): this
once<Name extends PropertyKey, Custom extends unknown[], Context>(name: Name, callback: (this: Context, ...args: ArtplayerToolThumbnail.EventArgs<Name, Custom>) => unknown, ctx?: Context): this
emit<Name extends PropertyKey, Custom extends unknown[]>(name: Name, ...args: ArtplayerToolThumbnail.EventArgs<Name, Custom>): this
off<Name extends PropertyKey, Custom extends unknown[]>(name: Name, callback?: ArtplayerToolThumbnail.Listener<ArtplayerToolThumbnail.EventArgs<Name, Custom>>): this
}
declare namespace ArtplayerToolThumbnail {
interface SheetOptions {
number: number
width: number
height: number
column: number
begin: number
end: number
}
/** fileInput must be a file input or an Element wrapper when constructing. */
interface Option extends Partial<SheetOptions> {
fileInput?: Element
[name: string]: unknown
}
interface ResolvedOption extends SheetOptions {
fileInput: HTMLInputElement
[name: string]: unknown
}
interface ScreenshotPoint {
time: number
x: number
y: number
}
interface Events {
file: [file: File]
video: [video: HTMLVideoElement]
canvas: [canvas: HTMLCanvasElement]
update: [url: string, progress: number]
done: []
download: [name: string]
/** Built-in failures are strings; user callbacks may throw any message value. */
error: [message: unknown]
destroy: []
}
type EventArgs<Name extends PropertyKey, Custom extends unknown[] = unknown[]> = Name extends keyof Events ? [...Events[Name]] : Custom
type Listener<Args extends unknown[]> = ((...args: Args) => unknown) & { _?: (...args: Args) => unknown }
/** Heterogeneous listener storage; dispatch through emit to retain event argument checks. */
type EventRegistry = Partial<Record<PropertyKey, { fn: Listener<never[]>, ctx: unknown }[]>>
}
export = ArtplayerToolThumbnail
export as namespace ArtplayerToolThumbnail
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,63 @@
# PKG-TOOL-THUMB-04 public types and installed entry checkpoint
Task remains doing while THUMB-COMPAT-01's default policy is pending. Starts from
15485b6420dece2b04503bd4fa99a9da0f9c8ba0. Public declarations and installed consumption
are implemented; this does not declare package release or full refactor readiness.
## Public contracts and distribution
The missing d.ts entry is replaced with a CommonJS class/namespace declaration,
paired d.cts/d.mts wrappers and type-only names. Root require/default keeps the
direct constructor. Root import/module targets the actually generated .mjs, main
is explicit, and the new legacy subpath includes old TS resolution. No .default
self-alias is invented. Existing frozen dist files remain packed. The frozen
.esm.js target was absent; fixing root import does not restore a nonexistent file.
Historical 3.5.31 CSS disposition remains a 06 question under BASE-DIST-01.
A files allowlist includes dist, declarations, README, architecture and full MIT
notices while excluding implementation/config/dependencies. No new dependency,
lockfile, runtime target, version or package-manager change is introduced.
Known tuples, ctx, coordinates, partial options, lazy fields, returns and old
method spellings are declared. Custom event callbacks infer application tuples
without any. Error payloads are unknown because arbitrary throws produce numeric,
object or absent message values; a new runtime test verifies delivery and identical
Promise rejection, including primitive/null throws. Source changes since the prior
checkpoint are erased types; all final built bundles are compared byte-for-byte.
## Editor generator issue found and fixed
Full build:ts exposed an existing regression: MediaBunny's auxiliary media.d.ts was
globbed into an invalid standalone media global, while the proxy declaration kept
an unresolved ./media re-export. Generation now selects package-named entries.
Explicit local type re-exports require supplied complete matching type sources;
missing/renamed/partial definitions fail rather than disappearing. Callable
variables and CommonJS classes preserve namespaces. MediaBunny's source declarations
are unchanged; its editor output now includes opt-in media types. Thumbnail's exact
uppercase global is generated and added to the real docs library list.
Tests reproduce the old missing-relative-type diagnostic, compile the new types
with current/old TS and reject incomplete inputs. The real Monaco worker consumes
core/Thumbnail/MediaBunny declarations, rejects invalid selector input, emits and
runs the Thumbnail constructor, custom event and repeated native DOM cleanup.
This is not a file extraction test.
## Evidence and continuation
The installed runner creates a fixture from frozen Git inputs and a candidate Yarn
tarball. The former is explicitly not an original npm archive. Frozen require
works, import reports ERR_MODULE_NOT_FOUND and strict compilers report missing
entry/declarations. Candidate installation is outside the workspace, offline with
an unchanged frozen lock and verification of every installed byte. Current/old TS,
no-interop CJS, root/legacy runtime and complete notices are covered. Final evidence
distinguishes exploratory and final packs after package documentation changes.
The pending user question describes the incompatible defaults: published recovered
3.5.31 delay/fixed height versus unpublished workspace 4.4.0 synchronous video and
aspect-derived height. No answer is inferred from elapsed time and these defaults
remain unchanged. Continue the chosen policy/regressions before closing 04.
Inherited emitter event-name handling still needs behavior review; 05/06 retain
full demo/core combinations and supported Safari/WebKit file extraction.
Revert this checkpoint to undo declaration/entry/generator changes, then run normal
builds. Preserve prior 03 lifecycle fixes and upstream attribution. No push, tag,
npm publication or hosted CI result is claimed.
+1 -1
View File
@@ -538,5 +538,5 @@
- PKG-TOOL-THUMB-01: [记录](baselines/thumbnail-release.json) [记录](baselines/thumbnail-contract.md) [记录](changes/2026-09-13-PKG-TOOL-THUMB-01-contract.md) [记录](baselines/thumbnail-contract-validation.json)
- PKG-TOOL-THUMB-02: [记录](baselines/thumbnail-behavior-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-02-behavior.md)
- PKG-TOOL-THUMB-03: [记录](changes/2026-09-13-PKG-TOOL-THUMB-03-input-checkpoint.md) [记录](baselines/thumbnail-input-checkpoint.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md) [记录](baselines/thumbnail-lifecycle-validation.json)
- PKG-TOOL-THUMB-04: [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-runtime-types.md) [记录](baselines/thumbnail-runtime-types-validation.json)
- PKG-TOOL-THUMB-04: [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-runtime-types.md) [记录](baselines/thumbnail-runtime-types-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-public-types.md) [记录](baselines/thumbnail-public-types-validation.json)
- PKG-FACTORY-01: [记录](baselines/factory-assignment-gaps.json) [记录](baselines/factory-compatibility-proposals.json) [记录](factory-compatibility-decision.md) [记录](changes/2026-09-12-PKG-FACTORY-01-decision.md)
+14
View File
@@ -1,5 +1,19 @@
# 进度与证据
## PKG-TOOL-THUMB-04 公开类型与真实安装检查点(doing)
补齐class/namespace及d.cts/d.mts,修复root ESM并保留直接CJS构造器,新增legacy类型路径。
仓库外实际安装验证7种候选编译模式、52项反例和全部文件;冻结Git夹具保留5种缺声明/
入口失败对照。明确该夹具不是原始npm包。完整MIT通知在实际tarball验证,相关来源风险关闭。
完整build:ts发现并修复MediaBunny辅助类型被误当独立插件的既有问题;原不支持runtime
re-export的错误提示在首次CI发现变化后恢复,未削弱测试。三浏览器真实Monaco通过,
同时消费核心/Thumbnail/MediaBunny类型并执行工具构造/清理。最终CI1658、339生产TS、
3导入通过;main/legacy各49项,运行文件和源码UMD与上个检查点完全一致,171项抽帧证据
按哈希复用并明确没有重跑。用户默认行为策略问题仍待答复,04不标完成;05/06继续设备/
完整demo/core与历史CSS分发。见[变更](changes/2026-09-13-PKG-TOOL-THUMB-04-public-types.md)
和[验证](baselines/thumbnail-public-types-validation.json)。222项:112 done、11 doing、99 todo。
## PKG-TOOL-THUMB-04 运行时 TS 与 emitter 来源检查点(doing)
8个运行时模块已迁为严格TS,新增类型模块明确option/事件/帧/job/资源状态;declare保留
+2 -2
View File
@@ -179,6 +179,6 @@
| IFRAME-DIST-01 | open / 已复现 | Iframe tool name is unavailable at registry observation and old archive includes distinct helper exports | PKG-IFRAME-04, PKG-IFRAME-06 |
| THUMB-COMPAT-01 | open / 已复现 | Thumbnail recovered 3.5.31 and workspace 4.4.0 have conflicting height and event timing defaults | PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-05 |
| THUMB-LIFE-01 | open / 已复现 | Thumbnail drop binding and repeated destroy fail; pending work and replacement resource ownership need repair | PKG-TOOL-THUMB-02, PKG-TOOL-THUMB-03, PKG-TOOL-THUMB-05 |
| THUMB-PROVENANCE-01 | open / 源码/产物事实 | Thumbnail local emitter attribution and historical distribution completeness remain unverified | PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-06 |
| THUMB-PROVENANCE-01 | resolved / 源码/产物事实 | Thumbnail local emitter attribution and historical distribution completeness remain unverified | PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-06 |
| THUMB-MEDIA-01 | open / 已复现 | Windows WebKit native Blob URLs cannot load tested MP4 media despite successful HTTP controls | PKG-TOOL-THUMB-05, PKG-TOOL-THUMB-06 |
| VENDOR-09 | open / 源码/产物事实 | Thumbnail tiny-emitter 改写来源与最终分发许可 | PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-06 |
| VENDOR-09 | resolved / 源码/产物事实 | Thumbnail tiny-emitter 改写来源与最终分发许可 | PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-06 |
+29 -10
View File
@@ -370,10 +370,12 @@
"refactor/baselines/distribution.json",
"refactor/baselines/thumbnail-contract.md",
"refactor/baselines/thumbnail-release.json",
"refactor/scripts/thumbnail-contract.test.mjs"
"refactor/scripts/thumbnail-contract.test.mjs",
"refactor/baselines/thumbnail-public-types-validation.json"
],
"compatibleResolution": "Recovered 3.5.31 has main and a CSS entry; baseline workspace lacks main and points to missing .esm.js/types. Recoverable CDN main is verified against Git, not an original tarball. Preserve supported paths, decide historical CSS disposition, add real declarations and verify isolated installs in 04/06.",
"closureCriteria": "隔离打包消费者能加载所有原承诺路径,dist 不作为源码直接手改。"
"closureCriteria": "隔离打包消费者能加载所有原承诺路径,dist 不作为源码直接手改。",
"workspaceState": "Current root ESM, direct CJS, legacy and declarations pass isolated installs. Missing historical .esm.js was replaced by the real root .mjs entry. Historical 3.5.31 CSS path/disposition and complete original promised-path review remain 06; item stays open."
},
{
"id": "BASE-PERF-01",
@@ -3867,8 +3869,10 @@
"refactor/changes/2026-09-13-PKG-TOOL-THUMB-02-behavior.md",
"test/thumbnail.test.js",
"test/browser/thumbnail-tool.spec.js",
"refactor/changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md"
]
"refactor/changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md",
"refactor/changes/2026-09-13-PKG-TOOL-THUMB-04-public-types.md"
],
"workspaceState": "Explicit user question pending: prefer published 3.5.31 defaults with opt-in 4.4 behavior, or accept the documented old-version differences and retain 4.4 defaults. No answer inferred; runtime unchanged."
},
{
"id": "THUMB-LIFE-01",
@@ -3910,12 +3914,20 @@
],
"compatibleResolution": "Historical release declares tiny-emitter; review local emitter provenance/license before TS migration. Keep recovered individual CDN files distinct from full npm archive evidence.",
"closureCriteria": "Reviewed emitter attribution and license ledger plus explicit historical artifact recovery limits and complete candidate tarball evidence.",
"status": "open",
"status": "resolved",
"evidence": [
"refactor/baselines/thumbnail-contract.md",
"refactor/baselines/thumbnail-release.json",
"refactor/scripts/thumbnail-contract.test.mjs"
]
"refactor/scripts/thumbnail-contract.test.mjs",
"refactor/baselines/thumbnail-public-types-validation.json"
],
"resolutionEvidence": [
"refactor/baselines/thumbnail-public-types-validation.json",
"refactor/baselines/thumbnail-vendor/sources.json",
"packages/artplayer-tool-thumbnail/THIRD_PARTY_NOTICES"
],
"resolutionRationale": "Fixed 2.1.0 upstream comparison and local adaptation reviewed; complete MIT notice verified in every standalone bundle and every byte of the actual installed candidate pack. Exact original copied revision and complete historical npm archive remain unknown and are explicitly bounded, not claimed recovered. Historical CSS/paths remain independently open under BASE-DIST-01.",
"workspaceState": "Current candidate source/bundle/packed attribution verified; repeat installed artifact verification when release contents change."
},
{
"id": "THUMB-MEDIA-01",
@@ -3939,7 +3951,7 @@
"id": "VENDOR-09",
"title": "Thumbnail tiny-emitter 改写来源与最终分发许可",
"confirmation": "source-observed",
"status": "open",
"status": "resolved",
"owners": [
"PKG-TOOL-THUMB-04",
"PKG-TOOL-THUMB-06"
@@ -3948,11 +3960,18 @@
"refactor/baselines/thumbnail-vendor/sources.json",
"packages/artplayer-tool-thumbnail/src/emitter.ts",
"packages/artplayer-tool-thumbnail/THIRD_PARTY_NOTICES",
"test/thumbnail-vendor.test.js"
"test/thumbnail-vendor.test.js",
"refactor/baselines/thumbnail-public-types-validation.json"
],
"compatibleResolution": "Preserve original attribution, typed local dispatch compatibility and complete MIT notice in source/package/standalone bundles.",
"closureCriteria": "Pinned upstream reference and local adaptation reviewed; all bundle and actual packed distribution notices verified. Do not claim the exact original copied revision or a recovered complete historical archive.",
"workspaceState": "TS adaptation and pinned 2.1.0 source/license references recorded; normal build includes package notices. Final installed/packed distribution evidence remains with 04/06."
"workspaceState": "Current candidate source/bundle/packed attribution verified; repeat installed artifact verification when release contents change.",
"resolutionEvidence": [
"refactor/baselines/thumbnail-public-types-validation.json",
"refactor/baselines/thumbnail-vendor/sources.json",
"packages/artplayer-tool-thumbnail/THIRD_PARTY_NOTICES"
],
"resolutionRationale": "Fixed 2.1.0 upstream comparison and local adaptation reviewed; complete MIT notice verified in every standalone bundle and every byte of the actual installed candidate pack. Exact original copied revision and complete historical npm archive remain unknown and are explicitly bounded, not claimed recovered. Historical CSS/paths remain independently open under BASE-DIST-01."
}
]
}
@@ -0,0 +1,114 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import ts from 'typescript'
import compat from 'typescript-compat'
import { checkFiles, packedFiles } from '../../scripts/package-check.mjs'
import { consumerDirectory, removeConsumer, run, workspace, writeJson } from '../../scripts/package-consumer.mjs'
import { verifyThumbnailContract } from './thumbnail-contract.mjs'
import { hash, readMember } from './releases.mjs'
async function main() {
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use yarn test:thumbnail-types-package')
const yarn = process.env.npm_execpath
assert(yarn && fs.existsSync(yarn))
const output = fs.mkdtempSync(path.join(workspace, 'refactor/.cache/thumbnail-package-types-'))
console.log(`Thumbnail installed package evidence: ${output}`)
const contract = verifyThumbnailContract()
const frozen = path.join(output, 'frozen-workspace')
fs.mkdirSync(frozen)
const prefix = 'packages/artplayer-tool-thumbnail/'
for (const [file, text] of contract.source) {
if (!file.startsWith(prefix)) continue
const target = path.join(frozen, file.slice(prefix.length))
fs.mkdirSync(path.dirname(target), { recursive: true })
fs.writeFileSync(target, text)
}
const packages = []
const matrix = []
const runtime = []
for (const [label, folder] of [['workspace-fixture', frozen], ['candidate', path.join(workspace, 'packages/artplayer-tool-thumbnail')]]) {
const archive = path.join(output, `${label}.tgz`)
fs.writeFileSync(path.join(output, `${label}-pack.log`), run([yarn, 'pack', '--filename', archive], folder))
const manifest = JSON.parse(readMember(archive, 'package/package.json'))
const files = packedFiles(archive)
if (label === 'candidate') {
checkFiles(manifest, files, [...contract.source.keys()].filter(file => file.startsWith(`${prefix}dist/`)).map(file => `package/${file.slice(prefix.length)}`))
assert(files.includes('package/THIRD_PARTY_NOTICES'))
assert.equal(readMember(archive, 'package/THIRD_PARTY_NOTICES').toString(), fs.readFileSync(path.join(folder, 'THIRD_PARTY_NOTICES'), 'utf8'))
}
else {
assert.equal(manifest.main, undefined)
assert(!files.includes(`package/${manifest.module}`))
assert(!files.includes(`package/${manifest.types}`))
}
const pkg = { label, name: manifest.name, archive, sha256: hash(fs.readFileSync(archive)), manifest, files: Object.fromEntries(files.map(member => [member, hash(readMember(archive, member))])) }
packages.push(pkg)
const consumer = consumerDirectory()
try {
writeJson(path.join(consumer, 'package.json'), { name: 'thumbnail-isolated-consumer', private: true, dependencies: { [pkg.name]: `file:${archive.replaceAll('\\', '/')}` } })
fs.writeFileSync(path.join(output, `${label}-install.log`), run([yarn, 'install', '--offline', '--ignore-scripts', '--non-interactive'], consumer))
const lock = fs.readFileSync(path.join(consumer, 'yarn.lock'))
fs.writeFileSync(path.join(output, `${label}-frozen-install.log`), run([yarn, 'install', '--offline', '--frozen-lockfile', '--ignore-scripts', '--non-interactive'], consumer))
assert.deepEqual(fs.readFileSync(path.join(consumer, 'yarn.lock')), lock)
fs.writeFileSync(path.join(output, `${label}-yarn.lock`), lock)
const installed = path.join(consumer, 'node_modules', pkg.name)
assert.equal(fs.realpathSync(installed), installed, 'Consumer must not use workspace links')
for (const [member, expected] of Object.entries(pkg.files))
assert.equal(hash(fs.readFileSync(path.join(installed, member.slice(8)))), expected, member)
const runtimeFile = path.join(consumer, 'runtime.cjs')
fs.writeFileSync(runtimeFile, `const assert = require('node:assert/strict');
const value = require('${pkg.name}');
assert.equal(typeof value, 'function');
assert.equal(value.default, undefined);
assert.equal(value.DEFAULTS.number, 60);
${label === 'candidate' ? `const legacy = require('${pkg.name}/legacy'); assert.equal(typeof legacy, 'function'); assert.equal(legacy.default, undefined);` : ''}
(async () => {
let imported;
try { imported = await import('${pkg.name}'); }
catch (error) { ${label === 'candidate' ? 'throw error;' : `assert.equal(error.code, 'ERR_MODULE_NOT_FOUND'); console.log(JSON.stringify({ commonjs: 'function', esm: error.code, typesMissing: true })); return;`} }
${label === 'candidate' ? `assert.equal(typeof imported.default, 'function'); assert.deepEqual(Object.keys(imported), ['default']); const legacy = await import('${pkg.name}/legacy'); assert.equal(typeof legacy.default, 'function'); console.log(JSON.stringify({ commonjs: 'function', esm: 'function', legacy: 'function', ssrImport: true }));` : `throw new Error('Frozen missing ESM unexpectedly loaded');`}
})().catch(error => { console.error(error); process.exitCode = 1; });`)
const observation = run([runtimeFile], consumer)
fs.writeFileSync(path.join(output, `${label}-runtime.log`), observation)
runtime.push({ package: label, ...JSON.parse(observation.trim()) })
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
if (label === 'candidate') modes.push([ts, 'nodenext-cjs-no-interop'], [compat, 'node10-commonjs-no-interop'])
for (const [compiler, mode] of modes) {
const next = mode.startsWith('nodenext')
const noInterop = mode.endsWith('no-interop')
const filename = path.join(consumer, `consumer.${next ? mode.includes('-cjs') ? 'cts' : 'mts' : 'ts'}`)
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: !noInterop, target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], module: next ? compiler.ModuleKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleKind.ESNext : compiler.ModuleKind.CommonJS, moduleResolution: next ? compiler.ModuleResolutionKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleResolutionKind.Bundler : compiler.ModuleResolutionKind.NodeJs }
const source = label === 'candidate' ? fs.readFileSync(path.join(workspace, noInterop ? 'test/types/thumbnail-commonjs.cts' : 'test/types/thumbnail.ts'), 'utf8') : `import Thumbnail from '${pkg.name}'; new Thumbnail({ fileInput: document.createElement('input') });`
function compile(code) {
fs.writeFileSync(filename, code)
const program = compiler.createProgram([filename], options)
for (const file of program.getSourceFiles()) {
const real = fs.realpathSync(file.fileName)
assert(real.startsWith(fs.realpathSync(consumer) + path.sep) || (program.isSourceFileDefaultLibrary(file) && path.dirname(real) === fs.realpathSync(path.dirname(compiler.sys.getExecutingFilePath()))), `Type escaped installed consumer: ${real}`)
}
return compiler.getPreEmitDiagnostics(program).map(item => ({ code: item.code, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n').replaceAll(consumer.replaceAll('\\', '/'), '<consumer>') }))
}
const diagnostics = compile(source)
writeJson(path.join(output, `${label}-${compiler.version}-${mode}.json`), diagnostics)
if (label === 'candidate') assert.deepEqual(diagnostics, [], `${compiler.version} ${mode}`)
else {
assert.deepEqual(diagnostics.map(item => item.code), [mode === 'node10-commonjs' ? 2307 : 7016], 'Only the exact frozen missing entry/declaration diagnostic is expected')
}
const negative = label === 'candidate' ? compile(source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')) : []
if (label === 'candidate') assert.equal(negative.length, noInterop ? 1 : 10)
matrix.push({ package: label, compiler: compiler.version, mode, diagnostics, negative })
}
if (label === 'candidate') fs.cpSync(installed, path.join(output, 'installed-artifacts'), { recursive: true })
}
finally { removeConsumer(consumer) }
}
writeJson(path.join(output, 'report.json'), { task: 'PKG-TOOL-THUMB-04', scope: 'Frozen Git input fixture and candidate Yarn pack installed outside workspace. The frozen fixture is not an original npm archive; no recovered 3.5.31 complete archive is claimed. Candidate root/legacy runtime, offline frozen install, all files and strict consumers verified.', packages, matrix, runtime })
console.log(`Thumbnail installed types verified: ${matrix.length} compiler cases; ${output}`)
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
+74
View File
@@ -0,0 +1,74 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
// eslint-disable-next-line test/no-import-node-test -- Public type and editor contract runner.
import test from 'node:test'
import ts from 'typescript'
import compat from 'typescript-compat'
import { ESLint } from 'eslint'
import { checkConsumer } from '../../scripts/typecheck.mjs'
import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from '../../scripts/plugin-editor-types.mjs'
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
const invalid = source => source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')
test('Thumbnail public declarations check actual callbacks, custom events and invalid calls in current and old TS', () => {
const source = fs.readFileSync('test/types/thumbnail.ts', 'utf8')
for (const [compiler, mode] of modes) {
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${compiler.version} ${mode}`)
assert.equal(checkConsumer(compiler, mode, invalid(source)).length, 10, `${compiler.version} ${mode} negatives`)
}
const commonjs = fs.readFileSync('test/types/thumbnail-commonjs.cts', 'utf8')
for (const [compiler, mode] of [[ts, 'nodenext-cjs'], [compat, 'node10-commonjs']]) {
assert.deepEqual(checkConsumer(compiler, mode, commonjs), [])
assert.equal(checkConsumer(compiler, mode, invalid(commonjs)).length, 1)
}
assert.deepEqual(checkConsumer(ts, 'node10-commonjs', fs.readFileSync('test/types/thumbnail-source.ts', 'utf8')), [], 'Source and declaration constructors must be mutually assignable')
})
test('Thumbnail editor declaration is generated with its exact uppercase global and works in old TS', async () => {
const source = fs.readFileSync('packages/artplayer-tool-thumbnail/types/artplayer-tool-thumbnail.d.ts', 'utf8')
const code = generatePluginEditorDeclaration(source, 'ArtplayerToolThumbnail')
const file = 'docs/assets/ts/artplayer-tool-thumbnail.d.ts'
const [formatted] = await new ESLint({ fix: true, fixTypes: ['layout'] }).lintText(code, { filePath: file })
assert.equal(formatted.errorCount, 0)
assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), formatted.output || code)
const consumer = `const input = document.createElement('input');
const options: ArtplayerToolThumbnail.Option = { fileInput: input };
const tool: ArtplayerToolThumbnail = new ArtplayerToolThumbnail(options);
tool.on('update', (url, progress) => url + progress.toFixed());
tool.on('custom', (value: number) => value.toFixed());
const promise: Promise<void> = tool.start();
// @ts-expect-error No lowercase global exists.
const bad = new artplayerToolThumbnail(options);
// @ts-expect-error Invalid known event payload.
tool.emit('update', 12, 'url');`
for (const compiler of [ts, compat]) {
const core = fs.readFileSync('docs/assets/ts/artplayer.d.ts', 'utf8')
assert.deepEqual(checkPluginEditorDeclaration(code, core, consumer, compiler), [])
assert.equal(checkPluginEditorDeclaration(code, core, invalid(consumer), compiler).length, 2)
}
assert.throws(() => generatePluginEditorDeclaration(source.replace('declare class ArtplayerToolThumbnail', 'declare class WrongClass'), 'ArtplayerToolThumbnail'), /Unexpected CommonJS editor class/)
})
test('Full editor generation bundles explicit local MediaBunny types instead of publishing an invalid media global', () => {
const source = fs.readFileSync('packages/artplayer-proxy-mediabunny/types/artplayer-proxy-mediabunny.d.ts', 'utf8')
const media = fs.readFileSync('packages/artplayer-proxy-mediabunny/types/media.d.ts', 'utf8')
const core = fs.readFileSync('docs/assets/ts/artplayer.d.ts', 'utf8')
const frozenGenerator = `${source.replace(/^import.*$/gm, '')}\nexport = artplayerProxyMediabunny;\nexport as namespace artplayerProxyMediabunny;`
const code = generatePluginEditorDeclaration(source, 'artplayerProxyMediabunny', { './media': media })
const consumer = `declare const player: artplayerProxyMediabunny.MediaBunnyPlayer;
const canvas: (art: Artplayer) => HTMLCanvasElement = artplayerProxyMediabunny();
const time: number | undefined = player.mediabunny?.currentTime;
// @ts-expect-error HLS levels are read from state, not directly from the player alias.
const bad = player.mediabunny?.levels;`
for (const compiler of [ts, compat]) {
assert(checkPluginEditorDeclaration(frozenGenerator, core, '', compiler).some(item => item.code === 2307), 'The old generator leaves a missing relative type import')
assert.deepEqual(checkPluginEditorDeclaration(code, core, consumer, compiler), [])
assert.equal(checkPluginEditorDeclaration(code, core, invalid(consumer), compiler).length, 1)
}
assert.throws(() => generatePluginEditorDeclaration(source, 'artplayerProxyMediabunny'), /Unsupported editor type re-export/)
assert.throws(() => generatePluginEditorDeclaration(source, 'artplayerProxyMediabunny', { './media': media + '\nexport interface Extra {}' }), /explicitly cover/)
const common = fs.readFileSync('docs/assets/js/common.js', 'utf8')
assert(common.includes('./assets/ts/artplayer-tool-thumbnail.d.ts'))
assert(!common.includes('./assets/ts/media.d.ts'))
})
+3 -1
View File
@@ -3815,7 +3815,9 @@
"acceptance": "严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容;为 BASE-DIST-01 提供兼容入口/声明及消费者验证",
"evidence": [
"changes/2026-09-13-PKG-TOOL-THUMB-04-runtime-types.md",
"baselines/thumbnail-runtime-types-validation.json"
"baselines/thumbnail-runtime-types-validation.json",
"changes/2026-09-13-PKG-TOOL-THUMB-04-public-types.md",
"baselines/thumbnail-public-types-validation.json"
]
},
{
+5 -3
View File
@@ -1037,7 +1037,7 @@
"PKG-TOOL-THUMB-06"
],
"sourceStatus": "Local on/once/emit/off structurally correspond to the fixed tiny-emitter 2.1.0 reference; recovered 3.5.31 declared ^2.1.0. Exact original copied revision is unknown. This migration adds types while preserving the local class and dispatch behavior.",
"licenseStatus": "Pinned upstream MIT notice copied verbatim into package THIRD_PARTY_NOTICES and included by the normal standalone build banner. Final packed distribution verification remains 06.",
"licenseStatus": "Full pinned upstream MIT notice verified in package, all standalone bundles and actual installed candidate tarball. Repeat on changed release candidates; original copy revision remains unknown.",
"upstreamSources": [
"https://raw.githubusercontent.com/scottcorgan/tiny-emitter/2.1.0/index.js",
"https://raw.githubusercontent.com/scottcorgan/tiny-emitter/2.1.0/LICENSE",
@@ -1049,14 +1049,16 @@
{
"path": "packages/artplayer-tool-thumbnail/src/emitter.ts",
"algorithm": "sha256-lf",
"sha256": "2844fa8d279d35d676f61eef4f8a3e173146548c64b8e49f2dabfc25aec56668"
"sha256": "bd4d5472361edd2e31a6ae1aecce01e310e4086edebe434733d0f5b5eea37a0b"
}
],
"reviewEvidence": [
"refactor/baselines/thumbnail-vendor/sources.json",
"test/thumbnail-runtime.test.js",
"test/thumbnail-vendor.test.js",
"refactor/changes/2026-09-13-PKG-TOOL-THUMB-04-runtime-types.md"
"refactor/changes/2026-09-13-PKG-TOOL-THUMB-04-runtime-types.md",
"refactor/changes/2026-09-13-PKG-TOOL-THUMB-04-public-types.md",
"refactor/baselines/thumbnail-public-types-validation.json"
]
}
],
+16
View File
@@ -99,3 +99,19 @@ Audio Track 的自有源码由 packages/artplayer-plugin-audio-track/tsconfig.js
详见包内 ARCHITECTURE.md;不能将部分参数直接覆盖默认签名而破坏旧推断。
新迁移包复制分包配置并按实际运行环境调整,加入特有正反例;编译器入口自动发现包配置。TS 构建入口与声明生成由 ENG-06/07 和各包迁移处理;不能只增加一个空 config 就宣布包迁移完成。所有生产变更仍按每任务独立提交和包内架构文档规则交付。
## Thumbnail 与完整编辑器声明生成
Thumbnail 的 public d.ts 是 CommonJS class/namespace,d.cts/d.mts 共享其构造器身份。
源码和声明双向检查;自定义事件可推导调用方元组,任意回调抛出的 message 保持 unknown。
`yarn test:thumbnail-types-package` 在仓库外安装实际 Yarn tarball,检查当前/旧 TS、
无 interop 的 CommonJS、root/legacy 导入、全部文件指纹和许可。旧 Git 夹具的 ESM/声明
失败保留为对照,不声称它是原始 npm 包。完整默认兼容策略仍由 THUMB-COMPAT-01 决定。
`yarn build:ts` 只枚举与包名对应的公开声明入口,辅助类型不作为独立全局导入。
语义生成器支持 CommonJS 类、函数及 callable const;显式的相对 type re-export
只接受调用方提供且完整匹配导出列表的本地类型源。MediaBunny 的 media.d.ts 使用此路径
合入主编辑器声明,避免生成无效 media 全局或遗留失效的相对 import。
`refactor/scripts/thumbnail-types.test.mjs` 覆盖该历史失败与旧/新编译器;
`test/browser/thumbnail-editor-types.spec.js` 使用真实 Monaco worker 验证核心、工具与
MediaBunny 声明并执行工具构造/清理。媒体解码和完整 demo 组合仍须独立验证。
+8 -6
View File
@@ -20,8 +20,9 @@ function parsePluginInfo(pluginPath) {
const file = path.basename(pluginPath)
const baseName = file.replace('.d.ts', '')
const name = baseName === 'artplayer-tool-iframe'
? 'ArtplayerToolIframe'
const classNames = { 'artplayer-tool-iframe': 'ArtplayerToolIframe', 'artplayer-tool-thumbnail': 'ArtplayerToolThumbnail' }
const name = classNames[baseName]
? classNames[baseName]
: baseName
.split('-')
.map((word, index) =>
@@ -40,9 +41,9 @@ fs.writeFileSync(artplayerTSoutput, code.trim())
console.log(`✨ Built ${artplayerTSoutput}`);
(async function () {
const available = glob.sync('packages/artplayer-*-*/types/*.d.ts')
const selected = process.argv.slice(2)
const packageOf = file => path.basename(path.dirname(path.dirname(file)))
const available = glob.sync('packages/artplayer-*-*/types/*.d.ts').filter(file => path.basename(file) === `${packageOf(file)}.d.ts`)
const selected = process.argv.slice(2)
for (const name of selected)
assert(available.some(file => packageOf(file) === name), `Unknown declaration package: ${name}`)
const pluginsTS = selected.length ? available.filter(file => selected.includes(packageOf(file))) : available
@@ -52,9 +53,10 @@ console.log(`✨ Built ${artplayerTSoutput}`);
const type = pluginsTS[index]
const { name, file } = parsePluginInfo(type)
const source = String(fs.readFileSync(type))
const semanticPlugin = ['artplayerPluginHlsControl', 'artplayerPluginAudioTrack', 'artplayerPluginDashControl', 'artplayerPluginAds', 'artplayerPluginAmbilight', 'artplayerProxyCanvas', 'artplayerPluginDocumentPip', 'ArtplayerToolIframe'].includes(name)
const semanticPlugin = ['artplayerPluginHlsControl', 'artplayerPluginAudioTrack', 'artplayerPluginDashControl', 'artplayerPluginAds', 'artplayerPluginAmbilight', 'artplayerProxyCanvas', 'artplayerProxyMediabunny', 'artplayerPluginDocumentPip', 'ArtplayerToolIframe', 'ArtplayerToolThumbnail'].includes(name)
const localTypes = name === 'artplayerProxyMediabunny' ? { './media': fs.readFileSync(path.join(path.dirname(type), 'media.d.ts'), 'utf8') } : {}
const code = semanticPlugin
? generatePluginEditorDeclaration(source, name)
? generatePluginEditorDeclaration(source, name, localTypes)
: `${source.replace(reg, '')}\nexport = ${name};\nexport as namespace ${name};\n`
if (semanticPlugin) {
const core = fs.readFileSync(artplayerTSoutput, 'utf8')
+39 -3
View File
@@ -4,10 +4,32 @@ import { fileURLToPath } from 'node:url'
import ts from 'typescript'
// Migrate each package explicitly; unknown imports/exports must not disappear silently.
export function generatePluginEditorDeclaration(code, name) {
export function generatePluginEditorDeclaration(code, name, localTypes = {}) {
assert(/^[a-z_$][\w$]*$/i.test(name), 'Invalid plugin global')
const source = ts.createSourceFile('plugin.d.ts', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
let source = ts.createSourceFile('plugin.d.ts', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
assert.equal(source.parseDiagnostics.length, 0, 'Invalid plugin declaration syntax')
const expanded = []
for (const node of source.statements) {
if (!ts.isExportDeclaration(node) || !node.moduleSpecifier) {
expanded.push(node)
continue
}
assert(node.isTypeOnly, 'Unsupported plugin editor declaration: runtime re-export')
const dependency = localTypes[node.moduleSpecifier.text]
assert(typeof dependency === 'string' && node.exportClause && ts.isNamedExports(node.exportClause), 'Unsupported editor type re-export')
const names = node.exportClause.elements.map((item) => {
assert(!item.propertyName, 'Renamed editor type re-export is unsupported')
return item.name.text
})
const types = ts.createSourceFile('dependency.d.ts', dependency, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
assert.equal(types.parseDiagnostics.length, 0)
const definitions = types.statements.filter(item => !ts.isImportDeclaration(item))
assert(definitions.every(item => ts.isInterfaceDeclaration(item) || ts.isTypeAliasDeclaration(item)), 'Editor dependencies must contain only types')
assert.deepEqual(definitions.map(item => item.name.text).sort(), names.sort(), 'Editor re-export must explicitly cover its local type definitions')
expanded.push(...types.statements)
}
// Reparse combined statements so comments and text positions belong to one file.
source = ts.createSourceFile('plugin.d.ts', expanded.map(node => ts.createPrinter().printNode(ts.EmitHint.Unspecified, node, node.getSourceFile())).join('\n'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
if (source.statements.some(node => ts.isExportAssignment(node) && node.isExportEquals))
return generateCommonJSPluginEditor(source, name)
const factory = ts.factory
@@ -27,6 +49,13 @@ export function generatePluginEditorDeclaration(code, name) {
exported = true
continue
}
if (ts.isVariableStatement(node)) {
const values = node.declarationList.declarations
assert(values.length === 1 && values[0].name.text === name && ts.isFunctionTypeNode(values[0].type), 'Unexpected plugin callable variable')
definitions.push(factory.replaceModifiers(node, [factory.createModifier(ts.SyntaxKind.ExportKeyword)]))
callable = true
continue
}
assert(ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node), 'Unsupported plugin editor declaration')
assert(node.name && node.name.text !== internal, 'Invalid plugin declaration name')
definitions.push(factory.replaceModifiers(node, [factory.createModifier(ts.SyntaxKind.ExportKeyword)]))
@@ -67,6 +96,7 @@ function generateCommonJSPluginEditor(source, name) {
let callable = false
let exported = false
let global = false
let classExport = false
for (const node of source.statements) {
if (ts.isImportDeclaration(node)) {
assert(node.importClause?.isTypeOnly && !node.importClause.namedBindings && node.importClause.name?.text === 'Artplayer' && node.moduleSpecifier.text === 'artplayer', 'Unsupported CommonJS plugin editor import')
@@ -77,6 +107,11 @@ function generateCommonJSPluginEditor(source, name) {
assert(node.body && ts.isModuleBlock(node.body) && node.body.statements.every(item => ts.isInterfaceDeclaration(item) || ts.isTypeAliasDeclaration(item)), 'Editor namespace must contain only public type declarations')
namespace = true
}
else if (ts.isClassDeclaration(node)) {
assert.equal(node.name?.text, name, 'Unexpected CommonJS editor class')
callable = true
classExport = true
}
else if (ts.isVariableStatement(node)) {
const definitions = node.declarationList.declarations
assert.equal(definitions.length, 1)
@@ -101,7 +136,8 @@ function generateCommonJSPluginEditor(source, name) {
}
assert(namespace && callable && exported && global, 'Incomplete CommonJS plugin editor declaration')
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed })
return `// Generated from the package public declaration by yarn build:ts. Do not edit.\n/* eslint-disable ts/no-redeclare -- Callable and public type namespace intentionally merge. */\n${statements.map(node => printer.printNode(ts.EmitHint.Unspecified, node, source)).join('\n')}\n`
const bridge = classExport ? '' : '/* eslint-disable ts/no-redeclare -- Callable and public type namespace intentionally merge. */\n'
return `// Generated from the package public declaration by yarn build:ts. Do not edit.\n${bridge}${statements.map(node => printer.printNode(ts.EmitHint.Unspecified, node, source)).join('\n')}\n`
}
export function checkPluginEditorDeclaration(code, core, consumer = '', compiler = ts) {
@@ -0,0 +1,66 @@
import fs from 'node:fs'
import { hash } from '../../refactor/scripts/releases.mjs'
import { thumbnailCandidate } from '../helpers/thumbnail.js'
import { expect, test } from './fixtures.js'
const candidate = await thumbnailCandidate()
test('Monaco consumes Thumbnail global declarations and runs the actual typed constructor and cleanup', async ({ page }, testInfo) => {
await page.goto('/test/player.html?core=candidate&chapter=published')
await page.addScriptTag({ content: candidate.code })
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 libraries = await Promise.all(['artplayer', 'artplayer-tool-thumbnail', 'artplayer-proxy-mediabunny'].map(async (name) => {
const uri = `file:///${name}.d.ts`
const source = await (await fetch(`/assets/ts/${name}.d.ts`)).text()
return { uri, handle: types.typescriptDefaults.addExtraLib(source, uri) }
}))
const model = api.editor.createModel(`
const input = document.createElement('input'); input.type = 'file'; document.body.appendChild(input);
const options: ArtplayerToolThumbnail.Option = { fileInput: input, number: 10 };
const tool: ArtplayerToolThumbnail = new ArtplayerToolThumbnail(options);
let last = 0;
tool.on('custom', (value: number) => { last = value; });
tool.emit('custom', 7);
tool.on('update', (url, progress) => url + progress.toFixed());
const video: HTMLVideoElement = tool.video;
tool.destroy(); tool.destroy(); input.remove();
function mediaTypes(player: artplayerProxyMediabunny.MediaBunnyPlayer): number | undefined { return player.mediabunny?.currentTime; }
`, 'typescript', api.Uri.parse('file:///thumbnail-consumer.ts'))
const invalid = api.editor.createModel("new ArtplayerToolThumbnail({ fileInput: '#input' });", 'typescript', api.Uri.parse('file:///thumbnail-invalid.ts'))
try {
const worker = await (await types.getTypeScriptWorker())(model.uri, invalid.uri)
const syntax = await worker.getSyntacticDiagnostics(model.uri.toString())
const semantic = await worker.getSemanticDiagnostics(model.uri.toString())
const declaration = (await Promise.all(libraries.map(library => worker.getSemanticDiagnostics(library.uri)))).flat()
const bad = await worker.getSemanticDiagnostics(invalid.uri.toString())
if (syntax.length || semantic.length || declaration.length)
return { syntax, semantic, declaration, invalid: bad.map(item => item.code), executed: false }
const output = await worker.getEmitOutput(model.uri.toString())
const script = output.outputFiles.find(file => file.name.endsWith('.js'))
if (!script)
throw new Error('Monaco did not emit the Thumbnail fixture')
// eslint-disable-next-line no-new-func -- Run actual code emitted by the local Monaco worker.
const actual = new Function(`${script.text}\nreturn { last, connected: video.isConnected, inputConnected: input.isConnected, processing: tool.processing }`)()
return { syntax, semantic, declaration, invalid: bad.map(item => item.code), executed: true, actual }
}
finally {
model.dispose()
invalid.dispose()
for (const library of libraries)
library.handle.dispose()
}
})
expect(result.syntax).toEqual([])
expect(result.semantic).toEqual([])
expect(result.declaration).toEqual([])
expect(result.invalid).toEqual([2322])
expect(result.executed).toBe(true)
expect(result.actual).toEqual({ last: 7, connected: false, inputConnected: false, processing: false })
await testInfo.attach('thumbnail-editor', { contentType: 'application/json', body: JSON.stringify({ candidate: candidate.name, sha256: hash(candidate.code), declarations: Object.fromEntries(['artplayer', 'artplayer-tool-thumbnail', 'artplayer-proxy-mediabunny'].map(name => [name, hash(fs.readFileSync(`docs/assets/ts/${name}.d.ts`))])), result, scope: 'Real local Monaco worker, typed constructor/custom event and native DOM cleanup; MediaBunny relative type bundle also compiles. This does not test file decoding or the complete demo/player integration.' }) })
})
+21
View File
@@ -129,3 +129,24 @@ test('Thumbnail typed facade retains synchronous validation, option mutation and
assert.equal(tool.errorHandle(true, 'unused'), undefined)
tool.destroy()
})
test('Thumbnail error event preserves arbitrary thrown message values and Promise rejection identity', async () => {
for (const failure of [{ message: 7 }, { message: { detail: 'failure' } }, 3, null]) {
const { tool } = create(candidate)
tool.setup({ number: 10 })
tool.file = { name: 'sample.mp4' }
tool.video.duration = 100
const errors = []
tool.on('error', value => errors.push(value))
tool.on('update', () => {
throw failure
})
const pending = tool.start()
await Promise.resolve()
tool.video.oncanplay()
await assert.rejects(pending, error => error === failure)
assert.deepEqual(errors, [failure?.message])
assert.equal(tool.processing, false)
tool.destroy()
}
})
+14
View File
@@ -0,0 +1,14 @@
// eslint-disable-next-line ts/no-require-imports -- Test the actual CommonJS constructor without interop helpers.
import Thumbnail = require('artplayer-tool-thumbnail')
// eslint-disable-next-line ts/no-require-imports -- The legacy subpath also directly exports its constructor.
import Legacy = require('artplayer-tool-thumbnail/legacy')
const option: Thumbnail.Option = { fileInput: document.createElement('input') }
const tool: Thumbnail = new Thumbnail(option)
const legacy: Thumbnail = new Legacy(option)
const pending: Promise<void> = tool.start()
tool.on('update', (url, progress) => url + progress.toFixed())
void [legacy, pending]
// @ts-expect-error No self-default constructor is exported.
const WrongAlias = Thumbnail.default
void WrongAlias
+2 -1
View File
@@ -11,7 +11,7 @@ tool.once('file', function (file) {
this.name = file.name
}, { name: '' })
tool.on('error', (message) => {
const value: string | undefined = message
const value: unknown = message
void value
})
tool.on('custom', (...payload) => {
@@ -19,6 +19,7 @@ tool.on('custom', (...payload) => {
void values
})
tool.emit(Symbol('extension'), 'value')
tool.on('custom-number', (value: number) => value.toFixed())
const pending: Promise<void> = tool.start()
const point: { time: number, x: number, y: number } | undefined = tool.creatScreenshotDate()[0]
const canvas: HTMLCanvasElement = tool.creatCanvas()
+8
View File
@@ -0,0 +1,8 @@
import type Definition from 'artplayer-tool-thumbnail'
import Implementation from '../../packages/artplayer-tool-thumbnail/src'
const constructor: typeof Definition = Implementation
const RuntimeConstructor: typeof Implementation = constructor
const value: Definition = new RuntimeConstructor({ fileInput: document.createElement('input') })
value.on('custom', (count: number) => count.toFixed())
void value
+58
View File
@@ -0,0 +1,58 @@
import type { EventArgs, Option, ScreenshotPoint } from 'artplayer-tool-thumbnail'
import Thumbnail from 'artplayer-tool-thumbnail'
import Legacy from 'artplayer-tool-thumbnail/legacy'
const input = document.createElement('input')
const option: Option = { fileInput: input, number: 10, width: 80, custom: { id: 1 } }
const tool: Thumbnail = new Thumbnail(option)
const legacy: Thumbnail = new Legacy(option)
const duration: number = tool.duration
const file: File | undefined = tool.file
const url: string | undefined = tool.thumbnailUrl
const pending: Promise<void> = tool.start()
const canvas: HTMLCanvasElement = tool.creatCanvas()
const points: ScreenshotPoint[] = tool.creatScreenshotDate()
const extension: unknown = tool.option.custom
const progress: EventArgs<'update'> = ['blob:sheet', 0.5]
tool.on('file', file => file.name)
tool.on('video', video => video.videoWidth)
tool.on('canvas', canvas => canvas.width)
tool.on('update', (url, progress) => url + progress.toFixed())
tool.on('error', (message) => {
if (typeof message === 'string')
message.toUpperCase()
})
tool.on('custom', (count: number, label: string) => label + count.toFixed())
tool.once(Symbol('custom'), (value: { id: number }) => value.id)
tool.on('update', function (url, progress) {
this.last = `${url}:${progress}`
}, { last: '' })
tool.emit('update', ...progress).emit('custom', 1, 'one')
tool.off('file').setup({ width: 100 }).download()
tool.loadVideo(input.files?.[0])
tool.loadVideo(null)
const defaults: Thumbnail.SheetOptions = Thumbnail.DEFAULTS
const end: void = tool.destroy()
void [legacy, duration, file, url, pending, canvas, points, extension, defaults, end]
// @ts-expect-error Numeric width is required.
tool.setup({ width: '80' })
// @ts-expect-error Selectors have never been resolved by the tool.
const wrongInput = new Thumbnail({ fileInput: '#input' })
// @ts-expect-error File input is not a media source URL.
tool.loadVideo('video.mp4')
// @ts-expect-error Progress is numeric and follows the URL.
tool.emit('update', 0.5, 'blob:sheet')
// @ts-expect-error File callbacks receive files, not media elements.
tool.on('file', (value: HTMLVideoElement) => value.duration)
// @ts-expect-error Completion returns no URL.
const wrongResult: Promise<string> = tool.start()
// @ts-expect-error The direct constructor has no runtime .default self-alias.
const WrongAlias = Thumbnail.default
// @ts-expect-error density is absent until extraction preflight.
const earlyDensity: number = tool.density
// @ts-expect-error Error listeners must narrow arbitrary thrown message values.
tool.on('error', (message: string) => message.length)
// @ts-expect-error Built-in done does not carry a file.
tool.emit('done', input.files?.[0])
void [wrongInput, wrongResult, WrongAlias, earlyDensity]