mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
fix(thumbnail): [PKG-TOOL-THUMB-04] supply public types and installed module entries
This commit is contained in:
1 parent
15485b6420
commit
3bff236145
33 files changed
+2466
-164
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
@@ -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保留
|
||||
|
||||
@@ -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
@@ -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
|
||||
})
|
||||
@@ -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
@@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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
@@ -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')
|
||||
|
||||
@@ -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.' }) })
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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]
|
||||
Reference in new issue
Block a user