From c4c7491e169e44db6130cfc75d163592e1063dfc Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Sun, 13 Sep 2026 16:53:27 +0800 Subject: [PATCH] fix(types): [PKG-FACTORY-01] preserve published factory compatibility --- .../assets/ts/artplayer-plugin-ambilight.d.ts | 29 ++++--- docs/assets/ts/artplayer-proxy-canvas.d.ts | 35 +++++--- .../ARCHITECTURE.md | 36 +++++--- packages/artplayer-plugin-ambilight/README.md | 43 ++++++++-- .../artplayer-plugin-ambilight/package.json | 27 +++--- .../types/artplayer-plugin-ambilight.d.cts | 4 - .../types/artplayer-plugin-ambilight.d.mts | 2 - .../types/artplayer-plugin-ambilight.d.ts | 67 +++++++-------- .../types/runtime.d.cts | 10 +++ .../types/runtime.d.mts | 5 ++ .../types/runtime.d.ts | 10 +++ .../artplayer-proxy-canvas/ARCHITECTURE.md | 23 +++-- packages/artplayer-proxy-canvas/README.md | 34 +++++++- packages/artplayer-proxy-canvas/package.json | 27 +++--- .../types/artplayer-proxy-canvas.d.cts | 6 +- .../types/artplayer-proxy-canvas.d.mts | 7 +- .../types/artplayer-proxy-canvas.d.ts | 33 +++---- .../types/runtime.d.cts | 12 +++ .../types/runtime.d.mts | 5 ++ .../artplayer-proxy-canvas/types/runtime.d.ts | 12 +++ .../factory-compatibility-validation.json | 61 +++++++++++++ ...6-09-13-PKG-FACTORY-01-compatible-types.md | 25 ++++++ refactor/factory-compatibility-decision.md | 24 +++--- refactor/plan.md | 10 +-- refactor/progress.md | 12 +++ refactor/risk-table.md | 2 +- refactor/risks.json | 14 ++- refactor/scripts/ambilight-package-types.mjs | 60 ++++++++++--- refactor/scripts/ambilight-types.test.mjs | 28 ++++-- refactor/scripts/canvas-package-types.mjs | 85 +++++++++++++++---- refactor/scripts/canvas-types.test.mjs | 30 +++++-- refactor/tasks.json | 12 ++- scripts/typecheck.mjs | 15 +++- test/types/ambilight.ts | 59 ++++++++++--- test/types/canvas.ts | 35 ++++++-- 35 files changed, 670 insertions(+), 229 deletions(-) delete mode 100644 packages/artplayer-plugin-ambilight/types/artplayer-plugin-ambilight.d.cts delete mode 100644 packages/artplayer-plugin-ambilight/types/artplayer-plugin-ambilight.d.mts create mode 100644 packages/artplayer-plugin-ambilight/types/runtime.d.cts create mode 100644 packages/artplayer-plugin-ambilight/types/runtime.d.mts create mode 100644 packages/artplayer-plugin-ambilight/types/runtime.d.ts create mode 100644 packages/artplayer-proxy-canvas/types/runtime.d.cts create mode 100644 packages/artplayer-proxy-canvas/types/runtime.d.mts create mode 100644 packages/artplayer-proxy-canvas/types/runtime.d.ts create mode 100644 refactor/baselines/factory-compatibility-validation.json create mode 100644 refactor/changes/2026-09-13-PKG-FACTORY-01-compatible-types.md diff --git a/docs/assets/ts/artplayer-plugin-ambilight.d.ts b/docs/assets/ts/artplayer-plugin-ambilight.d.ts index 6376c64ba..9479cc228 100644 --- a/docs/assets/ts/artplayer-plugin-ambilight.d.ts +++ b/docs/assets/ts/artplayer-plugin-ambilight.d.ts @@ -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; diff --git a/docs/assets/ts/artplayer-proxy-canvas.d.ts b/docs/assets/ts/artplayer-proxy-canvas.d.ts index 6a8e42168..857bc76c9 100644 --- a/docs/assets/ts/artplayer-proxy-canvas.d.ts +++ b/docs/assets/ts/artplayer-proxy-canvas.d.ts @@ -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> - 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> + /** 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; diff --git a/packages/artplayer-plugin-ambilight/ARCHITECTURE.md b/packages/artplayer-plugin-ambilight/ARCHITECTURE.md index d0e73d586..9913635bd 100644 --- a/packages/artplayer-plugin-ambilight/ARCHITECTURE.md +++ b/packages/artplayer-plugin-ambilight/ARCHITECTURE.md @@ -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 diff --git a/packages/artplayer-plugin-ambilight/README.md b/packages/artplayer-plugin-ambilight/README.md index 0f0d65af7..b519af12a 100644 --- a/packages/artplayer-plugin-ambilight/README.md +++ b/packages/artplayer-plugin-ambilight/README.md @@ -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 diff --git a/packages/artplayer-plugin-ambilight/package.json b/packages/artplayer-plugin-ambilight/package.json index 7e5f66722..59a1c3b90 100644 --- a/packages/artplayer-plugin-ambilight/package.json +++ b/packages/artplayer-plugin-ambilight/package.json @@ -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" ] } } diff --git a/packages/artplayer-plugin-ambilight/types/artplayer-plugin-ambilight.d.cts b/packages/artplayer-plugin-ambilight/types/artplayer-plugin-ambilight.d.cts deleted file mode 100644 index be3120dce..000000000 --- a/packages/artplayer-plugin-ambilight/types/artplayer-plugin-ambilight.d.cts +++ /dev/null @@ -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 diff --git a/packages/artplayer-plugin-ambilight/types/artplayer-plugin-ambilight.d.mts b/packages/artplayer-plugin-ambilight/types/artplayer-plugin-ambilight.d.mts deleted file mode 100644 index 5cf1ff44c..000000000 --- a/packages/artplayer-plugin-ambilight/types/artplayer-plugin-ambilight.d.mts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './artplayer-plugin-ambilight.js' -export type { Callable, Factory, Option, Result } from './artplayer-plugin-ambilight.js' diff --git a/packages/artplayer-plugin-ambilight/types/artplayer-plugin-ambilight.d.ts b/packages/artplayer-plugin-ambilight/types/artplayer-plugin-ambilight.d.ts index 89b5aa78a..3ec36c46f 100644 --- a/packages/artplayer-plugin-ambilight/types/artplayer-plugin-ambilight.d.ts +++ b/packages/artplayer-plugin-ambilight/types/artplayer-plugin-ambilight.d.ts @@ -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 diff --git a/packages/artplayer-plugin-ambilight/types/runtime.d.cts b/packages/artplayer-plugin-ambilight/types/runtime.d.cts new file mode 100644 index 000000000..81c04dbef --- /dev/null +++ b/packages/artplayer-plugin-ambilight/types/runtime.d.cts @@ -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 diff --git a/packages/artplayer-plugin-ambilight/types/runtime.d.mts b/packages/artplayer-plugin-ambilight/types/runtime.d.mts new file mode 100644 index 000000000..a297b37e5 --- /dev/null +++ b/packages/artplayer-plugin-ambilight/types/runtime.d.mts @@ -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' diff --git a/packages/artplayer-plugin-ambilight/types/runtime.d.ts b/packages/artplayer-plugin-ambilight/types/runtime.d.ts new file mode 100644 index 000000000..81c04dbef --- /dev/null +++ b/packages/artplayer-plugin-ambilight/types/runtime.d.ts @@ -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 diff --git a/packages/artplayer-proxy-canvas/ARCHITECTURE.md b/packages/artplayer-proxy-canvas/ARCHITECTURE.md index 5d09ae544..23a990415 100644 --- a/packages/artplayer-proxy-canvas/ARCHITECTURE.md +++ b/packages/artplayer-proxy-canvas/ARCHITECTURE.md @@ -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. diff --git a/packages/artplayer-proxy-canvas/README.md b/packages/artplayer-proxy-canvas/README.md index 7750cb884..d264c5877 100644 --- a/packages/artplayer-proxy-canvas/README.md +++ b/packages/artplayer-proxy-canvas/README.md @@ -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) diff --git a/packages/artplayer-proxy-canvas/package.json b/packages/artplayer-proxy-canvas/package.json index e7ee0744f..6b1a88bbd 100644 --- a/packages/artplayer-proxy-canvas/package.json +++ b/packages/artplayer-proxy-canvas/package.json @@ -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" ] } } diff --git a/packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.cts b/packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.cts index e8e8f037d..b9b0afc90 100644 --- a/packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.cts +++ b/packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.cts @@ -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' diff --git a/packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.mts b/packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.mts index 9fd185fa4..50f05d166 100644 --- a/packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.mts +++ b/packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.mts @@ -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' diff --git a/packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.ts b/packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.ts index 6e22a3fa7..3a8a6f078 100644 --- a/packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.ts +++ b/packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.ts @@ -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> +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> - 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 diff --git a/packages/artplayer-proxy-canvas/types/runtime.d.cts b/packages/artplayer-proxy-canvas/types/runtime.d.cts new file mode 100644 index 000000000..98e993d29 --- /dev/null +++ b/packages/artplayer-proxy-canvas/types/runtime.d.cts @@ -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 diff --git a/packages/artplayer-proxy-canvas/types/runtime.d.mts b/packages/artplayer-proxy-canvas/types/runtime.d.mts new file mode 100644 index 000000000..f42ea828e --- /dev/null +++ b/packages/artplayer-proxy-canvas/types/runtime.d.mts @@ -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' diff --git a/packages/artplayer-proxy-canvas/types/runtime.d.ts b/packages/artplayer-proxy-canvas/types/runtime.d.ts new file mode 100644 index 000000000..98e993d29 --- /dev/null +++ b/packages/artplayer-proxy-canvas/types/runtime.d.ts @@ -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 diff --git a/refactor/baselines/factory-compatibility-validation.json b/refactor/baselines/factory-compatibility-validation.json new file mode 100644 index 000000000..b992c49f4 --- /dev/null +++ b/refactor/baselines/factory-compatibility-validation.json @@ -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." + ] +} diff --git a/refactor/changes/2026-09-13-PKG-FACTORY-01-compatible-types.md b/refactor/changes/2026-09-13-PKG-FACTORY-01-compatible-types.md new file mode 100644 index 000000000..190bf3732 --- /dev/null +++ b/refactor/changes/2026-09-13-PKG-FACTORY-01-compatible-types.md @@ -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发布复盘仍须继续。一个任务一个独立完成提交, +不推送、不发布。 diff --git a/refactor/factory-compatibility-decision.md b/refactor/factory-compatibility-decision.md index 9482660db..0875bc15a 100644 --- a/refactor/factory-compatibility-decision.md +++ b/refactor/factory-compatibility-decision.md @@ -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)。 diff --git a/refactor/plan.md b/refactor/plan.md index 736032aaf..fae45f913 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -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
核对包契约与历史用法 | BASE-05 | url/sdk/icon/mimeType、loader、会话、全局 SDK 与结果 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done | | PKG-CAST-02 | artplayer-plugin-chromecast
建立特有行为与错误测试 | PKG-CAST-01, ENG-03, ENG-05 | SDK 脚本加载失败/重入、无设备/拒绝、重复实例与销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done | -| PKG-CAST-03 | artplayer-plugin-chromecast
整理内部职责与资源 | PKG-CAST-02, CORE-11, CORE-13 | SDK singleton/实例订阅/会话与控件职责分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo | +| PKG-CAST-03 | artplayer-plugin-chromecast
整理内部职责与资源 | PKG-CAST-02, CORE-11, CORE-13 | SDK singleton/实例订阅/会话与控件职责分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | doing | | PKG-CAST-04 | artplayer-plugin-chromecast
迁移自有源码和公开类型 | PKG-CAST-03, ENG-04, ENG-06, CORE-07 | Cast 能力 adapter、全局对象和元数据类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo | | PKG-CAST-05 | artplayer-plugin-chromecast
验证新旧核心和组合 | PKG-CAST-04, CORE-22 | stub 测试之外记录实际 Cast 设备会话、源更新和断开 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo | | PKG-CAST-06 | artplayer-plugin-chromecast
验证分发并同步文档 | 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
修复已迁移工厂的反向赋值兼容 | PKG-CANVAS-04, PKG-AMBILIGHT-04, PKG-DPIP-04 | 复现两包旧typeof工厂替换与新.default/可选重载冲突,修复公开类型并补安装消费者、产物和文档 | 默认工厂的历史合法反向赋值继续通过;既有类型/运行时导出逐版本核对,不能只验证Parameters或直接调用 | H | doing | +| PKG-FACTORY-01 | artplayer-proxy-canvas, artplayer-plugin-ambilight
修复已迁移工厂的反向赋值兼容 | 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) diff --git a/refactor/progress.md b/refactor/progress.md index 0b155b0e5..e32005260 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -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项新增错误/生命周期观察。首次会话结果误用、已就绪 diff --git a/refactor/risk-table.md b/refactor/risk-table.md index a4f68a477..3f5b72132 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -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 | diff --git a/refactor/risks.json b/refactor/risks.json index 6fa773082..c6fa4a2a2 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -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" ] }, { diff --git a/refactor/scripts/ambilight-package-types.mjs b/refactor/scripts/ambilight-package-types.mjs index b70ab7f5d..0415979ed 100644 --- a/refactor/scripts/ambilight-package-types.mjs +++ b/refactor/scripts/ambilight-package-types.mjs @@ -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('\\', '/'), '') })) } 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}`) } diff --git a/refactor/scripts/ambilight-types.test.mjs b/refactor/scripts/ambilight-types.test.mjs index fa9993df5..cc5dec677 100644 --- a/refactor/scripts/ambilight-types.test.mjs +++ b/refactor/scripts/ambilight-types.test.mjs @@ -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) } }) diff --git a/refactor/scripts/canvas-package-types.mjs b/refactor/scripts/canvas-package-types.mjs index 1455dd467..a384239c4 100644 --- a/refactor/scripts/canvas-package-types.mjs +++ b/refactor/scripts/canvas-package-types.mjs @@ -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[0] = undefined; +const result: ReturnType> = 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 + }) +} diff --git a/refactor/scripts/canvas-types.test.mjs b/refactor/scripts/canvas-types.test.mjs index a838fee06..f310959d5 100644 --- a/refactor/scripts/canvas-types.test.mjs +++ b/refactor/scripts/canvas-types.test.mjs @@ -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) } }) diff --git a/refactor/tasks.json b/refactor/tasks.json index c4f09c051..121216914 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -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" ] }, { diff --git a/scripts/typecheck.mjs b/scripts/typecheck.mjs index 9e09b89d0..827e264da 100644 --- a/scripts/typecheck.mjs +++ b/scripts/typecheck.mjs @@ -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']) diff --git a/test/types/ambilight.ts b/test/types/ambilight.ts index 7404b890a..bd0d479f9 100644 --- a/test/types/ambilight.ts +++ b/test/types/ambilight.ts @@ -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 = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false +type Assert = 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> +type RootArguments = Assert, [option: PublishedOption]>> +type RootResult = Assert>, PublishedResult>> +type RuntimeArguments = Assert, [option?: Option]>> +type RuntimeTypes = Assert> + +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[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[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 = 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 } diff --git a/test/types/canvas.ts b/test/types/canvas.ts index 6cab278be..1166c6f49 100644 --- a/test/types/canvas.ts +++ b/test/types/canvas.ts @@ -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 = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false type Assert = T type OptionalInput = Assert, [option?: Option]>> type ExactResult = Assert>, HTMLCanvasElement>> +type HistoricalFactory = (option?: (ctx: CanvasRenderingContext2D, video: HTMLVideoElement) => void) => (art: Artplayer) => HTMLCanvasElement +type ExactFactory = Assert> +type RuntimeEntries = Assert> +type ExactRuntime = Assert> + +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[0] = undefined @@ -51,9 +66,15 @@ const promise: Promise = 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 }