fix(types): [PKG-FACTORY-01] preserve published factory compatibility

This commit is contained in:
Harvey Zhao committed 2026-09-13 16:53:27 +08:00
1 parent 1117107032
commit c4c7491e16
35 files changed
+670 -229

No files matched your search

+18 -11
View File
@@ -1,7 +1,7 @@
// 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 artplayerPluginAmbilight {
interface Option {
declare namespace artplayerPluginAmbilightDefinitions {
export interface Option {
/** CSS blur radius. @default '50px' */
blur?: string
/** Grid cell opacity. @default 0.5 */
@@ -13,23 +13,30 @@ declare namespace artplayerPluginAmbilight {
/** Background color transition duration in seconds. @default 0.3 */
duration?: number
}
interface Result {
export interface Result {
name: 'artplayerPluginAmbilight'
/** Start sampling; does nothing after the player is destroyed. */
start: () => void
/** Stop sampling while retaining the last colors. */
stop: () => void
}
interface Callable {
/** Published 1.1.0 factory shape; the options argument remains required. */
export type Callable = (option: Option) => (art: Artplayer) => Result
export type Factory = Callable
/** Accurate optional invocation and CommonJS self alias, exposed by /runtime. */
export interface RuntimeFactory {
(option?: Option): (art: Artplayer) => Result
/** Keep Parameters extraction identical to the published 1.1.0 signature. */
(option: Option): (art: Artplayer) => Result
}
interface Factory extends Callable {
/** Same function, including historical require(package).default calls. */
readonly default: Factory
readonly default: RuntimeFactory
}
export const artplayerPluginAmbilight: (option: Option) => (art: Artplayer) => Result
}
declare const artplayerPluginAmbilight: typeof artplayerPluginAmbilightDefinitions.artplayerPluginAmbilight
declare namespace artplayerPluginAmbilight {
export type Option = artplayerPluginAmbilightDefinitions.Option
export type Result = artplayerPluginAmbilightDefinitions.Result
export type Callable = artplayerPluginAmbilightDefinitions.Callable
export type Factory = artplayerPluginAmbilightDefinitions.Factory
export type RuntimeFactory = artplayerPluginAmbilightDefinitions.RuntimeFactory
}
declare const artplayerPluginAmbilight: artplayerPluginAmbilight.Factory
export = artplayerPluginAmbilight
export as namespace artplayerPluginAmbilight;
+21 -14
View File
@@ -1,21 +1,28 @@
// 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 artplayerProxyCanvas {
declare namespace artplayerProxyCanvasDefinitions {
/** Runs after drawing and bitmap release, before the draw event. */
type Option = (ctx: CanvasRenderingContext2D, video: HTMLVideoElement) => void
/** Preserve the exact published return type and its assignability. */
type Result = HTMLCanvasElement
/** Opt-in view of supported enumerable media members; native Canvas members win. */
type MediaCanvas = HTMLCanvasElement & Pick<HTMLVideoElement, Exclude<keyof HTMLVideoElement, keyof HTMLCanvasElement>>
interface Callable {
/** Keep Parameters extraction optional, as published in 1.1.0. */
(option?: Option): (art: Artplayer) => Result
}
interface Factory extends Callable {
/** Same factory, for historical require(package).default calls. */
readonly default: Factory
export type Option = (ctx: CanvasRenderingContext2D, video: HTMLVideoElement) => void
/** Preserve the exact published 1.1.0 return type and its assignability. */
export type Result = HTMLCanvasElement
export type Factory = (option?: Option) => (art: Artplayer) => Result
export type Callable = Factory
/** Explicit view of forwarded media members; native Canvas members win. */
export type MediaCanvas = HTMLCanvasElement & Pick<HTMLVideoElement, Exclude<keyof HTMLVideoElement, keyof HTMLCanvasElement>>
/** Opt-in runtime identity; the historical root factory has no required properties. */
export interface RuntimeFactory extends Factory {
readonly default: RuntimeFactory
}
export const artplayerProxyCanvas: (option?: Option) => (art: Artplayer) => Result
}
declare const artplayerProxyCanvas: typeof artplayerProxyCanvasDefinitions.artplayerProxyCanvas
declare namespace artplayerProxyCanvas {
export type Option = artplayerProxyCanvasDefinitions.Option
export type Result = artplayerProxyCanvasDefinitions.Result
export type Factory = artplayerProxyCanvasDefinitions.Factory
export type Callable = artplayerProxyCanvasDefinitions.Callable
export type MediaCanvas = artplayerProxyCanvasDefinitions.MediaCanvas
export type RuntimeFactory = artplayerProxyCanvasDefinitions.RuntimeFactory
}
declare const artplayerProxyCanvas: artplayerProxyCanvas.Factory
export = artplayerProxyCanvas
export as namespace artplayerProxyCanvas;
@@ -53,22 +53,36 @@ video/canvas with a lexical native RAF observer across published cores 5.1.7/5.4
and candidate core in Chromium/Firefox/WebKit. Proxy/device and complete packed
browser/demo acceptance remain tasks 05-06; see `refactor/ambilight-validation.md`.
Public types merge a callable value with a type-only namespace. An optional first
overload permits omitted/undefined options; the required final overload preserves
the published 1.1.0 `Parameters` extraction. Do not reorder these signatures. The
runtime factory has a self-referencing `default` property to support both historical
CommonJS forms. `.d.mts`/`.d.cts` forward to the same declaration for the actual ESM/
CommonJS entry kinds; `typesVersions` supplies `/legacy` to older resolvers. The
editor global is generated by `yarn build:ts` through the semantic plugin generator.
Root and `/legacy` public types preserve the exact published 1.1.0 required-option
factory and result. Do not add an optional overload or a required `default`
property: preserving `Parameters` alone does not preserve reverse assignment of
ordinary replacement factories. The JavaScript factory still has its historical
self-referencing `default` property and accepts omitted options.
`RuntimeFactory` and `/runtime` describe those optional calls and the self alias.
The subpath maps to the same production files as root, with `runtime.d.mts` for
native ESM and `runtime.d.cts` for CommonJS. `runtime.d.ts` provides Node10's
export-assignment shape through `typesVersions`. Root and `/legacy` use a
types-first mapping to the original default-export declaration, retaining 1.1.0's
NodeNext ESM namespace shape. The editor global is generated from the root
declaration by `yarn build:ts`; it likewise requires an options argument.
This follows the approved `refactor/type-compatibility-policy.md` and ADR-025.
The earlier 1.0.0 export-assignment and required-field conflicts have migration
examples in README. No source, emitted JavaScript, defaults, or old JS entrypoint
changes are part of this type repair.
Run `node --test refactor/scripts/ambilight-types.test.mjs` for public/editor/format
contracts and `yarn test:ambilight-types-package` for offline tarball installations
outside the workspace. The latter verifies two actual published plugin archives and
candidate against the same packed candidate core, five compiler modes plus two
candidate modes with interop disabled, and ten negative uses. Published 1.1.0 fails
NodeNext ESM with its old declarations; candidate must not inherit that failure.
Published 1.0.0 required all option fields; 1.1.0 already made them optional. Current
inference follows 1.1.0 while retaining valid calls from both generations.
candidate modes with interop disabled, and fifteen negative uses at their exact
statement lines. Published 1.1.0 has four NodeNext ESM diagnostics for the frozen
direct-call fixture; candidate preserves those codes and its valid namespace
consumer. Earlier 1.0.0 raw `import = require` root calls are separately checked:
they compile against 1.0.0 and retain the 1.1.0/candidate non-callable diagnostic.
Candidate `/runtime` raw require and native ESM calls compile successfully.
Published 1.0.0 required all option fields; 1.1.0 already made them optional.
`test/ambilight-proxy.test.js` and `test/browser/ambilight-proxy.spec.js` protect
resized output geometry. Browser tests play real video with the actual workspace
+38 -5
View File
@@ -20,11 +20,44 @@ const option: Option = { blur: '50px', opacity: 0.5, frequency: 10, duration: 0.
const plugins = [artplayerPluginAmbilight(option)]
```
Options can be omitted. `require('artplayer-plugin-ambilight')` and the historical
`require('artplayer-plugin-ambilight').default` refer to the same factory. The
`/legacy` entry remains available. The historical `zIndex` input is accepted but
ignored; the grid uses 9. Option fields stay optional as in version 1.1.0; code
extracting their types should account for `undefined`.
Root and `/legacy` types preserve the latest published 1.1.0 contract: the options
argument is required, its fields are optional, and an ordinary replacement factory
does not need a `default` property. Pass `{}` to use defaults with these types.
The historical `zIndex` input is accepted but ignored; the grid uses 9.
For accurate optional invocation and module interop, use `/runtime`:
```ts
import ambilight from 'artplayer-plugin-ambilight/runtime'
const plugins = [ambilight(), ambilight(undefined), ambilight.default({})]
```
It reuses the root implementation. Existing JavaScript calls with omitted options,
`require('artplayer-plugin-ambilight')` and
`require('artplayer-plugin-ambilight').default` still work. Start/stop remain
synchronous and `/legacy` keeps the historical build.
### Earlier TypeScript declarations
Version 1.0.0 used `export =` and required every option field; 1.1.0 changed to a
default export and optional fields. The root follows 1.1.0 rather than combining
incompatible factory signatures. Earlier TypeScript `import = require` callers
can switch only their module path:
```ts
import ambilight = require('artplayer-plugin-ambilight/runtime')
ambilight({ blur: '50px', opacity: 0.5, frequency: 10, zIndex: 9, duration: 0.3 })
```
Code reading an option field as a required value should supply a fallback, for
example `option.blur ?? '50px'`. This optional-field change already existed in
1.1.0. NodeNext ESM sees the historical root declaration as a CommonJS namespace;
that type shape is preserved, including its previous direct-call diagnostics.
Use `/runtime` for an accurate callable default import in native ESM. Node10
TypeScript default imports of `/runtime` require `esModuleInterop`; `import =
require` also works with interop disabled. The runtime does not add any dependency.
## Demo
@@ -19,24 +19,24 @@
],
"exports": {
".": {
"types": "./types/artplayer-plugin-ambilight.d.ts",
"import": "./dist/artplayer-plugin-ambilight.mjs",
"require": "./dist/artplayer-plugin-ambilight.js"
},
"./legacy": {
"types": "./types/artplayer-plugin-ambilight.d.ts",
"import": "./dist/artplayer-plugin-ambilight.legacy.js",
"require": "./dist/artplayer-plugin-ambilight.legacy.js"
},
"./runtime": {
"import": {
"types": "./types/artplayer-plugin-ambilight.d.mts",
"types": "./types/runtime.d.mts",
"default": "./dist/artplayer-plugin-ambilight.mjs"
},
"require": {
"types": "./types/artplayer-plugin-ambilight.d.cts",
"types": "./types/runtime.d.cts",
"default": "./dist/artplayer-plugin-ambilight.js"
}
},
"./legacy": {
"import": {
"types": "./types/artplayer-plugin-ambilight.d.cts",
"default": "./dist/artplayer-plugin-ambilight.legacy.js"
},
"require": {
"types": "./types/artplayer-plugin-ambilight.d.cts",
"default": "./dist/artplayer-plugin-ambilight.legacy.js"
}
}
},
"main": "./dist/artplayer-plugin-ambilight.js",
@@ -48,6 +48,9 @@
"*": {
"legacy": [
"types/artplayer-plugin-ambilight.d.ts"
],
"runtime": [
"types/runtime.d.ts"
]
}
}
@@ -1,4 +0,0 @@
// eslint-disable-next-line ts/no-require-imports -- Preserve the CommonJS export-assignment namespace.
import artplayerPluginAmbilight = require('./artplayer-plugin-ambilight.js')
export = artplayerPluginAmbilight
@@ -1,2 +0,0 @@
export { default } from './artplayer-plugin-ambilight.js'
export type { Callable, Factory, Option, Result } from './artplayer-plugin-ambilight.js'
@@ -1,40 +1,35 @@
/* eslint-disable ts/no-redeclare -- Callable and public type namespace intentionally merge. */
import type Artplayer from 'artplayer'
declare namespace artplayerPluginAmbilight {
interface Option {
/** CSS blur radius. @default '50px' */
blur?: string
/** Grid cell opacity. @default 0.5 */
opacity?: number
/** Maximum sampling frequency in frames per second. @default 10 */
frequency?: number
/** Historical input retained for compatibility; runtime uses a fixed z-index of 9. */
zIndex?: number
/** Background color transition duration in seconds. @default 0.3 */
duration?: number
}
interface Result {
name: 'artplayerPluginAmbilight'
/** Start sampling; does nothing after the player is destroyed. */
start: () => void
/** Stop sampling while retaining the last colors. */
stop: () => void
}
interface Callable {
(option?: Option): (art: Artplayer) => Result
/** Keep Parameters extraction identical to the published 1.1.0 signature. */
(option: Option): (art: Artplayer) => Result
}
interface Factory extends Callable {
/** Same function, including historical require(package).default calls. */
readonly default: Factory
}
export interface Option {
/** CSS blur radius. @default '50px' */
blur?: string
/** Grid cell opacity. @default 0.5 */
opacity?: number
/** Maximum sampling frequency in frames per second. @default 10 */
frequency?: number
/** Historical input retained for compatibility; runtime uses a fixed z-index of 9. */
zIndex?: number
/** Background color transition duration in seconds. @default 0.3 */
duration?: number
}
declare const artplayerPluginAmbilight: artplayerPluginAmbilight.Factory
export = artplayerPluginAmbilight
export as namespace artplayerPluginAmbilight
export interface Result {
name: 'artplayerPluginAmbilight'
/** Start sampling; does nothing after the player is destroyed. */
start: () => void
/** Stop sampling while retaining the last colors. */
stop: () => void
}
/** Published 1.1.0 factory shape; the options argument remains required. */
export type Callable = (option: Option) => (art: Artplayer) => Result
export type Factory = Callable
/** Accurate optional invocation and CommonJS self alias, exposed by /runtime. */
export interface RuntimeFactory {
(option?: Option): (art: Artplayer) => Result
readonly default: RuntimeFactory
}
declare const artplayerPluginAmbilight: (option: Option) => (art: Artplayer) => Result
export default artplayerPluginAmbilight
@@ -0,0 +1,10 @@
import type { RuntimeFactory } from './artplayer-plugin-ambilight.js'
declare const artplayerPluginAmbilight: RuntimeFactory
// eslint-disable-next-line ts/no-redeclare -- Ambient merge exposes types on the CommonJS factory.
declare namespace artplayerPluginAmbilight {
type Option = import('./artplayer-plugin-ambilight.js').Option
type Result = import('./artplayer-plugin-ambilight.js').Result
type RuntimeFactory = import('./artplayer-plugin-ambilight.js').RuntimeFactory
}
export = artplayerPluginAmbilight
@@ -0,0 +1,5 @@
import type { RuntimeFactory } from './artplayer-plugin-ambilight.js'
declare const artplayerPluginAmbilight: RuntimeFactory
export default artplayerPluginAmbilight
export type { Option, Result, RuntimeFactory } from './artplayer-plugin-ambilight.js'
+10
View File
@@ -0,0 +1,10 @@
import type { RuntimeFactory } from './artplayer-plugin-ambilight.js'
declare const artplayerPluginAmbilight: RuntimeFactory
// eslint-disable-next-line ts/no-redeclare -- Ambient merge exposes types on the CommonJS factory.
declare namespace artplayerPluginAmbilight {
type Option = import('./artplayer-plugin-ambilight.js').Option
type Result = import('./artplayer-plugin-ambilight.js').Result
type RuntimeFactory = import('./artplayer-plugin-ambilight.js').RuntimeFactory
}
export = artplayerPluginAmbilight
@@ -3,10 +3,18 @@
Public entry: `src/index.ts`; public declarations remain in
`types/artplayer-proxy-canvas.d.ts`. The factory accepts the optional drawing callback,
then synchronously returns an actual HTMLCanvasElement when registered with ArtPlayer.
The public namespace preserves the optional callback and exact Canvas return extraction.
The root preserves the latest npm 1.1.0 pure factory, optional callback and exact Canvas
return extraction. Do not attach a required default member to this type: that breaks
assigning an ordinary historical replacement function back to typeof factory.
MediaCanvas is an explicit type view, not a narrowed factory result. CommonJS and browser
factory.default are self aliases; .d.mts/.d.cts wrappers distinguish ESM and CommonJS
resolution, and typesVersions keeps the legacy path available to older TypeScript.
factory.default remain runtime self aliases. Accurate RuntimeFactory lives behind the
additive /runtime entry, whose runtime.d.mts/runtime.d.cts bridges distinguish ESM and
CommonJS and whose runtime.d.ts supports old Node10 export= resolution. Both entrypoints
load the same existing dist files. The old root declaration bridges remain packaged,
but root/legacy conditional types deliberately select the original .d.ts module shape.
Root NodeNext ESM therefore keeps the published namespace shape; it is not silently made
callable. Use /runtime for precise ESM default calls. The package README documents the
approved 1.0 export= migration; no JavaScript calls were removed by this type decision.
Historical JavaScript falsy callback arguments remain ignored at runtime; the typed public
callback remains optional and function-valued.
@@ -72,8 +80,13 @@ geometry and is separate from native media pixel acceptance. npm core 5.1.7 has
option and is only a native-player capability control.
`yarn test:canvas-types-package` installs real archived and candidate packages outside the
workspace, verifies installed bytes and checks old/current compiler modes. The public
namespace also generates the local editor declaration through the semantic generator.
workspace, verifies installed bytes and checks old/current compiler modes. It preserves
the actual old NodeNext direct-import errors and 1.0 export= differences, while testing
latest namespace replacements, plain factory bidirectional assignments, runtime self
identity and negative statements at their exact lines. No skipLibCheck or strictness
relaxation is used. The public declarations also generate the local editor declaration
through the semantic generator; the editor root stays plain, with an explicit
artplayerProxyCanvas.RuntimeFactory assertion for code needing the runtime self alias.
Broader installed artifact contents/deep paths and 8082 demos remain 06.
Real Safari/mobile/device scope remains 05; Windows Playwright WebKit is not a device claim.
See `refactor/canvas-validation.md` and the task evidence for actual outcomes and limitations.
+30 -4
View File
@@ -29,14 +29,19 @@ historical `require('artplayer-proxy-canvas').default` refer to the same factory
## TypeScript
The optional callback and exact `HTMLCanvasElement` return type retain published 1.1.0
inference. `Option`, `Result`, `Factory` and `MediaCanvas` are exported types. The last is
an explicit view for code using supported forwarded media members:
The root and `/legacy` declarations preserve the latest published 1.1.0 factory:
an optional callback, an exact `HTMLCanvasElement` result, and no required factory
properties. Ordinary replacement functions remain assignable in both directions.
`Factory` describes that historical shape. The additive `/runtime` entry uses the same
JavaScript implementation and describes its readonly `default` self alias with
`RuntimeFactory`. It also exports an explicit `MediaCanvas` view:
```ts
import type { MediaCanvas, Option } from 'artplayer-proxy-canvas'
import type { MediaCanvas, Option } from 'artplayer-proxy-canvas/runtime'
import canvas from 'artplayer-proxy-canvas/runtime'
const callback: Option = (context, video) => context.drawImage(video, 0, 0)
canvas.default(callback)
const media = art.template.$video as MediaCanvas
await media.play()
```
@@ -48,6 +53,27 @@ Runtime falsy callback arguments retain historical behavior; declarations accept
optional function, matching the published types. Implementation and test guidance is in
[ARCHITECTURE.md](ARCHITECTURE.md).
### Older TypeScript consumers
The 1.0.0 declaration used `export =` and required a callback; 1.1.0 changed to
`export default` and made the callback optional. These conflicting type shapes cannot
both remain the root factory. Following the approved compatibility policy, the root
keeps 1.1.0. A 1.0.0 TypeScript `import canvas = require('artplayer-proxy-canvas')`
consumer that directly calls `canvas(...)` can select the accurate entry instead:
```ts
import canvas = require('artplayer-proxy-canvas/runtime')
canvas((context, video) => context.drawImage(video, 0, 0))
canvas.default()
```
This works with Node10 resolution even without `esModuleInterop`. Default imports from
`/runtime` work with interoperability enabled and with native NodeNext ESM. Under
NodeNext ESM, the root intentionally preserves 1.1.0's historical declaration namespace
shape (`root.default(...)` after a default import); that declaration shape does not
describe the actual ESM factory. Use a default import from `/runtime` for accurate ESM
typing. Existing JavaScript root/legacy calls and distribution paths keep working.
## Demo
[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-proxy-canvas/index.js&example=canvas)
+15 -12
View File
@@ -19,24 +19,24 @@
],
"exports": {
".": {
"types": "./types/artplayer-proxy-canvas.d.ts",
"import": "./dist/artplayer-proxy-canvas.mjs",
"require": "./dist/artplayer-proxy-canvas.js"
},
"./legacy": {
"types": "./types/artplayer-proxy-canvas.d.ts",
"import": "./dist/artplayer-proxy-canvas.legacy.js",
"require": "./dist/artplayer-proxy-canvas.legacy.js"
},
"./runtime": {
"import": {
"types": "./types/artplayer-proxy-canvas.d.mts",
"types": "./types/runtime.d.mts",
"default": "./dist/artplayer-proxy-canvas.mjs"
},
"require": {
"types": "./types/artplayer-proxy-canvas.d.cts",
"types": "./types/runtime.d.cts",
"default": "./dist/artplayer-proxy-canvas.js"
}
},
"./legacy": {
"import": {
"types": "./types/artplayer-proxy-canvas.d.cts",
"default": "./dist/artplayer-proxy-canvas.legacy.js"
},
"require": {
"types": "./types/artplayer-proxy-canvas.d.cts",
"default": "./dist/artplayer-proxy-canvas.legacy.js"
}
}
},
"main": "./dist/artplayer-proxy-canvas.js",
@@ -48,6 +48,9 @@
"*": {
"legacy": [
"types/artplayer-proxy-canvas.d.ts"
],
"runtime": [
"types/runtime.d.ts"
]
}
}
@@ -1,4 +1,2 @@
// eslint-disable-next-line ts/no-require-imports -- Preserve the CommonJS export-assignment namespace.
import artplayerProxyCanvas = require('./artplayer-proxy-canvas.js')
export = artplayerProxyCanvas
export { default } from './artplayer-proxy-canvas.js'
export type { Callable, Factory, MediaCanvas, Option, Result, RuntimeFactory } from './artplayer-proxy-canvas.js'
@@ -1,2 +1,5 @@
export { default } from './artplayer-proxy-canvas.js'
export type { Callable, Factory, MediaCanvas, Option, Result } from './artplayer-proxy-canvas.js'
import type { Factory } from './artplayer-proxy-canvas.js'
declare const artplayerProxyCanvas: Factory
export default artplayerProxyCanvas
export type { Callable, Factory, MediaCanvas, Option, Result, RuntimeFactory } from './artplayer-proxy-canvas.js'
@@ -1,27 +1,22 @@
/* eslint-disable ts/no-redeclare -- Callable and public type namespace intentionally merge. */
import type Artplayer from 'artplayer'
declare namespace artplayerProxyCanvas {
/** Runs after drawing and bitmap release, before the draw event. */
type Option = (ctx: CanvasRenderingContext2D, video: HTMLVideoElement) => void
/** Runs after drawing and bitmap release, before the draw event. */
export type Option = (ctx: CanvasRenderingContext2D, video: HTMLVideoElement) => void
/** Preserve the exact published return type and its assignability. */
type Result = HTMLCanvasElement
/** Preserve the exact published 1.1.0 return type and its assignability. */
export type Result = HTMLCanvasElement
/** Opt-in view of supported enumerable media members; native Canvas members win. */
type MediaCanvas = HTMLCanvasElement & Pick<HTMLVideoElement, Exclude<keyof HTMLVideoElement, keyof HTMLCanvasElement>>
export type Factory = (option?: Option) => (art: Artplayer) => Result
export type Callable = Factory
interface Callable {
/** Keep Parameters extraction optional, as published in 1.1.0. */
(option?: Option): (art: Artplayer) => Result
}
/** Explicit view of forwarded media members; native Canvas members win. */
export type MediaCanvas = HTMLCanvasElement & Pick<HTMLVideoElement, Exclude<keyof HTMLVideoElement, keyof HTMLCanvasElement>>
interface Factory extends Callable {
/** Same factory, for historical require(package).default calls. */
readonly default: Factory
}
/** Opt-in runtime identity; the historical root factory has no required properties. */
export interface RuntimeFactory extends Factory {
readonly default: RuntimeFactory
}
declare const artplayerProxyCanvas: artplayerProxyCanvas.Factory
export = artplayerProxyCanvas
export as namespace artplayerProxyCanvas
declare const artplayerProxyCanvas: (option?: Option) => (art: Artplayer) => Result
export default artplayerProxyCanvas
@@ -0,0 +1,12 @@
import type { RuntimeFactory } from './artplayer-proxy-canvas.js'
declare const artplayerProxyCanvas: RuntimeFactory
// eslint-disable-next-line ts/no-redeclare -- Ambient merge exposes types on the CommonJS factory.
declare namespace artplayerProxyCanvas {
type Option = import('./artplayer-proxy-canvas.js').Option
type Result = import('./artplayer-proxy-canvas.js').Result
type Factory = import('./artplayer-proxy-canvas.js').Factory
type MediaCanvas = import('./artplayer-proxy-canvas.js').MediaCanvas
type RuntimeFactory = import('./artplayer-proxy-canvas.js').RuntimeFactory
}
export = artplayerProxyCanvas
@@ -0,0 +1,5 @@
import type { RuntimeFactory } from './artplayer-proxy-canvas.js'
declare const artplayerProxyCanvas: RuntimeFactory
export default artplayerProxyCanvas
export type { Factory, MediaCanvas, Option, Result, RuntimeFactory } from './artplayer-proxy-canvas.js'
+12
View File
@@ -0,0 +1,12 @@
import type { RuntimeFactory } from './artplayer-proxy-canvas.js'
declare const artplayerProxyCanvas: RuntimeFactory
// eslint-disable-next-line ts/no-redeclare -- Ambient merge exposes types on the CommonJS factory.
declare namespace artplayerProxyCanvas {
type Option = import('./artplayer-proxy-canvas.js').Option
type Result = import('./artplayer-proxy-canvas.js').Result
type Factory = import('./artplayer-proxy-canvas.js').Factory
type MediaCanvas = import('./artplayer-proxy-canvas.js').MediaCanvas
type RuntimeFactory = import('./artplayer-proxy-canvas.js').RuntimeFactory
}
export = artplayerProxyCanvas
@@ -0,0 +1,61 @@
{
"schemaVersion": 1,
"task": "PKG-FACTORY-01",
"capturedAt": "2026-09-13T08:50:20.867Z",
"status": "done",
"policy": "refactor/type-compatibility-policy.md",
"baseline": "Latest actual npm 1.1.0 for both packages; earlier 1.0.0 conflicts explicitly migrated, not silently claimed compatible.",
"reports": [
{
"name": "canvas",
"report": "refactor/.cache/canvas-package-types-2pltVA/report.json",
"sha256": "a1c381b23928f4bcc3fe4a8e54d90277a1ff6f4a4ec8bc0423ba1a6117cc18d1",
"archiveSha256": "da8cb6d8eb0f01cd97a9e4bccdebdab028fb4f985e54ff45743a5a6661980e62",
"modes": 17,
"candidateModes": 7,
"negativeStatementsPerCandidateMode": 14,
"historicalDiagnosticsRetained": true,
"sourceAndDistUnchanged": true
},
{
"name": "ambilight",
"report": "refactor/.cache/ambilight-package-types-sHTRf5/report.json",
"sha256": "4efcad331c5879d62b50d7333538bf908959b3ccda70933a37c233e804d7ff62",
"archiveSha256": "1a07bde5804eb63bd0436d0f63af6a9d0d35873e30e120b9ff1553806729649e",
"modes": 17,
"candidateModes": 7,
"negativeStatementsPerCandidateMode": 15,
"historicalDiagnosticsRetained": true,
"sourceAndDistUnchanged": true
}
],
"checks": [
{
"file": "refactor/.cache/factory01-types-final.log",
"sha256": "e73fd496e06fd0eaba1646183d4f55e0e57350d24f3cdbbefebee5db40301476"
},
{
"file": "refactor/.cache/factory01-typecheck.log",
"sha256": "876eed09bef39ddecdbdf7202ba3b5d0ae062502da8f6b8d87d73efe90cab8d9"
},
{
"file": "refactor/.cache/factory01-editor.log",
"sha256": "c61d3d927caaf293c1442a2bbcc88ac3c79d25f6f7b16efcdb05feb7e4b02c79"
},
{
"file": "refactor/.cache/factory01-parent-lint.log",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
],
"verified": [
"Plain root factories assign both ways, exact Parameters/ReturnType.",
"Canvas optional callback and exact HTMLCanvasElement result; Ambilight required option and exact synchronous Result.",
"Root/legacy preserve 1.1.0 NodeNext namespace; runtime provides callable native ESM and CommonJS self alias.",
"Each actual tarball installed outside workspace, offline frozen reinstall and exact installed bytes.",
"Generated editor declarations and strict old/current compilers, actual main/legacy/global/ESM factory identities."
],
"limitations": [
"No source or emitted JavaScript changed; no new browser/device evidence claimed.",
"Package 05/06 and release reviews remain separate tasks."
]
}
@@ -0,0 +1,25 @@
# PKG-FACTORY-01 历史工厂类型修复完成
根据用户2026-09-13确认及ADR-025,Canvas和Ambilight根/legacy声明保留真实npm
1.1.0的完整纯函数形状。Canvas回调可选、结果精确为HTMLCanvasElement;Ambilight
参数必填、字段可选、结果仍为同步start/stop。普通替代函数可双向赋值,不再要求
必填default成员,也不靠重载只保住Parameters。
新增/runtime映射到现有JS文件,独立描述可选调用及readonly self.default。
ESM/CommonJS/Node10解析各有准确声明。根入口保留1.1.0 NodeNext ESM历史namespace
及原直接调用诊断;不能以修复该旧诊断为由破坏原合法namespace赋值。较早1.0.0的
export=及参数/字段差异有真实归档对照,并在包README给出/runtime迁移示例。
两包最终tarball分别验证17配置,合计34配置。候选各7配置均无正例诊断,Canvas
每配置拒绝14个指定反例,Ambilight为15个;反例按语句行核对。安装目录位于工作区
之外,执行离线安装、frozen重装、归档及安装字节比对;不借用workspace类型软链。
旧1.0及1.1诊断保留,不将其混作候选失败或静默删除。
8项专项测试核对真实历史声明、完整工厂赋值、编辑器生成和main/legacy/global/ESM
身份。共享typecheck在修改当时验证361个生产TS文件及所有已有消费模式,通过;
后续Chromecast新增源码不计入该快照。定向lint通过。可追溯报告和哈希见
[验证记录](../baselines/factory-compatibility-validation.json)。
生产源码和JS产物未改,已有运行时行为保留;本次没有新增浏览器或设备验证声明。
FACTORY-TYPE-01已关闭,包05/06和npm发布复盘仍须继续。一个任务一个独立完成提交,
不推送、不发布。
+13 -11
View File
@@ -1,15 +1,17 @@
# Canvas / Ambilight 工厂类型兼容取舍(已确认,待实施)
# Canvas / Ambilight 工厂类型兼容取舍(已确认并实施)
PKG-FACTORY-01 已复现当前声明的问题,但尚未修改生产声明,不能标记完成。
2026-09-13用户已接受统一规则,见[确认记录](type-compatibility-policy.md)。本页推荐
方向已授权;此前Ads单项授权之外的待决状态由此次确认替代,实施和验证仍须完成。
2026-09-13用户已接受统一规则,见[确认记录](type-compatibility-policy.md)。
PKG-FACTORY-01随后完成两包声明、/runtime、迁移文档及真实安装验证,见
[完成记录](changes/2026-09-13-PKG-FACTORY-01-compatible-types.md)和
[验证证据](baselines/factory-compatibility-validation.json)。以下矩阵保留决策时的
历史方案对照,不能将其中故意展示的失败方案视为当前生产实现。
## 问题与真实发布对照
两包 npm 1.0.0 声明均为 `export =`,npm 1.1.0 均改为 `export default`。
归档由各包 release 基线核验;测试读取真实 tarball 中的声明,不用工作区文件替代。
当前为了兼容运行时 `require(package)` 和 `require(package).default`,公开 Factory
修复前为了兼容运行时 `require(package)` 和 `require(package).default`,公开 Factory
增加了必填 `.default`。这使 1.1.0 原本合法的 `const replacement: typeof factory = ...`
报 TS2741。Ambilight 另外增加的可选参数重载也使只接受必填参数的旧替代工厂报 TS2322;
保留最后一个必填重载只能保住 Parameters,无法保住整个函数的赋值关系。
@@ -63,17 +65,17 @@ import module = require('artplayer-plugin-ambilight')
module.default({})
```
完整候选声明由 `scripts/factory-assignability.mjs` 的 declaration(pkg, 'latest-default')
生成。当前它只是可编译的核心签名提案,尚未加入全部命名类型、格式包装和安装验收,
属于已批准方向,尚不属于已经实施并验证的生产修复。
提案核心签名仍由 `scripts/factory-assignability.mjs` 的 declaration(pkg, 'latest-default')
生成用于历史对照。实际生产声明已加入命名类型与/runtime格式桥,并完成34个真实安装
配置验证。根NodeNext ESM保留1.1.0 namespace形状;准确默认导入应使用/runtime。
## 决策与后续
- 状态:用户已明确接受上述较早版本TypeScript导入迁移;不因批准而跳过验证。
- 下一步:修改两包声明和包装,补充 RuntimeFactory、全工厂赋值测试、编辑器和包内文档,
重建并执行真实安装消费矩阵;保留逐版本负例,验证 JS 产物与浏览器证据,单独完成任务提交。
- 已完成:两包声明、RuntimeFactory、全工厂赋值测试、编辑器与文档、真实安装消费矩阵。
JS产物没有修改,现有实际分发身份测试通过,不声称新增浏览器设备证据。
- 仍不得把optional .default、any、skipLibCheck或删除历史消费者当作修复。
- PKG-CANVAS-05、PKG-AMBILIGHT-05、REL-01 仍依赖本任务完成。
- PKG-CANVAS-05、PKG-AMBILIGHT-05、REL-01的此项依赖已满足,各自剩余验证继续实施。
证据:[编译矩阵](baselines/factory-compatibility-proposals.json)、
[原始回归](baselines/factory-assignment-gaps.json)。
+5 -5
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 228 项,范围 22 个包及工作区/示例。
状态:todo 79 / doing 17 / blocked 0 / done 132 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 78 / doing 17 / blocked 0 / done 133 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -289,7 +289,7 @@
| --- | --- | --- | --- | --- | --- | --- |
| PKG-CAST-01 | artplayer-plugin-chromecast<br>核对包契约与历史用法 | BASE-05 | url/sdk/icon/mimeType、loader、会话、全局 SDK 与结果 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-CAST-02 | artplayer-plugin-chromecast<br>建立特有行为与错误测试 | PKG-CAST-01, ENG-03, ENG-05 | SDK 脚本加载失败/重入、无设备/拒绝、重复实例与销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-CAST-03 | artplayer-plugin-chromecast<br>整理内部职责与资源 | PKG-CAST-02, CORE-11, CORE-13 | SDK singleton/实例订阅/会话与控件职责分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
| PKG-CAST-03 | artplayer-plugin-chromecast<br>整理内部职责与资源 | PKG-CAST-02, CORE-11, CORE-13 | SDK singleton/实例订阅/会话与控件职责分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | doing |
| PKG-CAST-04 | artplayer-plugin-chromecast<br>迁移自有源码和公开类型 | PKG-CAST-03, ENG-04, ENG-06, CORE-07 | Cast 能力 adapter、全局对象和元数据类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
| PKG-CAST-05 | artplayer-plugin-chromecast<br>验证新旧核心和组合 | PKG-CAST-04, CORE-22 | stub 测试之外记录实际 Cast 设备会话、源更新和断开 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-CAST-06 | artplayer-plugin-chromecast<br>验证分发并同步文档 | PKG-CAST-05, ENG-07 | chromecast.js、SDK URL/权限能力及待支持环境说明 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
@@ -417,7 +417,7 @@
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| PKG-FACTORY-01 | artplayer-proxy-canvas, artplayer-plugin-ambilight<br>修复已迁移工厂的反向赋值兼容 | PKG-CANVAS-04, PKG-AMBILIGHT-04, PKG-DPIP-04 | 复现两包旧typeof工厂替换与新.default/可选重载冲突,修复公开类型并补安装消费者、产物和文档 | 默认工厂的历史合法反向赋值继续通过;既有类型/运行时导出逐版本核对,不能只验证Parameters或直接调用 | H | doing |
| PKG-FACTORY-01 | artplayer-proxy-canvas, artplayer-plugin-ambilight<br>修复已迁移工厂的反向赋值兼容 | PKG-CANVAS-04, PKG-AMBILIGHT-04, PKG-DPIP-04 | 复现两包旧typeof工厂替换与新.default/可选重载冲突,修复公开类型并补安装消费者、产物和文档 | 默认工厂的历史合法反向赋值继续通过;既有类型/运行时导出逐版本核对,不能只验证Parameters或直接调用 | H | done |
## 2 测试与工程基础
@@ -533,7 +533,7 @@
- PKG-ASR-02: [记录](changes/2026-09-13-PKG-ASR-02-audio-baseline.md) [记录](baselines/asr-audio-validation.json)
- PKG-ASR-03: [记录](changes/2026-09-13-PKG-ASR-03-audio-ownership.md) [记录](baselines/asr-ownership-validation.json)
- PKG-ASR-04: [记录](changes/2026-09-13-PKG-ASR-04-public-types.md) [记录](baselines/asr-types-validation.json) [记录](baselines/asr-type-diagnostics.json)
- PKG-ASR-05: [记录](baselines/asr-volume-validation.json) [记录](baselines/asr-fallback-validation.json) [记录](baselines/asr-core.json) [记录](baselines/asr-local-core-validation.json) [记录](changes/2026-09-13-PKG-ASR-05-local-core-checkpoint.md) [记录](baselines/asr-combinations-validation.json) [记录](changes/2026-09-13-PKG-ASR-05-cors-combinations.md) [记录](baselines/asr-explicit-capture-validation.json)
- PKG-ASR-05: [记录](baselines/asr-volume-validation.json) [记录](baselines/asr-fallback-validation.json) [记录](baselines/asr-core.json) [记录](baselines/asr-local-core-validation.json) [记录](changes/2026-09-13-PKG-ASR-05-local-core-checkpoint.md) [记录](baselines/asr-combinations-validation.json) [记录](changes/2026-09-13-PKG-ASR-05-cors-combinations.md) [记录](baselines/asr-explicit-capture-validation.json) [记录](changes/2026-09-13-PKG-ASR-05-editor-generation.md)
- PKG-ADS-01: [记录](baselines/ads-release.json) [记录](baselines/ads-contract.md) [记录](baselines/ads-contract-validation.json) [记录](changes/2026-09-12-PKG-ADS-01-contract.md) [记录](scripts/ads-contract.test.mjs)
- PKG-ADS-02: [记录](changes/2026-09-12-PKG-ADS-02-tests.md) [记录](ads-validation.md) [记录](baselines/ads-validation.json)
- PKG-ADS-03: [记录](changes/2026-09-12-PKG-ADS-03-lifecycle.md) [记录](ads-validation.md) [记录](baselines/ads-lifecycle-validation.json)
@@ -571,7 +571,7 @@
- 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) [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-public-types.md) [记录](baselines/thumbnail-public-types-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-emitter.md) [记录](baselines/thumbnail-emitter-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)
- 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) [记录](type-compatibility-policy.md) [记录](baselines/factory-compatibility-validation.json) [记录](changes/2026-09-13-PKG-FACTORY-01-compatible-types.md)
- CORE-25: [记录](changes/2026-09-13-CORE-25-defaults-ssr.md) [记录](baselines/defaults-ssr-validation.json)
- ENG-12: [记录](changes/2026-09-13-ENG-12-library-public.md) [记录](baselines/library-public-validation.json)
- PKG-ASR-07: [记录](changes/2026-09-13-PKG-ASR-07-playback-volume.md) [记录](baselines/asr-volume-validation.json)
+12
View File
@@ -1,5 +1,17 @@
# 进度与证据
## PKG-FACTORY-01 历史工厂类型修复完成
Canvas/Ambilight落实已批准规则:保留真实npm1.1.0根工厂与NodeNext namespace,
/runtime提供准确可选调用与self.default;1.0冲突迁移已入两包README。共34个真实
安装配置、14个候选配置全部正例通过,每配置分别拒绝14/15个指定反例。8项专项
测试、共享strict消费检查和编辑器生成通过。见[变更](changes/2026-09-13-PKG-FACTORY-01-compatible-types.md)
及[证据](baselines/factory-compatibility-validation.json)。FACTORY-TYPE-01关闭;
包05/06及发布验收仍独立。ASR编辑器导出冲突另已修复并提交111710703。
228项:133 done、17 doing、78 todo。CAST03源码迁移开始;VTT/MultiSub类型规则
由独立子代理继续落实。未推送或发布。
## PKG-CAST-02 Chromecast 错误基线完成
93项受控历史测试通过,其中65项新增错误/生命周期观察。首次会话结果误用、已就绪
+1 -1
View File
@@ -167,7 +167,7 @@
| DPIP-STYLE-01 | resolved / 已复现 | Document PiP 1.1.0 and frozen source duplicate style IDs when evaluated before DOMContentLoaded | PKG-DPIP-03 |
| DPIP-MEDIA-01 | open / 已复现 | WebKit reports styled width640 for width320 media in the controlled Document PiP iframe matrix | PKG-DPIP-05 |
| DPIP-RESOLVE-01 | resolved / 已复现 | Historical Document PiP exports hide declarations or infer an uncallable CommonJS namespace in modern resolution | PKG-DPIP-04, PKG-DPIP-06 |
| FACTORY-TYPE-01 | open / 已复现 | Canvas and Ambilight default self property narrows assignability of historical factory replacements | PKG-FACTORY-01 |
| FACTORY-TYPE-01 | resolved / 已复现 | Canvas and Ambilight default self property narrows assignability of historical factory replacements | PKG-FACTORY-01 |
| MB-LICENSE-01 | open / 已复现 | Historical MediaBunny proxy bundles embed SDK code without MPL notice markers or separate license/source provenance | PKG-MB-10 |
| MB-LIFE-01 | open / 已复现 | MediaBunny load, track replacement, timeout, RAF and audio callbacks lack one shared terminal owner | PKG-MB-02, PKG-MB-03, PKG-MB-04, PKG-MB-05, PKG-MB-06, PKG-MB-07, PKG-MB-09 |
| MB-TYPE-01 | resolved / 已复现 | MediaBunny proxy declares only Canvas while exposing a shim and changes CommonJS export generation between releases | PKG-MB-08, PKG-MB-10 |
+11 -3
View File
@@ -3632,18 +3632,26 @@
{
"id": "FACTORY-TYPE-01",
"title": "Canvas and Ambilight default self property narrows assignability of historical factory replacements",
"status": "open",
"status": "resolved",
"confirmation": "reproduced",
"owners": [
"PKG-FACTORY-01"
],
"compatibleResolution": "Preserve historical complete factory assignments while modeling actual export variants explicitly; do not equate unchanged Parameters with complete callable compatibility. The 72-case proposal matrix demonstrates that required or optional self.default and pure export-default/export-equals shapes each preserve different historical consumers; the proposed latest-release type baseline is awaiting explicit user acceptance.",
"compatibleResolution": "User accepted latest published 1.1.0 root type shapes on 2026-09-13 (ADR-025). Pure factories preserve reverse assignments and NodeNext namespace; additive runtime declarations model optional calls and self aliases. Earlier 1.0 conflicts have explicit migrations.",
"closureCriteria": "Both historical and candidate replacement fixtures, all supported export/declaration paths and installed consumers verified; unresolved variant conflicts require an explicit decision rather than silent narrowing.",
"evidence": [
"refactor/baselines/factory-assignment-gaps.json",
"refactor/changes/2026-09-12-PKG-DPIP-04-types.md",
"refactor/baselines/factory-compatibility-proposals.json",
"refactor/factory-compatibility-decision.md"
"refactor/factory-compatibility-decision.md",
"refactor/type-compatibility-policy.md",
"refactor/baselines/factory-compatibility-validation.json",
"refactor/changes/2026-09-13-PKG-FACTORY-01-compatible-types.md"
],
"resolutionRationale": "34 actual isolated installation cases, 14 zero-diagnostic candidate modes with 14/15 invalid statements each, eight focused tests and shared strict consumer checks; old diagnostics and legal namespace replacements retained. JavaScript files unchanged.",
"resolutionEvidence": [
"refactor/baselines/factory-compatibility-validation.json",
"refactor/changes/2026-09-13-PKG-FACTORY-01-compatible-types.md"
]
},
{
+46 -14
View File
@@ -50,8 +50,9 @@ async function main() {
const runtimeFile = path.join(consumer, 'exports.cjs')
fs.writeFileSync(runtimeFile, plugin.label === 'candidate'
? `const assert = require('node:assert/strict');
for (const name of ['artplayer-plugin-ambilight', 'artplayer-plugin-ambilight/legacy']) { const factory = require(name); assert.equal(typeof factory, 'function'); assert.equal(factory.default, factory); assert.equal(typeof factory(), 'function'); }
import('artplayer-plugin-ambilight').then(module => { assert.deepEqual(Object.keys(module), ['default']); assert.equal(module.default.default, module.default); }).catch(error => { console.error(error); process.exitCode = 1; });`
for (const name of ['artplayer-plugin-ambilight', 'artplayer-plugin-ambilight/legacy', 'artplayer-plugin-ambilight/runtime']) { const factory = require(name); assert.equal(typeof factory, 'function'); assert.equal(factory.default, factory); assert.equal(typeof factory(), 'function'); }
assert.equal(require('artplayer-plugin-ambilight/runtime'), require('artplayer-plugin-ambilight'));
Promise.all(['artplayer-plugin-ambilight', 'artplayer-plugin-ambilight/runtime', 'artplayer-plugin-ambilight/legacy'].map(name => import(name))).then(([root, runtime, legacy]) => { assert.deepEqual(Object.keys(root), ['default']); assert.deepEqual(Object.keys(runtime), ['default']); assert.equal(runtime.default, root.default); assert.equal(root.default.default, root.default); assert.equal(legacy.default, require('artplayer-plugin-ambilight/legacy')); }).catch(error => { console.error(error); process.exitCode = 1; });`
: `const assert = require('node:assert/strict'); const value = require('artplayer-plugin-ambilight'); assert.equal(typeof value, '${plugin.version === '1.0.0' ? 'object' : 'function'}'); assert.equal(typeof (value.default || value)({}), 'function');`)
fs.writeFileSync(path.join(output, `${plugin.label}-runtime.log`), run([runtimeFile], consumer))
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
@@ -60,14 +61,24 @@ import('artplayer-plugin-ambilight').then(module => { assert.deepEqual(Object.ke
for (const [compiler, mode] of modes) {
const next = mode.startsWith('nodenext')
const extension = next ? mode.endsWith('-cjs') ? 'cts' : 'mts' : 'ts'
const source = fs.readFileSync(path.join(workspace, plugin.label === 'candidate' ? 'test/types/ambilight.ts' : 'refactor/fixtures/consumers/ambilight-published.ts'), 'utf8')
let source = fs.readFileSync(path.join(workspace, plugin.label === 'candidate' ? 'test/types/ambilight.ts' : 'refactor/fixtures/consumers/ambilight-published.ts'), 'utf8')
if (plugin.label === 'candidate' && mode === 'nodenext-esm') {
source = source
.replace('import ambilight from \'artplayer-plugin-ambilight\'', 'import ambilightModule from \'artplayer-plugin-ambilight\'\nconst ambilight = ambilightModule.default')
.replace('import legacy from \'artplayer-plugin-ambilight/legacy\'', 'import legacyModule from \'artplayer-plugin-ambilight/legacy\'\nconst legacy = legacyModule.default')
}
if (mode.endsWith('-no-interop'))
source = source.replace('import runtime from \'artplayer-plugin-ambilight/runtime\'', 'import runtime = require(\'artplayer-plugin-ambilight/runtime\')')
const filename = path.join(consumer, `consumer.${extension}`)
const historical = path.join(consumer, `historical.${extension}`)
fs.copyFileSync(path.join(workspace, 'refactor/fixtures/consumers/ambilight-published.ts'), historical)
const files = plugin.label === 'candidate' ? [filename, historical] : [filename]
if (mode === 'nodenext-cjs' && plugin.label !== 'published-1.1.0') {
const commonjs = path.join(consumer, 'commonjs.cts')
fs.writeFileSync(commonjs, `import ambilight = require('artplayer-plugin-ambilight'); ambilight({ blur: '50px', opacity: 0.5, frequency: 10, zIndex: 9, duration: 0.3 }); ${plugin.label === 'candidate' ? 'ambilight.default();' : ''}`)
const files = [filename]
if (plugin.label === 'candidate' && mode !== 'bundler-esm' && mode !== 'nodenext-esm') {
const commonjs = path.join(consumer, `commonjs.${extension}`)
fs.writeFileSync(commonjs, `import ambilight = require('artplayer-plugin-ambilight'); import legacy = require('artplayer-plugin-ambilight/legacy'); import runtime = require('artplayer-plugin-ambilight/runtime');
import type Artplayer from 'artplayer';
const replacement: typeof ambilight.default = (_option) => (_art: Artplayer) => ({name: 'artplayerPluginAmbilight', start() {}, stop() {}});
const legacyReplacement: typeof legacy.default = replacement;
ambilight.default({}); legacy.default({}); runtime(); runtime.default();
const option: runtime.Option = {}; runtime(option); void legacyReplacement;`)
files.push(commonjs)
}
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: !mode.endsWith('-no-interop'), 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 }
@@ -78,22 +89,43 @@ import('artplayer-plugin-ambilight').then(module => { assert.deepEqual(Object.ke
const actual = fs.realpathSync(file.fileName)
assert(actual.startsWith(fs.realpathSync(consumer) + path.sep) || (program.isSourceFileDefaultLibrary(file) && path.dirname(actual) === fs.realpathSync(path.dirname(compiler.sys.getExecutingFilePath()))), `Types escaped installed consumer: ${actual}`)
}
return compiler.getPreEmitDiagnostics(program).map(item => ({ code: item.code, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n') }))
return compiler.getPreEmitDiagnostics(program).map(item => ({ code: item.code, line: item.file && item.start !== undefined ? item.file.getLineAndCharacterOfPosition(item.start).line + 1 : null, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n').replaceAll(consumer.replaceAll('\\', '/'), '<consumer>') }))
}
const diagnostics = compile(source)
const historicalFailure = plugin.label === 'published-1.1.0' && mode === 'nodenext-esm'
assert.deepEqual(diagnostics.map(item => item.code), historicalFailure ? [2349, 2349, 2322, 2344] : [], `${plugin.label} ${compiler.version} ${mode}`)
const invalid = plugin.label === 'candidate' ? compile(source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')) : []
if (plugin.label === 'candidate')
assert.equal(invalid.length, 10, 'Installed declarations must reject all invalid uses')
matrix.push({ plugin: plugin.label, compiler: compiler.version, mode, historicalFailure, diagnostics, invalid })
if (plugin.label === 'candidate') {
assert.equal(invalid.length, 15, 'Installed declarations must reject all invalid uses')
const expectedLines = []
let line = 1
for (const text of source.split('\n')) {
if (text.startsWith('// @ts-expect-error'))
expectedLines.push(line)
else line++
}
assert.deepEqual(invalid.map(item => item.line).sort((a, b) => a - b), expectedLines, 'Each invalid statement must fail at its own line')
}
const published = plugin.label === 'candidate' ? compile(fs.readFileSync(path.join(workspace, 'refactor/fixtures/consumers/ambilight-published.ts'), 'utf8')) : diagnostics
assert.deepEqual(published.map(item => item.code), (plugin.label === 'candidate' || plugin.label === 'published-1.1.0') && mode === 'nodenext-esm' ? [2349, 2349, 2322, 2344] : [], 'Preserve exact historical direct-consumer diagnostic codes')
const namespace = plugin.label !== 'published-1.0.0' && mode === 'nodenext-esm'
? compile(`import ambilight from 'artplayer-plugin-ambilight'; import type Artplayer from 'artplayer';
const replacement: typeof ambilight.default = (_option) => (_art: Artplayer) => ({name: 'artplayerPluginAmbilight', start() {}, stop() {}});
ambilight.default({}); void replacement;`)
: []
assert.deepEqual(namespace, [], 'Latest published NodeNext namespace remains valid')
const olderRequire = mode !== 'bundler-esm' && mode !== 'nodenext-esm'
? compile(`import ambilight = require('artplayer-plugin-ambilight'); ambilight({blur: '50px', opacity: 0.5, frequency: 10, zIndex: 9, duration: 0.3});`)
: []
assert.deepEqual(olderRequire.map(item => item.code), mode !== 'bundler-esm' && mode !== 'nodenext-esm' && plugin.label !== 'published-1.0.0' ? [2349] : [], 'Earlier export-assignment typing conflicts are retained and documented')
matrix.push({ plugin: plugin.label, compiler: compiler.version, mode, historicalFailure, diagnostics, invalid, published, namespace, olderRequire })
}
}
finally {
removeConsumer(consumer)
}
}
writeJson(path.join(output, 'report.json'), { suite: 'ambilight-isolated-package-types', introducedBy: 'PKG-AMBILIGHT-04', scope: 'Two actual published plugins and packed candidate installed outside workspace with packed candidate core; offline and frozen reinstall, exact bytes, compiler/default/import= contracts. Published 1.0.0 required option fields became optional in 1.1.0; candidate retains 1.1.0 extraction. This is not proxy, device or complete distribution acceptance.', packages, published: candidates.slice(0, 2).map(pkg => ({ label: pkg.label, archive: pkg.archive, sha256: pkg.sha256 })), matrix })
writeJson(path.join(output, 'report.json'), { suite: 'ambilight-isolated-package-types', introducedBy: 'PKG-FACTORY-01', scope: 'Two actual published plugins and packed candidate installed outside workspace with packed candidate core; offline and frozen reinstall, exact bytes, full factory replacements and required root option; runtime optional calls and self alias. Published 1.1.0 NodeNext ESM namespace and four old direct-call diagnostics are retained. Published 1.0.0 export-assignment and required fields have documented migration. This is not proxy, device or complete distribution acceptance.', packages, published: candidates.slice(0, 2).map(pkg => ({ label: pkg.label, archive: pkg.archive, sha256: pkg.sha256 })), matrix })
console.log(`Ambilight installed types passed: ${matrix.length} cases; ${output}`)
}
+21 -7
View File
@@ -14,14 +14,23 @@ import { readMember } from './releases.mjs'
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
test('Ambilight current types retain inference, optional runtime arguments and ten invalid-use rejections', () => {
function publicSource(source, mode) {
return mode === 'nodenext-esm'
? source.replace('import ambilight from \'artplayer-plugin-ambilight\'', 'import ambilightModule from \'artplayer-plugin-ambilight\'\nconst ambilight = ambilightModule.default')
.replace('import legacy from \'artplayer-plugin-ambilight/legacy\'', 'import legacyModule from \'artplayer-plugin-ambilight/legacy\'\nconst legacy = legacyModule.default')
: source
}
test('Ambilight retains published factory replacements and separates accurate optional runtime calls', () => {
const source = fs.readFileSync('test/types/ambilight.ts', 'utf8')
for (const [compiler, mode] of modes) {
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${compiler.version} ${mode}`)
assert.equal(checkConsumer(compiler, mode, source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')).length, 10)
const adapted = publicSource(source, mode)
assert.deepEqual(checkConsumer(compiler, mode, adapted), [], `${compiler.version} ${mode}`)
assert.equal(checkConsumer(compiler, mode, adapted.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')).length, 15)
}
assert.deepEqual(checkConsumer(ts, 'nodenext-cjs', `import ambilight = require('artplayer-plugin-ambilight'); import legacy = require('artplayer-plugin-ambilight/legacy');
const option: ambilight.Option = {}; ambilight(option); ambilight.default(option); legacy(); legacy.default(option);`), [])
import runtime = require('artplayer-plugin-ambilight/runtime');
const option: ambilight.Option = {}; ambilight.default(option); legacy.default(option); runtime(); runtime.default(option);`), [])
})
test('Ambilight published calls compile against both exact historical declarations and candidate without new inference widening', async () => {
@@ -53,7 +62,7 @@ test('Ambilight published calls compile against both exact historical declaratio
}
})
test('Ambilight editor globals are generated from the public namespace and retain negative checks', async () => {
test('Ambilight editor globals retain the required root factory and negative checks', async () => {
const source = fs.readFileSync('packages/artplayer-plugin-ambilight/types/artplayer-plugin-ambilight.d.ts', 'utf8')
const generated = generatePluginEditorDeclaration(source, 'artplayerPluginAmbilight')
const file = 'docs/assets/ts/artplayer-plugin-ambilight.d.ts'
@@ -61,12 +70,17 @@ test('Ambilight editor globals are generated from the public namespace and retai
assert.equal(formatted.errorCount, 0)
assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), formatted.output || generated)
const core = fs.readFileSync('docs/assets/ts/artplayer.d.ts', 'utf8')
const consumer = `const option: artplayerPluginAmbilight.Option = {}; artplayerPluginAmbilight(); artplayerPluginAmbilight.default(option);
const consumer = `const option: artplayerPluginAmbilight.Option = {}; artplayerPluginAmbilight(option);
const replacement: typeof artplayerPluginAmbilight = (_option) => (_art) => ({name: 'artplayerPluginAmbilight', start() {}, stop() {}});
// @ts-expect-error Required root argument.
artplayerPluginAmbilight();
// @ts-expect-error Optional invocation belongs to the runtime module.
artplayerPluginAmbilight.default(option);
// @ts-expect-error Invalid blur.
artplayerPluginAmbilight({ blur: 2 });`
for (const compiler of [ts, compat]) {
assert.deepEqual(checkPluginEditorDeclaration(generated, core, consumer, compiler), [])
assert.equal(checkPluginEditorDeclaration(generated, core, consumer.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, ''), compiler).length, 1)
assert.equal(checkPluginEditorDeclaration(generated, core, consumer.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, ''), compiler).length, 3)
}
})
+70 -15
View File
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import ts from 'typescript'
import compat from 'typescript-compat'
import { checkFiles, packedFiles } from '../../scripts/package-check.mjs'
@@ -9,7 +10,46 @@ import { consumerDirectory, removeConsumer, run, workspace, writeJson } from '..
import { verifyCanvasContract } from './canvas-contract.mjs'
import { hash, readMember } from './releases.mjs'
export function canvasConsumerSource(source, mode) {
if (mode === 'nodenext-esm') {
source = source.replace('import canvas from \'artplayer-proxy-canvas\'', 'import canvasModule from \'artplayer-proxy-canvas\'\nconst canvas = canvasModule.default')
.replace('import legacy from \'artplayer-proxy-canvas/legacy\'', 'import legacyModule from \'artplayer-proxy-canvas/legacy\'\nconst legacy = legacyModule.default')
.replace('namespace.default', 'namespace.default.default')
}
if (mode.endsWith('-no-interop'))
source = source.replace('import runtime from \'artplayer-proxy-canvas/runtime\'', 'import runtime = require(\'artplayer-proxy-canvas/runtime\')')
return source
}
export const canvasNamespaceConsumer = `import type Artplayer from 'artplayer';
import canvasModule from 'artplayer-proxy-canvas';
import * as canvasNamespace from 'artplayer-proxy-canvas';
import legacyModule from 'artplayer-proxy-canvas/legacy';
type Callback = (ctx: CanvasRenderingContext2D, video: HTMLVideoElement) => void;
type Factory = (option?: Callback) => (art: Artplayer) => HTMLCanvasElement;
const replacement: Factory = (_option?: Callback) => (_art: Artplayer) => document.createElement('canvas');
canvasModule.default(); canvasNamespace.default.default(); legacyModule.default();
const fromOld: typeof canvasModule = { default: replacement };
const toOld: { default: Factory } = canvasModule;
const namespaceReplacement: typeof canvasNamespace.default = { default: replacement };
const oldNamespace: { default: Factory } = canvasNamespace.default;
const optional: Parameters<typeof canvasModule.default>[0] = undefined;
const result: ReturnType<ReturnType<typeof canvasModule.default>> = document.createElement('canvas');
void [fromOld, toOld, namespaceReplacement, oldNamespace, optional, result];`
export function checkInvalidCanvasStatements(source, diagnostics) {
const expected = []
let line = 1
for (const text of source.split('\n')) {
if (text.startsWith('// @ts-expect-error'))
expected.push(line)
else line++
}
assert.deepEqual(diagnostics.map(item => item.line).sort((a, b) => a - b), expected, 'Each invalid Canvas statement must fail at its own line')
}
async function main() {
assert.equal(process.version.slice(1), fs.readFileSync(path.join(workspace, '.node-version'), 'utf8').trim(), 'Use the repository Node version')
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use yarn test:canvas-types-package')
const yarn = process.env.npm_execpath
assert(yarn && fs.existsSync(yarn))
@@ -50,8 +90,9 @@ async function main() {
const runtimeFile = path.join(consumer, 'exports.cjs')
fs.writeFileSync(runtimeFile, plugin.label === 'candidate'
? `const assert = require('node:assert/strict');
for (const name of ['artplayer-proxy-canvas', 'artplayer-proxy-canvas/legacy']) { const factory = require(name); assert.equal(typeof factory, 'function'); assert.equal(factory.default, factory); assert.equal(typeof factory(), 'function'); }
import('artplayer-proxy-canvas').then(module => { assert.deepEqual(Object.keys(module), ['default']); assert.equal(module.default.default, module.default); }).catch(error => { console.error(error); process.exitCode = 1; });`
for (const name of ['artplayer-proxy-canvas', 'artplayer-proxy-canvas/legacy', 'artplayer-proxy-canvas/runtime']) { const factory = require(name); assert.equal(typeof factory, 'function'); assert.equal(factory.default, factory); assert.equal(typeof factory(), 'function'); }
assert.equal(require('artplayer-proxy-canvas/runtime'), require('artplayer-proxy-canvas'));
Promise.all([import('artplayer-proxy-canvas'), import('artplayer-proxy-canvas/runtime')]).then(([module, runtime]) => { assert.deepEqual(Object.keys(module), ['default']); assert.equal(module.default.default, module.default); assert.equal(runtime.default, module.default); }).catch(error => { console.error(error); process.exitCode = 1; });`
: `const assert = require('node:assert/strict'); const value = require('artplayer-proxy-canvas'); assert.equal(typeof value, '${plugin.version === '1.0.0' ? 'object' : 'function'}'); assert.equal(typeof (value.default || value)(() => {}), 'function');`)
fs.writeFileSync(path.join(output, `${plugin.label}-runtime.log`), run([runtimeFile], consumer))
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
@@ -60,14 +101,19 @@ import('artplayer-proxy-canvas').then(module => { assert.deepEqual(Object.keys(m
for (const [compiler, mode] of modes) {
const next = mode.startsWith('nodenext')
const extension = next ? mode.endsWith('-cjs') ? 'cts' : 'mts' : 'ts'
const source = fs.readFileSync(path.join(workspace, plugin.label === 'candidate' ? 'test/types/canvas.ts' : 'refactor/fixtures/consumers/canvas-published.ts'), 'utf8')
const fixture = fs.readFileSync(path.join(workspace, plugin.label === 'candidate' ? 'test/types/canvas.ts' : 'refactor/fixtures/consumers/canvas-published.ts'), 'utf8')
const source = plugin.label === 'candidate' ? canvasConsumerSource(fixture, mode) : fixture
const filename = path.join(consumer, `consumer.${extension}`)
const historical = path.join(consumer, `historical.${extension}`)
fs.copyFileSync(path.join(workspace, 'refactor/fixtures/consumers/canvas-published.ts'), historical)
fs.writeFileSync(historical, canvasConsumerSource(fs.readFileSync(path.join(workspace, 'refactor/fixtures/consumers/canvas-published.ts'), 'utf8'), mode))
const files = plugin.label === 'candidate' ? [filename, historical] : [filename]
if (mode === 'nodenext-cjs' && plugin.label !== 'published-1.1.0') {
const commonjs = path.join(consumer, 'commonjs.cts')
fs.writeFileSync(commonjs, `import canvas = require('artplayer-proxy-canvas'); canvas((ctx, video) => ctx.drawImage(video, 0, 0)); ${plugin.label === 'candidate' ? 'canvas.default();' : ''}`)
if ((mode === 'nodenext-cjs' && plugin.label === 'published-1.0.0') || (plugin.label === 'candidate' && mode !== 'nodenext-esm' && mode !== 'bundler-esm')) {
const commonjs = path.join(consumer, `commonjs.${extension}`)
fs.writeFileSync(commonjs, plugin.label === 'candidate'
? `import canvas = require('artplayer-proxy-canvas'); import legacy = require('artplayer-proxy-canvas/legacy'); import runtime = require('artplayer-proxy-canvas/runtime');
canvas.default(); legacy.default(); runtime(); runtime.default();
const callback: runtime.Option = (ctx, video) => ctx.drawImage(video, 0, 0); runtime(callback);`
: `import canvas = require('artplayer-proxy-canvas'); canvas((ctx, video) => ctx.drawImage(video, 0, 0));`)
files.push(commonjs)
}
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: !mode.endsWith('-no-interop'), 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 }
@@ -78,7 +124,7 @@ import('artplayer-proxy-canvas').then(module => { assert.deepEqual(Object.keys(m
const actual = fs.realpathSync(file.fileName)
assert(actual.startsWith(fs.realpathSync(consumer) + path.sep) || (program.isSourceFileDefaultLibrary(file) && path.dirname(actual) === fs.realpathSync(path.dirname(compiler.sys.getExecutingFilePath()))), `Types escaped installed consumer: ${actual}`)
}
return compiler.getPreEmitDiagnostics(program).map(item => ({ code: item.code, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n') }))
return compiler.getPreEmitDiagnostics(program).map(item => ({ file: item.file && path.relative(consumer, item.file.fileName), line: item.file && item.start !== undefined ? item.file.getLineAndCharacterOfPosition(item.start).line + 1 : null, code: item.code, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n') }))
}
const diagnostics = compile(source)
const historicalFailure = plugin.label === 'published-1.1.0' && mode === 'nodenext-esm'
@@ -86,19 +132,28 @@ import('artplayer-proxy-canvas').then(module => { assert.deepEqual(Object.keys(m
assert.deepEqual(diagnostics.map(item => item.code), historicalFailure ? [2349, 2349, 2322, 2344, 2344] : [], `${plugin.label} ${compiler.version} ${mode}`)
const invalid = plugin.label === 'candidate' ? compile(source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')) : []
if (plugin.label === 'candidate')
assert.equal(invalid.length, 11, 'Installed declarations must reject all invalid uses')
matrix.push({ plugin: plugin.label, compiler: compiler.version, mode, historicalFailure, diagnostics, invalid })
checkInvalidCanvasStatements(source, invalid)
const namespace = mode === 'nodenext-esm' && plugin.label !== 'published-1.0.0' ? compile(canvasNamespaceConsumer) : []
assert.deepEqual(namespace, [], 'Latest published and candidate NodeNext ESM namespace shapes must accept plain module replacements')
const exportAssignment = mode.includes('commonjs') || mode === 'nodenext-cjs'
? compile(`import canvas = require('artplayer-proxy-canvas'); canvas((context: CanvasRenderingContext2D, video: HTMLVideoElement) => context.drawImage(video, 0, 0));`)
: null
if (exportAssignment)
assert.deepEqual(exportAssignment.map(item => item.code), plugin.label === 'published-1.0.0' ? [] : [2349], 'Preserve the approved 1.0 export= versus latest default-export distinction')
matrix.push({ plugin: plugin.label, compiler: compiler.version, mode, historicalFailure, diagnostics, invalid, namespace, exportAssignment })
}
}
finally {
removeConsumer(consumer)
}
}
writeJson(path.join(output, 'report.json'), { suite: 'canvas-isolated-package-types', introducedBy: 'PKG-CANVAS-04', scope: 'Two actual published proxies and packed candidate installed outside workspace with packed candidate core; offline and frozen reinstall, exact bytes, compiler/default/import= contracts. Published 1.0.0 required callback became optional in 1.1.0; candidate retains optional Parameters and exact HTMLCanvasElement return inference. This is not native proxy media, device or complete distribution acceptance.', packages, published: candidates.slice(0, 2).map(pkg => ({ label: pkg.label, archive: pkg.archive, sha256: pkg.sha256 })), matrix })
writeJson(path.join(output, 'report.json'), { suite: 'canvas-isolated-package-types', introducedBy: 'PKG-CANVAS-04', updatedBy: 'PKG-FACTORY-01', node: process.version, scope: 'Two actual published proxies and packed candidate installed outside workspace with packed candidate core; offline and frozen reinstall, exact bytes and root/legacy/runtime contracts. Root preserves published 1.1.0 pure factory, optional Parameters, exact HTMLCanvasElement result and NodeNext ESM namespace shape. Runtime adds accurate self identity with CommonJS/ESM and old Node10 resolution. Published 1.0.0 required callback/export= differences and 1.1.0 direct NodeNext ESM errors remain recorded. No native media, device or complete distribution acceptance.', packages, published: candidates.slice(0, 2).map(pkg => ({ label: pkg.label, archive: pkg.archive, sha256: pkg.sha256 })), matrix })
console.log(`Canvas installed types passed: ${matrix.length} cases; ${output}`)
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
}
+22 -8
View File
@@ -10,18 +10,21 @@ import compat from 'typescript-compat'
import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from '../../scripts/plugin-editor-types.mjs'
import { checkConsumer } from '../../scripts/typecheck.mjs'
import { verifyCanvasContract } from './canvas-contract.mjs'
import { canvasConsumerSource, canvasNamespaceConsumer, checkInvalidCanvasStatements } from './canvas-package-types.mjs'
import { readMember } from './releases.mjs'
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
test('Canvas current types preserve optional Parameters and exact Canvas result, while rejecting eleven invalid uses', () => {
test('Canvas types preserve the latest plain factory and expose accurate runtime identity separately', () => {
const source = fs.readFileSync('test/types/canvas.ts', 'utf8')
for (const [compiler, mode] of modes) {
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${compiler.version} ${mode}`)
assert.equal(checkConsumer(compiler, mode, source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')).length, 11)
const consumer = canvasConsumerSource(source, mode)
assert.deepEqual(checkConsumer(compiler, mode, consumer), [], `${compiler.version} ${mode}`)
checkInvalidCanvasStatements(consumer, checkConsumer(compiler, mode, consumer.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')))
}
assert.deepEqual(checkConsumer(ts, 'nodenext-cjs', `import canvas = require('artplayer-proxy-canvas'); import legacy = require('artplayer-proxy-canvas/legacy');
const callback: canvas.Option = (ctx, video) => ctx.drawImage(video, 0, 0); canvas(callback); canvas.default(); legacy(); legacy.default(callback);`), [])
assert.deepEqual(checkConsumer(ts, 'nodenext-cjs', `import canvas = require('artplayer-proxy-canvas'); import legacy = require('artplayer-proxy-canvas/legacy'); import runtime = require('artplayer-proxy-canvas/runtime');
const callback: canvas.Option = (ctx, video) => ctx.drawImage(video, 0, 0); canvas.default(callback); legacy.default(); runtime(); runtime.default(callback);`), [])
assert.deepEqual(checkConsumer(ts, 'nodenext-esm', canvasNamespaceConsumer), [])
})
test('Canvas published declarations distinguish required 1.0.0 and optional 1.1.0 callbacks without changing Canvas return assignability', async () => {
@@ -48,11 +51,17 @@ test('Canvas published declarations distinguish required 1.0.0 and optional 1.1.
assert.deepEqual(historical(source), [])
assert.deepEqual(historical(`${source}\n${optional}`), version === '1.0.0' ? [2322, 2554] : [])
assert.deepEqual(checkConsumer(compiler, 'node10-commonjs', `${source}\n${optional}`), [])
if (version === '1.1.0') {
assert.deepEqual(historical(`${source}
type Factory = (option?: (ctx: CanvasRenderingContext2D, video: HTMLVideoElement) => void) => (art: Artplayer) => HTMLCanvasElement;
const replacement: Factory = () => () => document.createElement('canvas');
const fromOld: typeof canvas = replacement; const toOld: Factory = canvas; void [fromOld, toOld];`), [])
}
}
}
})
test('Canvas editor globals are generated from public types with optional callback, media view and negative checking', async () => {
test('Canvas editor globals preserve the plain root factory with an explicit accurate runtime type view', async () => {
const source = fs.readFileSync('packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.ts', 'utf8')
const generated = generatePluginEditorDeclaration(source, 'artplayerProxyCanvas')
const file = 'docs/assets/ts/artplayer-proxy-canvas.d.ts'
@@ -60,14 +69,19 @@ test('Canvas editor globals are generated from public types with optional callba
assert.equal(formatted.errorCount, 0)
assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), formatted.output || generated)
const core = fs.readFileSync('docs/assets/ts/artplayer.d.ts', 'utf8')
const consumer = `artplayerProxyCanvas(); artplayerProxyCanvas.default((ctx, video) => ctx.drawImage(video, 0, 0));
const consumer = `artplayerProxyCanvas(); artplayerProxyCanvas((ctx, video) => ctx.drawImage(video, 0, 0));
const replacement: typeof artplayerProxyCanvas = (_option?: artplayerProxyCanvas.Option) => (_art: Artplayer) => document.createElement('canvas');
const precise = artplayerProxyCanvas as artplayerProxyCanvas.RuntimeFactory; precise.default();
const result: artplayerProxyCanvas.Result = document.createElement('canvas');
const media = result as artplayerProxyCanvas.MediaCanvas; media.play();
void replacement;
// @ts-expect-error Root factory does not require or expose a self alias.
artplayerProxyCanvas.default();
// @ts-expect-error Callback must be a function in public types.
artplayerProxyCanvas(42);`
for (const compiler of [ts, compat]) {
assert.deepEqual(checkPluginEditorDeclaration(generated, core, consumer, compiler), [])
assert.equal(checkPluginEditorDeclaration(generated, core, consumer.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, ''), compiler).length, 1)
assert.equal(checkPluginEditorDeclaration(generated, core, consumer.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, ''), compiler).length, 2)
}
})
+8 -4
View File
@@ -2718,7 +2718,8 @@
"changes/2026-09-13-PKG-ASR-05-local-core-checkpoint.md",
"baselines/asr-combinations-validation.json",
"changes/2026-09-13-PKG-ASR-05-cors-combinations.md",
"baselines/asr-explicit-capture-validation.json"
"baselines/asr-explicit-capture-validation.json",
"changes/2026-09-13-PKG-ASR-05-editor-generation.md"
]
},
{
@@ -3036,7 +3037,7 @@
"CORE-11",
"CORE-13"
],
"status": "todo",
"status": "doing",
"risk": "H",
"deliverable": "SDK singleton/实例订阅/会话与控件职责分离",
"acceptance": "结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过",
@@ -4505,7 +4506,7 @@
"PKG-AMBILIGHT-04",
"PKG-DPIP-04"
],
"status": "doing",
"status": "done",
"risk": "H",
"deliverable": "复现两包旧typeof工厂替换与新.default/可选重载冲突,修复公开类型并补安装消费者、产物和文档",
"acceptance": "默认工厂的历史合法反向赋值继续通过;既有类型/运行时导出逐版本核对,不能只验证Parameters或直接调用",
@@ -4513,7 +4514,10 @@
"baselines/factory-assignment-gaps.json",
"baselines/factory-compatibility-proposals.json",
"factory-compatibility-decision.md",
"changes/2026-09-12-PKG-FACTORY-01-decision.md"
"changes/2026-09-12-PKG-FACTORY-01-decision.md",
"type-compatibility-policy.md",
"baselines/factory-compatibility-validation.json",
"changes/2026-09-13-PKG-FACTORY-01-compatible-types.md"
]
},
{
+13 -2
View File
@@ -85,8 +85,19 @@ export function runTypechecks() {
for (const fixture of ['test/types/public.ts', 'test/types/declaration-inputs.ts', 'test/types/declaration-legacy.ts', 'test/types/plugins-public.ts', 'test/types/playback-public.ts', 'test/types/chapter-options.ts', 'test/types/chapter-exports.ts', 'test/types/hls-control.ts', 'test/types/dash-control.ts', 'test/types/ads.ts', 'refactor/fixtures/consumers/ads-published.ts', 'refactor/fixtures/consumers/ads-workspace.ts', 'refactor/fixtures/consumers/dash-control-legacy.ts', 'test/types/audio-track.ts', 'test/types/language-value.ts', 'refactor/fixtures/consumers/language.ts', 'refactor/fixtures/consumers/legacy-plugin.ts']) {
assert.deepEqual(checkConsumer(compiler, mode, fs.readFileSync(path.join(root, fixture), 'utf8')), [], `Consumer failed: TS ${compiler.version} ${mode} ${fixture}`)
}
for (const fixture of ['test/types/ambilight.ts', 'refactor/fixtures/consumers/ambilight-published.ts', 'test/types/canvas.ts', 'refactor/fixtures/consumers/canvas-published.ts', 'test/types/mediabunny.ts', 'refactor/fixtures/consumers/mediabunny-1.0.ts', 'refactor/fixtures/consumers/mediabunny-published.ts', 'test/types/dpip.ts', 'refactor/fixtures/consumers/dpip-published.ts'])
assert.deepEqual(checkConsumer(compiler, mode, fs.readFileSync(path.join(root, fixture), 'utf8')), [], `Consumer failed: TS ${compiler.version} ${mode} ${fixture}`)
for (const fixture of ['test/types/ambilight.ts', 'refactor/fixtures/consumers/ambilight-published.ts', 'test/types/canvas.ts', 'refactor/fixtures/consumers/canvas-published.ts', 'test/types/mediabunny.ts', 'refactor/fixtures/consumers/mediabunny-1.0.ts', 'refactor/fixtures/consumers/mediabunny-published.ts', 'test/types/dpip.ts', 'refactor/fixtures/consumers/dpip-published.ts']) {
let source = fs.readFileSync(path.join(root, fixture), 'utf8')
if (mode === 'nodenext-esm') {
for (const [binding, name] of [['canvas', 'artplayer-proxy-canvas'], ['ambilight', 'artplayer-plugin-ambilight']]) {
if (fixture.includes(binding)) {
source = source.replace(`import ${binding} from '${name}'`, `import ${binding}Module from '${name}'\nconst ${binding} = ${binding}Module.default`)
.replace(`import legacy from '${name}/legacy'`, `import legacyModule from '${name}/legacy'\nconst legacy = legacyModule.default`)
.replace('namespace.default', 'namespace.default.default')
}
}
}
assert.deepEqual(checkConsumer(compiler, mode, source), [], `Consumer failed: TS ${compiler.version} ${mode} ${fixture}`)
}
if (mode === 'nodenext-cjs')
assert.deepEqual(checkConsumer(compiler, mode, fs.readFileSync(path.join(root, 'test/types/commonjs.cts'), 'utf8')), [], 'CommonJS export assignment consumer failed')
for (const fixture of ['test/types/iframe.ts', 'refactor/fixtures/consumers/iframe-workspace.ts'])
+48 -11
View File
@@ -1,23 +1,59 @@
import type { Factory, Option, Result } from 'artplayer-plugin-ambilight'
import type { Factory, Option, Result, RuntimeFactory } from 'artplayer-plugin-ambilight'
import type { RuntimeFactory as EntryFactory, Option as EntryOption, Result as EntryResult } from 'artplayer-plugin-ambilight/runtime'
import Artplayer from 'artplayer'
import ambilight from 'artplayer-plugin-ambilight'
import legacy from 'artplayer-plugin-ambilight/legacy'
import runtime from 'artplayer-plugin-ambilight/runtime'
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false
type Assert<T extends true> = T
interface PublishedOption {
blur?: string
opacity?: number
frequency?: number
zIndex?: number
duration?: number
}
interface PublishedResult {
name: 'artplayerPluginAmbilight'
start: () => void
stop: () => void
}
type PublishedFactory = (option: PublishedOption) => (art: Artplayer) => PublishedResult
type RootFactory = Assert<Equal<typeof ambilight, PublishedFactory>>
type RootArguments = Assert<Equal<Parameters<typeof ambilight>, [option: PublishedOption]>>
type RootResult = Assert<Equal<ReturnType<ReturnType<typeof ambilight>>, PublishedResult>>
type RuntimeArguments = Assert<Equal<Parameters<typeof runtime>, [option?: Option]>>
type RuntimeTypes = Assert<Equal<[EntryOption, EntryResult, EntryFactory], [Option, Result, RuntimeFactory]>>
const replacement: PublishedFactory = (_option: PublishedOption) => (_art: Artplayer) => ({ name: 'artplayerPluginAmbilight', start() {}, stop() {} })
const assignToOld: PublishedFactory = ambilight
const assignFromOld: typeof ambilight = replacement
const option: Option = { blur: '40px', opacity: 0.6, frequency: 10, duration: 0.2, zIndex: 3 }
const art = new Artplayer({ container: '#player', url: 'video.mp4', plugins: [ambilight(), ambilight(option), legacy({})] })
ambilight(undefined)
ambilight.default(option)
legacy.default()
const art = new Artplayer({ container: '#player', url: 'video.mp4', plugins: [ambilight(option), legacy({}), runtime()] })
runtime(undefined)
runtime.default(option)
runtime.default.default()
declare const optional: Option | undefined
ambilight(optional)
runtime(optional)
const inferred: Parameters<typeof ambilight>[0] = option
const blur: string | undefined = inferred.blur
const factory: Factory = ambilight
const factory: Factory = replacement
const result: Result = factory(option)(art)
const name: 'artplayerPluginAmbilight' = result.name
const calls: void[] = [result.start(), result.stop()]
const registration: (art: Artplayer) => Result = ambilight()
// @ts-expect-error Keep required last overload for existing Parameters extraction.
const registration: (art: Artplayer) => Result = runtime()
// @ts-expect-error Published 1.1.0 requires the options argument.
ambilight()
// @ts-expect-error Published 1.1.0 does not permit undefined as its argument.
ambilight(undefined)
// @ts-expect-error Optional input belongs to the accurate runtime entry.
ambilight(optional)
// @ts-expect-error Historical replacement factories must not require a self alias.
ambilight.default(option)
// @ts-expect-error Legacy retains the root required-argument type.
legacy()
// @ts-expect-error Root Parameters retains the required options argument.
const missing: Parameters<typeof ambilight>[0] = undefined
// @ts-expect-error Blur is a CSS string, not a number.
ambilight({ blur: 50 })
@@ -32,9 +68,10 @@ ambilight({ zIndex: '9' })
// @ts-expect-error Null was never accepted by the public types.
ambilight(null)
// @ts-expect-error Unknown options do not silently enter the contract.
ambilight({ speed: 10 })
runtime({ speed: 10 })
// @ts-expect-error Methods are synchronous.
const promise: Promise<void> = result.start()
// @ts-expect-error No public plugin destroy method is introduced.
result.destroy()
void [blur, name, calls, registration, missing, promise]
void [assignToOld, assignFromOld, blur, name, calls, registration, missing, promise]
export type { RootArguments, RootFactory, RootResult, RuntimeArguments, RuntimeTypes }
+28 -7
View File
@@ -1,12 +1,26 @@
import type { Factory, MediaCanvas, Option, Result } from 'artplayer-proxy-canvas'
import type { Factory, Option, Result, RuntimeFactory } from 'artplayer-proxy-canvas'
import type { MediaCanvas, RuntimeFactory as RuntimeEntry, Factory as RuntimeEntryFactory, Option as RuntimeOption, Result as RuntimeResult } from 'artplayer-proxy-canvas/runtime'
import Artplayer from 'artplayer'
import canvas from 'artplayer-proxy-canvas'
import * as namespace from 'artplayer-proxy-canvas'
import legacy from 'artplayer-proxy-canvas/legacy'
import runtime from 'artplayer-proxy-canvas/runtime'
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false
type Assert<T extends true> = T
type OptionalInput = Assert<Equal<Parameters<typeof canvas>, [option?: Option]>>
type ExactResult = Assert<Equal<ReturnType<ReturnType<typeof canvas>>, HTMLCanvasElement>>
type HistoricalFactory = (option?: (ctx: CanvasRenderingContext2D, video: HTMLVideoElement) => void) => (art: Artplayer) => HTMLCanvasElement
type ExactFactory = Assert<Equal<typeof canvas, HistoricalFactory>>
type RuntimeEntries = Assert<Equal<[RuntimeEntryFactory, RuntimeOption, RuntimeResult, RuntimeEntry], [Factory, Option, Result, RuntimeFactory]>>
type ExactRuntime = Assert<Equal<typeof runtime, RuntimeFactory>>
const replacement: HistoricalFactory = (_option?: Option) => (_art: Artplayer) => document.createElement('canvas')
const toOld: HistoricalFactory = canvas
const fromOld: typeof canvas = replacement
const namedFactory: Factory = replacement
const namespaceFactory: HistoricalFactory = namespace.default
const legacyFactory: typeof legacy = replacement
const option: Option = (context, video) => {
context.drawImage(video, 0, 0)
@@ -17,8 +31,9 @@ const option: Option = (context, video) => {
const art = new Artplayer({ container: '#player', url: 'video.mp4', proxy: canvas(option) })
canvas()
canvas(undefined)
canvas.default(option)
legacy.default()
runtime.default(option)
runtime.default.default()
legacy()
declare const optional: Option | undefined
canvas(optional)
const missing: Parameters<typeof canvas>[0] = undefined
@@ -51,9 +66,15 @@ const promise: Promise<HTMLCanvasElement> = canvas()(art)
media.getContext('2d')?.play()
// @ts-expect-error Native Canvas dimensions remain numeric.
media.width = '320'
// @ts-expect-error Self default identity is readonly in declarations.
canvas.default = legacy
// @ts-expect-error Historical root factory has no required or exposed self default member.
canvas.default()
// @ts-expect-error Legacy factory retains the same plain factory shape.
legacy.default()
// @ts-expect-error A precise runtime replacement must provide its self identity.
const missingAlias: RuntimeFactory = replacement
// @ts-expect-error Self default identity is readonly in precise declarations.
runtime.default = runtime
// @ts-expect-error Registration requires a host.
canvas()()
void [missing, originalResult, play, context, listen, width, video, promise]
export type { ExactResult, OptionalInput }
void [toOld, fromOld, namedFactory, namespaceFactory, legacyFactory, missing, originalResult, play, context, listen, width, video, promise, missingAlias]
export type { ExactFactory, ExactResult, ExactRuntime, OptionalInput, RuntimeEntries }