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,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