From 047e2df945fffe5699def60f2cf0497c00264117 Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Sun, 13 Sep 2026 01:43:09 +0800 Subject: [PATCH] refactor(iframe): [PKG-IFRAME-04] preserve types across module consumers --- docs/assets/ts/artplayer-tool-iframe.d.ts | 137 +++- package.json | 3 +- packages/artplayer-tool-iframe/.npmignore | 3 +- .../artplayer-tool-iframe/ARCHITECTURE.md | 37 +- packages/artplayer-tool-iframe/README.md | 29 + packages/artplayer-tool-iframe/package.json | 20 +- .../types/artplayer-tool-iframe.d.cts | 22 + .../types/artplayer-tool-iframe.d.mts | 2 + .../types/artplayer-tool-iframe.d.ts | 66 +- .../baselines/iframe-types-validation.json | 723 ++++++++++++++++++ .../changes/2026-09-13-PKG-IFRAME-04-types.md | 88 +++ .../fixtures/consumers/iframe-workspace.ts | 37 + refactor/plan.md | 9 +- refactor/progress.md | 14 + refactor/risks.json | 6 +- refactor/scripts/iframe-package-types.mjs | 112 +++ refactor/scripts/iframe-types.test.mjs | 105 +++ refactor/tasks.json | 11 +- scripts/build-ts.js | 27 +- scripts/plugin-editor-types.mjs | 9 +- scripts/typecheck.mjs | 4 + test/types/iframe-commonjs.cts | 16 + test/types/iframe-source.ts | 8 + test/types/iframe.ts | 77 ++ 24 files changed, 1496 insertions(+), 69 deletions(-) create mode 100644 packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.cts create mode 100644 packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.mts create mode 100644 refactor/baselines/iframe-types-validation.json create mode 100644 refactor/changes/2026-09-13-PKG-IFRAME-04-types.md create mode 100644 refactor/fixtures/consumers/iframe-workspace.ts create mode 100644 refactor/scripts/iframe-package-types.mjs create mode 100644 refactor/scripts/iframe-types.test.mjs create mode 100644 test/types/iframe-commonjs.cts create mode 100644 test/types/iframe-source.ts create mode 100644 test/types/iframe.ts diff --git a/docs/assets/ts/artplayer-tool-iframe.d.ts b/docs/assets/ts/artplayer-tool-iframe.d.ts index 8484624a0..11e5ea8ba 100644 --- a/docs/assets/ts/artplayer-tool-iframe.d.ts +++ b/docs/assets/ts/artplayer-tool-iframe.d.ts @@ -1,32 +1,109 @@ -interface Message { - type: string - data: any - id?: number +// 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 ArtplayerToolIframeDefinitions { + export interface Option { + iframe: HTMLIFrameElement + url: string + } + /** Historical open envelope. data remains required in the default class API. */ + export interface Message { + type: string + data: T + id?: number + } + export interface Callbacks { + resove: (...args: any[]) => any + reject: (...args: any[]) => any + } + /** Public notifications omit the request id and all private session metadata. */ + export interface Notification { + type: string + data: T + } + export type MessageCallback = (this: ArtplayerToolIframe, message: Notification) => void + /** Actual outgoing calls allow an omitted data field. Custom message types remain valid. */ + export interface OutboundMessage { + type: string + data?: T + id?: number + } + /** Known built-in envelopes; use Message/OutboundMessage for application protocols. */ + export type ProtocolMessage = { + type: 'inject' + data?: undefined + id?: number + } | { + type: 'commit' + data: string + id: number + } | { + type: 'response' + data: T + id: number + } | { + type: 'error' + data: unknown + id: number + } + export type Resolve = (value: T | PromiseLike) => void + export type ResolverCallback = (resolve: Resolve) => void + /** Opt-in view. Response T is supplied by the application's protocol, not validated at runtime. */ + export interface RuntimeInstance extends Omit { + messageCallback: MessageCallback | null + postMessage: (message: OutboundMessage) => Promise + message: (callback: MessageCallback) => void + } + /** For the existing serialized resolve(...) protocol; callback bodies must use that exact name. */ + export interface ResolverInstance extends Omit { + commit: (callback: ResolverCallback) => Promise + } + /** Opt-in static async/optional-data view. There is no runtime self-default property. */ + export interface RuntimeConstructor { + new (option: Option): RuntimeInstance + readonly prototype: RuntimeInstance + readonly iframe: boolean + postMessage: (message: OutboundMessage) => void + onMessage: (event: MessageEvent) => Promise + inject: () => void + } + export class ArtplayerToolIframe { + constructor(option: Option) + static iframe: boolean + static postMessage(message: Message): void + static onMessage(event: MessageEvent & { + data: Message + }): void + static inject(): void + readonly promises: Record + readonly injected: boolean + readonly destroyed: boolean + readonly $iframe: HTMLIFrameElement + readonly url: string + readonly messageCallback: (...args: any[]) => any + onMessage(event: MessageEvent & { + data: Message + }): void + postMessage(message: Message): Promise + commit any>(callback: T): Promise> + message(callback: (...args: any[]) => any): void + destroy(): void + } } - -declare class ArtplayerToolIframe { - constructor(option: { iframe: HTMLIFrameElement, url: string }) - - static iframe: boolean - static postMessage(message: Message): void - static onMessage(event: MessageEvent & { data: Message }): void - static inject(): void - - readonly promises: Record any, reject: (...args: any[]) => any }> - readonly injected: boolean - readonly destroyed: boolean - readonly $iframe: HTMLIFrameElement - readonly url: string - readonly messageCallback: (...args: any[]) => any - - onMessage(event: MessageEvent & { data: Message }): void - postMessage(message: Message): Promise - commit any>(callback: T): Promise> - message(callback: (...args: any[]) => any): void - destroy(): void +declare const ArtplayerToolIframe: typeof ArtplayerToolIframeDefinitions.ArtplayerToolIframe +type ArtplayerToolIframe = ArtplayerToolIframeDefinitions.ArtplayerToolIframe +declare namespace ArtplayerToolIframe { + export type Option = ArtplayerToolIframeDefinitions.Option + export type Message = ArtplayerToolIframeDefinitions.Message + export type Callbacks = ArtplayerToolIframeDefinitions.Callbacks + export type Notification = ArtplayerToolIframeDefinitions.Notification + export type MessageCallback = ArtplayerToolIframeDefinitions.MessageCallback + export type OutboundMessage = ArtplayerToolIframeDefinitions.OutboundMessage + export type ProtocolMessage = ArtplayerToolIframeDefinitions.ProtocolMessage + export type Resolve = ArtplayerToolIframeDefinitions.Resolve + export type ResolverCallback = ArtplayerToolIframeDefinitions.ResolverCallback + export type RuntimeInstance = ArtplayerToolIframeDefinitions.RuntimeInstance + export type ResolverInstance = ArtplayerToolIframeDefinitions.ResolverInstance + export type RuntimeConstructor = ArtplayerToolIframeDefinitions.RuntimeConstructor } - -export default ArtplayerToolIframe - -export = artplayerToolIframe -export as namespace artplayerToolIframe; +export = ArtplayerToolIframe +export as namespace ArtplayerToolIframe; diff --git a/package.json b/package.json index d84fc2979..75fff5d75 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,8 @@ "test:mediabunny-types-package": "node refactor/scripts/mb-package-types.mjs", "test:mediabunny-soak": "playwright test --config playwright.soak.config.js", "generate:media:hls": "node scripts/generate-hls-fixture.mjs", - "test:iframe": "node --test test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe.test.js test/iframe-lifecycle.test.js refactor/scripts/iframe-contract.test.mjs" + "test:iframe": "node --test test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe.test.js test/iframe-lifecycle.test.js refactor/scripts/iframe-contract.test.mjs refactor/scripts/iframe-types.test.mjs", + "test:iframe-types-package": "node refactor/scripts/iframe-package-types.mjs" }, "browserslist": "last 1 Chrome version", "devDependencies": { diff --git a/packages/artplayer-tool-iframe/.npmignore b/packages/artplayer-tool-iframe/.npmignore index 4439c6874..2b272d565 100644 --- a/packages/artplayer-tool-iframe/.npmignore +++ b/packages/artplayer-tool-iframe/.npmignore @@ -2,4 +2,5 @@ node_modules .DS_Store yarn.lock package-lock.json -src \ No newline at end of file +src +tsconfig.json diff --git a/packages/artplayer-tool-iframe/ARCHITECTURE.md b/packages/artplayer-tool-iframe/ARCHITECTURE.md index 3c830dd1d..6d9bcf53a 100644 --- a/packages/artplayer-tool-iframe/ARCHITECTURE.md +++ b/packages/artplayer-tool-iframe/ARCHITECTURE.md @@ -110,18 +110,41 @@ history reload is BFCache: actual persisted restoration, full-player integration devices and externally interrupted navigation remain PKG-IFRAME-05/release gates. IFRAME-LIFE-01 and IFRAME-TRUST-01 remain open for those integration/review scopes. -Public declarations remain in `types/artplayer-tool-iframe.d.ts`; source typing -does not yet establish historical declaration/entry compatibility. PKG-IFRAME-04 -must reconcile the old npm plugin name, namespace/default and extra helper -protocol separately. PKG-IFRAME-06 owns final distribution. No version bump or -publication is implied by this source migration. +Public declarations are maintained in `types/artplayer-tool-iframe.d.ts`. +The `.d.cts` bridge describes the actual CommonJS constructor and its named types; +`.d.mts` routes native ESM to the same class. Keep the root `types` field and +legacy `typesVersions` entry for TS 4.3 consumers. The runtime has no self-default +property. Ordinary default imports use interop in CommonJS consumers; modern +TypeScript can use direct `import Iframe = require(...)` without interop. + +The default class intentionally retains required Message.data, void static +onMessage, readonly instance fields, non-null callback and legacy commit inference. +RuntimeConstructor/RuntimeInstance expose optional outgoing data, async static +receiver, nullable callback and application-supplied response types. ResolverInstance +offers an explicit result parameter for the old serialized resolve(...) protocol. +These are erased type views, not new runtime methods or payload validators. The +source class satisfies RuntimeConstructor; its nullable callback intentionally +does not satisfy the old non-null class declaration, and tests pin that difference. + +Old npm plugin-iframe used Function callbacks and a CommonJS default namespace; +those differ from the frozen workspace before this refactor. Installed tests +preserve that evidence and the separate helper's static destroy protocol. They +do not claim the renamed tool supplies the old package name or helper entrypoints. +PKG-IFRAME-06 still owns those distribution decisions and compatibility facades. +No version bump or publication is implied by declaration validation. ## Verification - `yarn test:iframe`: frozen historical contract/defect assertions and candidate lifecycle assertions. Historical failures are not candidate acceptance. -- `yarn typecheck`: strict source and existing consumers; full package declaration - consumers are still a separate PKG-IFRAME-04 gate. +- `yarn typecheck`: strict source and existing consumers, including old extraction + and new typed views in TS 5.9 and TS 4.3. +- `yarn test:iframe-types-package`: install the real old npm archive, frozen + workspace pack and candidate outside the workspace with offline/frozen Yarn. + Check declaration resolution, exact bytes, exports and positive/negative uses. +- `yarn build:ts artplayer-tool-iframe`: regenerate only this package's standalone + editor declaration (plus the shared core declaration). The generator uses the + actual uppercase class global and exports named types without external imports. - `yarn build artplayer-tool-iframe`: normal main/legacy/ESM production output and generated docs copies. Never hand-edit those files. - `yarn test:browser test/browser/iframe.spec.js`: real same/cross-origin windows diff --git a/packages/artplayer-tool-iframe/README.md b/packages/artplayer-tool-iframe/README.md index b26edffc0..1e00e4d82 100644 --- a/packages/artplayer-tool-iframe/README.md +++ b/packages/artplayer-tool-iframe/README.md @@ -6,6 +6,35 @@ Iframe tool for ArtPlayer [https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-tool-iframe/index.js&example=iframe) +## TypeScript + +Default imports keep the historical class signatures, including `resove`, +required message `data`, and the original `commit` result inference. Named types +include `Option`, `Message`, `ProtocolMessage` and optional runtime views: + +```ts +import type { ResolverInstance, RuntimeConstructor } from 'artplayer-tool-iframe' +import Iframe from 'artplayer-tool-iframe' + +const Runtime = Iframe as RuntimeConstructor +const tool = new Runtime({ iframe: document.querySelector('iframe')!, url: '/iframe.html' }) +const resolver = tool as ResolverInstance +const value = await resolver.commit((resolve) => { + resolve(42) +}) +``` + +`commit` serializes the function body. Keep its braces, use the literal `resolve` +name for asynchronous replies, and do not depend on closures or an async function +body. Handle rejection when the child document changes or the tool is destroyed. +The runtime views are types only; they do not validate messages or sandbox code. + +The browser global is `ArtplayerToolIframe`. Modern TypeScript CommonJS consumers +can use `import Iframe = require('artplayer-tool-iframe')`; older Node resolution +uses the default import with `esModuleInterop`. The class has no `.default` +property. The historical `artplayer-plugin-iframe` package and its separate helper +have different export/protocol details; they are not aliases for this tool. + ## Maintenance See [ARCHITECTURE.md](ARCHITECTURE.md) for the TypeScript module map, request diff --git a/packages/artplayer-tool-iframe/package.json b/packages/artplayer-tool-iframe/package.json index 9b8fd25ce..03f28e4be 100644 --- a/packages/artplayer-tool-iframe/package.json +++ b/packages/artplayer-tool-iframe/package.json @@ -19,12 +19,17 @@ ], "exports": { ".": { - "types": "./types/artplayer-tool-iframe.d.ts", - "import": "./dist/artplayer-tool-iframe.mjs", - "require": "./dist/artplayer-tool-iframe.js" + "import": { + "types": "./types/artplayer-tool-iframe.d.mts", + "default": "./dist/artplayer-tool-iframe.mjs" + }, + "require": { + "types": "./types/artplayer-tool-iframe.d.cts", + "default": "./dist/artplayer-tool-iframe.js" + } }, "./legacy": { - "types": "./types/artplayer-tool-iframe.d.ts", + "types": "./types/artplayer-tool-iframe.d.cts", "import": "./dist/artplayer-tool-iframe.legacy.js", "require": "./dist/artplayer-tool-iframe.legacy.js" } @@ -33,5 +38,10 @@ "module": "./dist/artplayer-tool-iframe.mjs", "types": "./types/artplayer-tool-iframe.d.ts", "legacy": "./dist/artplayer-tool-iframe.legacy.js", - "browserslist": "last 1 Chrome version" + "browserslist": "last 1 Chrome version", + "typesVersions": { + "*": { + "legacy": ["types/artplayer-tool-iframe.d.ts"] + } + } } diff --git a/packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.cts b/packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.cts new file mode 100644 index 000000000..72d56ab7d --- /dev/null +++ b/packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.cts @@ -0,0 +1,22 @@ +/* eslint-disable ts/no-redeclare -- Constructor, instance and named-type namespace intentionally merge. */ +import type * as Definition from './artplayer-tool-iframe.js' + +declare const ArtplayerToolIframe: typeof Definition.default +type ArtplayerToolIframe = Definition.default + +declare namespace ArtplayerToolIframe { + type Option = Definition.Option + type Message = Definition.Message + type Callbacks = Definition.Callbacks + type Notification = Definition.Notification + type MessageCallback = Definition.MessageCallback + type OutboundMessage = Definition.OutboundMessage + type ProtocolMessage = Definition.ProtocolMessage + type Resolve = Definition.Resolve + type ResolverCallback = Definition.ResolverCallback + type RuntimeInstance = Definition.RuntimeInstance + type ResolverInstance = Definition.ResolverInstance + type RuntimeConstructor = Definition.RuntimeConstructor +} + +export = ArtplayerToolIframe diff --git a/packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.mts b/packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.mts new file mode 100644 index 000000000..34876fd8d --- /dev/null +++ b/packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.mts @@ -0,0 +1,2 @@ +export { default } from './artplayer-tool-iframe.cjs' +export type { Callbacks, Message, MessageCallback, Notification, Option, OutboundMessage, ProtocolMessage, Resolve, ResolverCallback, ResolverInstance, RuntimeConstructor, RuntimeInstance } from './artplayer-tool-iframe.js' diff --git a/packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.ts b/packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.ts index b4093a112..87a7133dd 100644 --- a/packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.ts +++ b/packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.ts @@ -1,18 +1,76 @@ -interface Message { +export interface Option { + iframe: HTMLIFrameElement + url: string +} + +/** Historical open envelope. data remains required in the default class API. */ +export interface Message { type: string - data: any + data: T id?: number } +export interface Callbacks { + resove: (...args: any[]) => any + reject: (...args: any[]) => any +} + +/** Public notifications omit the request id and all private session metadata. */ +export interface Notification { + type: string + data: T +} + +export type MessageCallback = (this: ArtplayerToolIframe, message: Notification) => void + +/** Actual outgoing calls allow an omitted data field. Custom message types remain valid. */ +export interface OutboundMessage { + type: string + data?: T + id?: number +} + +/** Known built-in envelopes; use Message/OutboundMessage for application protocols. */ +export type ProtocolMessage + = | { type: 'inject', data?: undefined, id?: number } + | { type: 'commit', data: string, id: number } + | { type: 'response', data: T, id: number } + | { type: 'error', data: unknown, id: number } + +export type Resolve = (value: T | PromiseLike) => void +export type ResolverCallback = (resolve: Resolve) => void + +/** Opt-in view. Response T is supplied by the application's protocol, not validated at runtime. */ +export interface RuntimeInstance extends Omit { + messageCallback: MessageCallback | null + postMessage: (message: OutboundMessage) => Promise + message: (callback: MessageCallback) => void +} + +/** For the existing serialized resolve(...) protocol; callback bodies must use that exact name. */ +export interface ResolverInstance extends Omit { + commit: (callback: ResolverCallback) => Promise +} + +/** Opt-in static async/optional-data view. There is no runtime self-default property. */ +export interface RuntimeConstructor { + new (option: Option): RuntimeInstance + readonly prototype: RuntimeInstance + readonly iframe: boolean + postMessage: (message: OutboundMessage) => void + onMessage: (event: MessageEvent) => Promise + inject: () => void +} + declare class ArtplayerToolIframe { - constructor(option: { iframe: HTMLIFrameElement, url: string }) + constructor(option: Option) static iframe: boolean static postMessage(message: Message): void static onMessage(event: MessageEvent & { data: Message }): void static inject(): void - readonly promises: Record any, reject: (...args: any[]) => any }> + readonly promises: Record readonly injected: boolean readonly destroyed: boolean readonly $iframe: HTMLIFrameElement diff --git a/refactor/baselines/iframe-types-validation.json b/refactor/baselines/iframe-types-validation.json new file mode 100644 index 000000000..328a9e3a6 --- /dev/null +++ b/refactor/baselines/iframe-types-validation.json @@ -0,0 +1,723 @@ +{ + "task": "PKG-IFRAME-04", + "status": "done", + "checkpoint": "verified-public-types-editor-and-installed-consumers", + "beforeCommit": "1342de94ef2e9dc3e06ddac12fcf1fc5229b648d", + "sourceSha256LF": { + "package.json": "1ca2df37409d1634abe58ce40f50bbee162033af4c57a9908697477e564a7909", + "packages/artplayer-tool-iframe/package.json": "91985c93522c98be5d2a6d02dc0a39df5eeededdd94077758511873fe2290cf4", + "packages/artplayer-tool-iframe/.npmignore": "145c5fa0341195e21cf13df3423a057282f8ea374e9a1b3065e2280e1ce1c5c1", + "packages/artplayer-tool-iframe/README.md": "6e0b3afbad28bf0160d744b89d1a4dad997fff77fb23c61d707389f08f49e219", + "packages/artplayer-tool-iframe/ARCHITECTURE.md": "b5754cfecc61728db0742739f488973f6d9330ebd2f2ec9b60505953fc746546", + "packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.ts": "e1efb4c7f22de24721923f02cf0488728a6ef381505c49187eba0bc2b90b1207", + "packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.cts": "66850135f45c81d4bd7a0de33b6a2f63a655de9000f2698f43660039727919f3", + "packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.mts": "cd394fd68e61fab3c4474daab29852a3f87dd0086b007e7937106871272676fb", + "docs/assets/ts/artplayer-tool-iframe.d.ts": "c06c6819146ba64bd6ee498871d9e19ab5a9e88c9cc7bed2c0c4968fa39dfed7", + "scripts/build-ts.js": "ce6e8da55d934c31f2291326b33a577c9b7dd15b84167f3c63142085d3dcb56d", + "scripts/plugin-editor-types.mjs": "c485550ed0f44d38c2c78faced58c2bec08d496f4bd5a66f4c3dd2ed4bc8b57e", + "scripts/typecheck.mjs": "02aa38836fc33b01f1e2de3def1443ed4d35f038a3595e11106a4dfbff12e86d", + "test/types/iframe.ts": "7ecdc1f7cafda5edb3dbe0908c1029ea7cf3308eb909db661f3cfec27f434b90", + "test/types/iframe-commonjs.cts": "16ac9bea7bb5fd1e5b79301d3f4e3d2761357fbe96a05211a941e26853866729", + "test/types/iframe-source.ts": "29d816c19301d16c81521e809245ac33783362a9023d8cd9ac26185fb3e85ad6", + "refactor/fixtures/consumers/iframe-workspace.ts": "c81fc54c919aabab57ead9efcffc9fe3e469be7fb3b57fbcafa5fd2653d83afb", + "refactor/scripts/iframe-types.test.mjs": "69931a4b0779f996c0af46929c92ce1c9ccc437067a42fa1e9fac9d39ab856a0", + "refactor/scripts/iframe-package-types.mjs": "8dd0006f605752ffdbf51c58bf5ee4cca503dc6c3c1793a8b03dc14fc62d27ea" + }, + "artifacts": { + "packages/artplayer-tool-iframe/dist/artplayer-tool-iframe.js": "7408fceccb000e1b304abe30ac9bc364da7860c3896ad5aaa6af427804c237e6", + "packages/artplayer-tool-iframe/dist/artplayer-tool-iframe.legacy.js": "4e8bd39cdc37e98bff9cd19b5e0c9453ebe5231264242fc1c573aa96edb5ea8a", + "packages/artplayer-tool-iframe/dist/artplayer-tool-iframe.mjs": "ac4c5efa670c0429fc95bdab7902df60f5b5540f0dad9c7e65c2bd660796dc83" + }, + "browserEvidenceReuse": { + "file": "refactor/baselines/iframe-navigation-validation.json", + "reason": "All six production source modules and three runtime build bytes are unchanged; this task changes declarations/editor generation and manifest type routing. Native frame matrices remain 351/351/207 from task 03; installed export routing is separately tested here." + }, + "installed": { + "report": "refactor/.cache/iframe-package-types-AHSyi2/report.json", + "sha256": "1dadc173afa37655cd6c148cb34483f93e82fe82ae7897f6f4dfe89b8eb8b18b", + "task": "PKG-IFRAME-04", + "scope": "Actual npm archive, frozen Git workspace pack and candidate Yarn tarball installed outside workspace; byte verification, offline/frozen lock and strict positive/negative consumers. Old npm CJS namespace and distinct helper are preserved observations, not claims the candidate provides old package-name/helper aliases. Native player and distribution migration remain 05/06.", + "packages": [ + { + "label": "published", + "name": "artplayer-plugin-iframe", + "archive": "D:\\github\\ArtPlayer\\refactor\\.cache\\releases\\artplayer-plugin-iframe-1.0.0.tgz", + "sha256": "16dc92aa84d93bd99bc0a34815dc49f1609fafc1ff255eb353ea893e70b5f91f", + "files": { + "package/README.md": "52c0a1199b6529d60ce8f4414984f9bd1bc3a8daa5bb6cfec8f0aae0c8338e46", + "package/dist/artplayer-helper-iframe.js": "47730ed608f0637594549b9762c01191686a4a55b3bd467132304a016d227f4a", + "package/dist/artplayer-helper-iframe.legacy.js": "f4c3b11806cad6d984f03a2d9de63deba78cbe538ffe4044a106b0dddc5d4197", + "package/dist/artplayer-plugin-iframe.js": "df469c3e4582b127f35798494d0845f1fc4ca2d19bbaf01b918492e035479745", + "package/dist/artplayer-plugin-iframe.legacy.js": "15299cb5c848cc61614f7573a802443d69b7c6d0a938a1867835c7cbb4884edd", + "package/package.json": "563a6c8407753b92b86319a5e6d9d567525146c32cb010b45b0d88ff481859f6", + "package/src/index.js": "2e0b9efeedef0d6c4f2d2b526db35f634cd02af044e4e28dd1a0b26a5b9a02bf", + "package/types/artplayer-plugin-iframe.d.ts": "d55d99a2488d60892a0ab82882dc96a0f373dab0c3d2a3cee978bc35aa7c2d0c" + } + }, + { + "label": "workspace", + "name": "artplayer-tool-iframe", + "archive": "D:\\github\\ArtPlayer\\refactor\\.cache\\iframe-package-types-AHSyi2\\workspace.tgz", + "sha256": "6fc603122610b785aa3ecb43f2692fb92c39e96386ca1bcab5ee23253af92ae6", + "files": { + "package/README.md": "6910a44b767ff80c1d1edc3c90ca7ca77a9b45307fb61d4abd9d52526c7614b3", + "package/dist/artplayer-tool-iframe.js": "2ec257c8055a9448d12af3fecf1fae8ba58c06f47e85072d02c386cd258b0c86", + "package/dist/artplayer-tool-iframe.legacy.js": "c0755d79f316cda71f045da86749648f95faf47c09180d525fb04bb753576d5f", + "package/dist/artplayer-tool-iframe.mjs": "cbcdb0516dd5f17ca2572a6b3e363ea63b0cfd9733f3e47689c02088f23f29b9", + "package/package.json": "b28f5156f02a250539f0acaf302ec87fdd506f63b0e73dd308326ec78c691d03", + "package/types/artplayer-tool-iframe.d.ts": "4f02aa22a6b373393ead533ae3421ad5acf0de7116057f351021c72d3448bd1e" + } + }, + { + "label": "candidate", + "name": "artplayer-tool-iframe", + "archive": "D:\\github\\ArtPlayer\\refactor\\.cache\\iframe-package-types-AHSyi2\\candidate.tgz", + "sha256": "25e88268584f67cb690048f8cb53312b6fcfe64d622a70bf954604a402be9471", + "files": { + "package/ARCHITECTURE.md": "b5754cfecc61728db0742739f488973f6d9330ebd2f2ec9b60505953fc746546", + "package/README.md": "ef5af60bcb831667a78cc3421f6e3c7b5d9a50b3d2e571c5cd02d46fc533e9ce", + "package/dist/artplayer-tool-iframe.js": "7408fceccb000e1b304abe30ac9bc364da7860c3896ad5aaa6af427804c237e6", + "package/dist/artplayer-tool-iframe.legacy.js": "4e8bd39cdc37e98bff9cd19b5e0c9453ebe5231264242fc1c573aa96edb5ea8a", + "package/dist/artplayer-tool-iframe.mjs": "ac4c5efa670c0429fc95bdab7902df60f5b5540f0dad9c7e65c2bd660796dc83", + "package/package.json": "0de9c86eeb3f9a2fb46f9297105b5beddb8d6164163ffaa9874cf5cd68ba7a58", + "package/types/artplayer-tool-iframe.d.cts": "66850135f45c81d4bd7a0de33b6a2f63a655de9000f2698f43660039727919f3", + "package/types/artplayer-tool-iframe.d.mts": "cd394fd68e61fab3c4474daab29852a3f87dd0086b007e7937106871272676fb", + "package/types/artplayer-tool-iframe.d.ts": "e760de22559caf7ffff48d7c5713485bf9f0ddbe876a93080295fb6106535bfa" + } + } + ], + "matrix": [ + { + "package": "published", + "compiler": "5.9.3", + "mode": "node10-commonjs", + "diagnostics": [], + "negative": [] + }, + { + "package": "published", + "compiler": "5.9.3", + "mode": "nodenext-cjs", + "diagnostics": [], + "negative": [] + }, + { + "package": "published", + "compiler": "5.9.3", + "mode": "nodenext-esm", + "diagnostics": [], + "negative": [] + }, + { + "package": "published", + "compiler": "5.9.3", + "mode": "bundler-esm", + "diagnostics": [], + "negative": [] + }, + { + "package": "published", + "compiler": "4.3.5", + "mode": "node10-commonjs", + "diagnostics": [], + "negative": [] + }, + { + "package": "workspace", + "compiler": "5.9.3", + "mode": "node10-commonjs", + "diagnostics": [], + "negative": [] + }, + { + "package": "workspace", + "compiler": "5.9.3", + "mode": "nodenext-cjs", + "diagnostics": [], + "negative": [] + }, + { + "package": "workspace", + "compiler": "5.9.3", + "mode": "nodenext-esm", + "diagnostics": [ + { + "code": 2344, + "message": "Type 'false' does not satisfy the constraint 'true'." + }, + { + "code": 2344, + "message": "Type 'typeof import(\"/node_modules/artplayer-tool-iframe/types/artplayer-tool-iframe\")' does not satisfy the constraint 'abstract new (...args: any) => any'.\n Type 'typeof import(\"/node_modules/artplayer-tool-iframe/types/artplayer-tool-iframe\")' provides no match for the signature 'new (...args: any): any'." + }, + { + "code": 2344, + "message": "Type 'false' does not satisfy the constraint 'true'." + }, + { + "code": 2709, + "message": "Cannot use namespace 'Iframe' as a type." + }, + { + "code": 2339, + "message": "Property 'onMessage' does not exist on type 'typeof import(\"/node_modules/artplayer-tool-iframe/types/artplayer-tool-iframe\")'." + }, + { + "code": 2344, + "message": "Type 'false' does not satisfy the constraint 'true'." + }, + { + "code": 2709, + "message": "Cannot use namespace 'Iframe' as a type." + }, + { + "code": 2344, + "message": "Type 'false' does not satisfy the constraint 'true'." + }, + { + "code": 2709, + "message": "Cannot use namespace 'Iframe' as a type." + }, + { + "code": 2709, + "message": "Cannot use namespace 'Iframe' as a type." + }, + { + "code": 2351, + "message": "This expression is not constructable.\n Type 'typeof import(\"/node_modules/artplayer-tool-iframe/types/artplayer-tool-iframe\")' has no construct signatures." + }, + { + "code": 2339, + "message": "Property 'postMessage' does not exist on type 'typeof import(\"/node_modules/artplayer-tool-iframe/types/artplayer-tool-iframe\")'." + }, + { + "code": 2339, + "message": "Property 'inject' does not exist on type 'typeof import(\"/node_modules/artplayer-tool-iframe/types/artplayer-tool-iframe\")'." + }, + { + "code": 2339, + "message": "Property 'onMessage' does not exist on type 'typeof import(\"/node_modules/artplayer-tool-iframe/types/artplayer-tool-iframe\")'." + }, + { + "code": 7006, + "message": "Parameter 'value' implicitly has an 'any' type." + }, + { + "code": 7006, + "message": "Parameter 'additional' implicitly has an 'any' type." + }, + { + "code": 2339, + "message": "Property 'onMessage' does not exist on type 'typeof import(\"/node_modules/artplayer-tool-iframe/types/artplayer-tool-iframe\")'." + }, + { + "code": 2339, + "message": "Property 'iframe' does not exist on type 'typeof import(\"/node_modules/artplayer-tool-iframe/types/artplayer-tool-iframe\")'." + } + ], + "negative": [] + }, + { + "package": "workspace", + "compiler": "5.9.3", + "mode": "bundler-esm", + "diagnostics": [], + "negative": [] + }, + { + "package": "workspace", + "compiler": "4.3.5", + "mode": "node10-commonjs", + "diagnostics": [], + "negative": [] + }, + { + "package": "candidate", + "compiler": "5.9.3", + "mode": "node10-commonjs", + "diagnostics": [], + "negative": [ + { + "code": 2554, + "message": "Expected 1 arguments, but got 0." + }, + { + "code": 2322, + "message": "Type 'string' is not assignable to type 'HTMLIFrameElement'." + }, + { + "code": 2322, + "message": "Type 'number' is not assignable to type 'string'." + }, + { + "code": 2345, + "message": "Argument of type '{ type: string; }' is not assignable to parameter of type 'Message'.\n Property 'data' is missing in type '{ type: string; }' but required in type 'Message'." + }, + { + "code": 2322, + "message": "Type 'number' is not assignable to type 'string'." + }, + { + "code": 2322, + "message": "Type 'string' is not assignable to type 'number'." + }, + { + "code": 2540, + "message": "Cannot assign to 'injected' because it is a read-only property." + }, + { + "code": 2322, + "message": "Type 'void' is not assignable to type 'Promise'." + }, + { + "code": 2339, + "message": "Property 'default' does not exist on type 'typeof ArtplayerToolIframe'." + }, + { + "code": 2540, + "message": "Cannot assign to 'iframe' because it is a read-only property." + }, + { + "code": 2353, + "message": "Object literal may only specify known properties, and 'id' does not exist in type 'Notification'." + }, + { + "code": 2322, + "message": "Type 'string' is not assignable to type 'number'." + }, + { + "code": 2345, + "message": "Argument of type '{ type: \"response\"; data: string; id: number; }' is not assignable to parameter of type 'ProtocolMessage'.\n Types of property 'data' are incompatible.\n Type 'string' is not assignable to type 'number'." + }, + { + "code": 2345, + "message": "Argument of type 'string' is not assignable to parameter of type 'number | PromiseLike'." + }, + { + "code": 2551, + "message": "Property 'resolve' does not exist on type 'Callbacks'. Did you mean 'resove'?" + }, + { + "code": 18046, + "message": "'message.data' is of type 'unknown'." + } + ] + }, + { + "package": "candidate", + "compiler": "5.9.3", + "mode": "nodenext-cjs", + "diagnostics": [], + "negative": [ + { + "code": 2554, + "message": "Expected 1 arguments, but got 0." + }, + { + "code": 2322, + "message": "Type 'string' is not assignable to type 'HTMLIFrameElement'." + }, + { + "code": 2322, + "message": "Type 'number' is not assignable to type 'string'." + }, + { + "code": 2345, + "message": "Argument of type '{ type: string; }' is not assignable to parameter of type 'Message'.\n Property 'data' is missing in type '{ type: string; }' but required in type 'Message'." + }, + { + "code": 2322, + "message": "Type 'number' is not assignable to type 'string'." + }, + { + "code": 2322, + "message": "Type 'string' is not assignable to type 'number'." + }, + { + "code": 2540, + "message": "Cannot assign to 'injected' because it is a read-only property." + }, + { + "code": 2322, + "message": "Type 'void' is not assignable to type 'Promise'." + }, + { + "code": 2339, + "message": "Property 'default' does not exist on type 'typeof ArtplayerToolIframe'." + }, + { + "code": 2540, + "message": "Cannot assign to 'iframe' because it is a read-only property." + }, + { + "code": 2353, + "message": "Object literal may only specify known properties, and 'id' does not exist in type 'Notification'." + }, + { + "code": 2322, + "message": "Type 'string' is not assignable to type 'number'." + }, + { + "code": 2345, + "message": "Argument of type '{ type: \"response\"; data: string; id: number; }' is not assignable to parameter of type 'ProtocolMessage'.\n Types of property 'data' are incompatible.\n Type 'string' is not assignable to type 'number'." + }, + { + "code": 2345, + "message": "Argument of type 'string' is not assignable to parameter of type 'number | PromiseLike'." + }, + { + "code": 2551, + "message": "Property 'resolve' does not exist on type 'Callbacks'. Did you mean 'resove'?" + }, + { + "code": 18046, + "message": "'message.data' is of type 'unknown'." + } + ] + }, + { + "package": "candidate", + "compiler": "5.9.3", + "mode": "nodenext-esm", + "diagnostics": [], + "negative": [ + { + "code": 2554, + "message": "Expected 1 arguments, but got 0." + }, + { + "code": 2322, + "message": "Type 'string' is not assignable to type 'HTMLIFrameElement'." + }, + { + "code": 2322, + "message": "Type 'number' is not assignable to type 'string'." + }, + { + "code": 2345, + "message": "Argument of type '{ type: string; }' is not assignable to parameter of type 'Message'.\n Property 'data' is missing in type '{ type: string; }' but required in type 'Message'." + }, + { + "code": 2322, + "message": "Type 'number' is not assignable to type 'string'." + }, + { + "code": 2322, + "message": "Type 'string' is not assignable to type 'number'." + }, + { + "code": 2540, + "message": "Cannot assign to 'injected' because it is a read-only property." + }, + { + "code": 2322, + "message": "Type 'void' is not assignable to type 'Promise'." + }, + { + "code": 2339, + "message": "Property 'default' does not exist on type 'typeof ArtplayerToolIframe'." + }, + { + "code": 2540, + "message": "Cannot assign to 'iframe' because it is a read-only property." + }, + { + "code": 2353, + "message": "Object literal may only specify known properties, and 'id' does not exist in type 'Notification'." + }, + { + "code": 2322, + "message": "Type 'string' is not assignable to type 'number'." + }, + { + "code": 2345, + "message": "Argument of type '{ type: \"response\"; data: string; id: number; }' is not assignable to parameter of type 'ProtocolMessage'.\n Types of property 'data' are incompatible.\n Type 'string' is not assignable to type 'number'." + }, + { + "code": 2345, + "message": "Argument of type 'string' is not assignable to parameter of type 'number | PromiseLike'." + }, + { + "code": 2551, + "message": "Property 'resolve' does not exist on type 'Callbacks'. Did you mean 'resove'?" + }, + { + "code": 18046, + "message": "'message.data' is of type 'unknown'." + } + ] + }, + { + "package": "candidate", + "compiler": "5.9.3", + "mode": "bundler-esm", + "diagnostics": [], + "negative": [ + { + "code": 2554, + "message": "Expected 1 arguments, but got 0." + }, + { + "code": 2322, + "message": "Type 'string' is not assignable to type 'HTMLIFrameElement'." + }, + { + "code": 2322, + "message": "Type 'number' is not assignable to type 'string'." + }, + { + "code": 2345, + "message": "Argument of type '{ type: string; }' is not assignable to parameter of type 'Message'.\n Property 'data' is missing in type '{ type: string; }' but required in type 'Message'." + }, + { + "code": 2322, + "message": "Type 'number' is not assignable to type 'string'." + }, + { + "code": 2322, + "message": "Type 'string' is not assignable to type 'number'." + }, + { + "code": 2540, + "message": "Cannot assign to 'injected' because it is a read-only property." + }, + { + "code": 2322, + "message": "Type 'void' is not assignable to type 'Promise'." + }, + { + "code": 2339, + "message": "Property 'default' does not exist on type 'typeof ArtplayerToolIframe'." + }, + { + "code": 2540, + "message": "Cannot assign to 'iframe' because it is a read-only property." + }, + { + "code": 2353, + "message": "Object literal may only specify known properties, and 'id' does not exist in type 'Notification'." + }, + { + "code": 2322, + "message": "Type 'string' is not assignable to type 'number'." + }, + { + "code": 2345, + "message": "Argument of type '{ type: \"response\"; data: string; id: number; }' is not assignable to parameter of type 'ProtocolMessage'.\n Types of property 'data' are incompatible.\n Type 'string' is not assignable to type 'number'." + }, + { + "code": 2345, + "message": "Argument of type 'string' is not assignable to parameter of type 'number | PromiseLike'." + }, + { + "code": 2551, + "message": "Property 'resolve' does not exist on type 'Callbacks'. Did you mean 'resove'?" + }, + { + "code": 18046, + "message": "'message.data' is of type 'unknown'." + } + ] + }, + { + "package": "candidate", + "compiler": "4.3.5", + "mode": "node10-commonjs", + "diagnostics": [], + "negative": [ + { + "code": 2554, + "message": "Expected 1 arguments, but got 0." + }, + { + "code": 2322, + "message": "Type 'string' is not assignable to type 'HTMLIFrameElement'." + }, + { + "code": 2322, + "message": "Type 'number' is not assignable to type 'string'." + }, + { + "code": 2345, + "message": "Argument of type '{ type: string; }' is not assignable to parameter of type 'Message'.\n Property 'data' is missing in type '{ type: string; }' but required in type 'Message'." + }, + { + "code": 2322, + "message": "Type 'number' is not assignable to type 'string'." + }, + { + "code": 2322, + "message": "Type 'string' is not assignable to type 'number | undefined'." + }, + { + "code": 2540, + "message": "Cannot assign to 'injected' because it is a read-only property." + }, + { + "code": 2322, + "message": "Type 'void' is not assignable to type 'Promise'." + }, + { + "code": 2339, + "message": "Property 'default' does not exist on type 'typeof ArtplayerToolIframe'." + }, + { + "code": 2540, + "message": "Cannot assign to 'iframe' because it is a read-only property." + }, + { + "code": 2322, + "message": "Type '{ type: string; data: number; id: number; }' is not assignable to type 'Notification'.\n Object literal may only specify known properties, and 'id' does not exist in type 'Notification'." + }, + { + "code": 2322, + "message": "Type 'string' is not assignable to type 'number | undefined'." + }, + { + "code": 2345, + "message": "Argument of type '{ type: \"response\"; data: string; id: number; }' is not assignable to parameter of type 'ProtocolMessage'.\n Type '{ type: \"response\"; data: string; id: number; }' is not assignable to type '{ type: \"response\"; data: number; id: number; }'.\n Types of property 'data' are incompatible.\n Type 'string' is not assignable to type 'number'." + }, + { + "code": 2345, + "message": "Argument of type 'string' is not assignable to parameter of type 'number | PromiseLike'." + }, + { + "code": 2551, + "message": "Property 'resolve' does not exist on type 'Callbacks'. Did you mean 'resove'?" + }, + { + "code": 2571, + "message": "Object is of type 'unknown'." + } + ] + }, + { + "package": "candidate", + "compiler": "5.9.3", + "mode": "nodenext-cjs-no-interop", + "diagnostics": [], + "negative": [ + { + "code": 2339, + "message": "Property 'default' does not exist on type 'typeof ArtplayerToolIframe'." + } + ] + } + ], + "runtime": [ + { + "package": "published", + "commonjs": "object", + "commonjsDefault": "function", + "esmDefault": "object", + "helperIsDistinct": true, + "ssrImport": true + }, + { + "package": "workspace", + "commonjs": "function", + "commonjsDefault": "undefined", + "esmDefault": "function", + "helperIsDistinct": false, + "ssrImport": true + }, + { + "package": "candidate", + "commonjs": "function", + "commonjsDefault": "undefined", + "esmDefault": "function", + "helperIsDistinct": false, + "ssrImport": true + } + ] + }, + "diagnostics": { + "editorBefore": { + "file": "refactor/.cache/iframe04-editor-before.json", + "codes": [ + 2309, + 2686, + 2303 + ], + "meaning": "Frozen default/export= combination and incorrect lowercase global; both compiler versions reproduce the same three codes, in compiler-specific ordering." + }, + "workspaceNodeNextEsm": { + "report": "refactor/.cache/iframe-package-types-6qThuH/workspace-5.9.3-nodenext-esm.json", + "count": 18, + "meaning": "Frozen workspace declaration represents a namespace rather than a constructable native ESM default. Candidate paired d.mts/d.cts routes pass the unchanged workspace fixture." + }, + "trials": [ + "refactor/.cache/iframe04-types-first.log", + "refactor/.cache/iframe04-types-second.log", + "refactor/.cache/iframe04-editor-build.log", + "refactor/.cache/iframe04-lint-fix.log" + ], + "meaning": "Initial tests exposed historical diagnostic ordering/context differences and the known nullable-source callback mismatch; exact expectations are explicit. The editor build found method-signature lint errors, repaired in authored types before regeneration.", + "intermediateCi": { + "log": "refactor/.cache/iframe04-ci.log", + "tests": 1542, + "status": "pass-before-error-payload-review", + "scope": "Not final acceptance after ProtocolMessage error payload was corrected to unknown." + }, + "nonStringErrors": { + "file": "refactor/.cache/iframe04-error-payload-probe.json", + "sha256": "e5d81d661be5fe555def11f4366443ce463e7b0b53fbd5cca4d11b0ac64a779e", + "observations": 10, + "scope": "Two non-Error throws across four immutable historical artifacts and candidate source produce numeric/undefined data. Permanent baseline regression repeats these checks." + } + }, + "preservedDifferences": [ + "Default Message.data remains required and static onMessage remains void; RuntimeConstructor exposes the actual optional-data/Promise surface.", + "Default callback remains non-null while actual source allows null. The source satisfies RuntimeConstructor; exact historical mismatch 2419 is asserted separately, not hidden by skipLibCheck.", + "Default commit keeps Promise>, including old nested-Promise inference. ResolverInstance provides an explicit result for the literal resolve(...) body protocol; neither view adds async-function-body or closure support.", + "Published Function callbacks versus callable workspace callbacks are an existing cross-package-name declaration difference, with both compiler diagnostics pinned. Old-name/helper distribution is still IFRAME-06.", + "Historical npm native ESM default is an object despite accepted old declaration usage; the candidate tool native ESM default is a class. No old package/helper alias has been delivered here.", + "No self-default is added. Old-style TypeScript CommonJS default imports require interop; modern direct import= is checked without interop.", + "Known error packets carry unknown data because thrown values need not be Error objects. New typed consumers narrow before treating the payload as text." + ], + "checks": [ + { + "command": "yarn ci:check", + "result": "pass", + "exitCode": 0, + "tests": 1543, + "unit": 1365, + "engineering": 14, + "baseline": 164, + "repeatedContracts": 44, + "productionTypeScriptFiles": 330, + "log": "refactor/.cache/iframe04-ci-final.log", + "sha256": "7a0b080a0e6f708c0f8bdbd831d8e93b0970b5779f4c7ded42ea96be4fc0da64" + }, + { + "command": "yarn test:imports", + "result": "pass", + "tests": 3, + "log": "refactor/.cache/iframe04-imports.log" + }, + { + "command": "yarn build:ts artplayer-tool-iframe", + "result": "pass", + "log": "refactor/.cache/iframe04-editor-build-verified.log" + }, + { + "command": "yarn build artplayer-tool-iframe", + "result": "pass", + "log": "refactor/.cache/iframe04-build.log", + "scope": "All three runtime outputs remain byte-identical to task 03." + }, + { + "command": "yarn test:iframe-types-package", + "result": "pass", + "log": "refactor/.cache/iframe04-package-verified.log", + "compilerCases": 16, + "candidateCases": 6, + "candidateNegativeDiagnostics": 81 + } + ], + "remaining": [ + "PKG-IFRAME-05 full core/demo/BFCache/device/interrupted-navigation integration", + "PKG-IFRAME-06 old package name/helper paths, emitted consumer runtime and final distribution", + "PKG-MB-10 standalone editor treatment of split media declarations surfaced during an initial all-package generation; unrelated generated changes were restored and no completion is claimed" + ], + "verifiedAt": "2026-09-12T17:41:36.279Z" +} diff --git a/refactor/changes/2026-09-13-PKG-IFRAME-04-types.md b/refactor/changes/2026-09-13-PKG-IFRAME-04-types.md new file mode 100644 index 000000000..1b0ef56f0 --- /dev/null +++ b/refactor/changes/2026-09-13-PKG-IFRAME-04-types.md @@ -0,0 +1,88 @@ +# PKG-IFRAME-04 public types and installed consumers + +Started from 1342de94ef2e9dc3e06ddac12fcf1fc5229b648d with a clean worktree. +The previous task completed six strict source modules and native document +ownership. This task owns public declarations and their consumer/editor paths. + +## Verified starting points + +- Frozen npm plugin-iframe@1.0.0 has export= / global ArtplayerPluginIframe but + actually returns a CommonJS default namespace. Its extra helper is a distinct + protocol and has no dedicated declaration. No tool@1.1.0 npm archive is invented. +- Frozen/current workspace default class has required Message.data, readonly + instance fields, void static onMessage and Promise> commit. + Runtime static onMessage is async, data may be omitted and callback can be null. + Preserve legacy extraction/replacement types; expose precise opt-in views. +- Current main/legacy return the class directly, with no self-default property. + Do not borrow factory.default declarations from other packages. +- The generated editor declaration uses lowercase artplayerToolIframe although + runtime/class is ArtplayerToolIframe, and combines default export with export=. + Reproduce and repair generation, not the generated file by hand. + +## Planned acceptance + +Named option/message/protocol/callback types, unchanged default class signatures, +precise optional runtime views, correct CJS/ESM declaration routing, strict +current/old compiler consumers and standalone editor generation. Isolated Yarn +pack/install must distinguish the real old npm archive, frozen workspace and +candidate, preserve exact historical errors and verify no workspace type leakage. +Legacy package-name/helper distribution remains independently tracked by 06; +full player/demo/device integration remains 05. Do not claim either passed here. + +## Implementation and decisions + +The default .d.ts keeps all old field/method types and adds named option, envelope, +notification and callback types. The CJS .d.cts exports the actual class with an +instance alias and type namespace; .d.mts handles native ESM. The legacy route and +typesVersions preserve old compiler resolution. No runtime default property or +JavaScript method is added. Source files and all three runtime artifacts remain +byte-identical to task 03; package/editor declaration routes are tested separately. + +RuntimeConstructor/RuntimeInstance opt into async static receipt, optional outgoing +data and a nullable typed callback. Generic response T is an application assertion, +not payload validation. ResolverInstance gives an explicit result for the existing +literal resolve(...) function-body protocol. The default nested-Promise/void commit +inference remains unchanged; no async-function-body or closure execution is added. + +Independent review proved error payloads may be numbers or undefined when executed +code throws non-Error values, in four historical main/legacy builds and candidate +source. The new ProtocolMessage error data therefore uses unknown with narrowing; +the first green CI predated this correction and is retained as intermediate only. + +The frozen editor yields 2309/2686/2303; semantic class generation removes the +default/export= conflict and uses ArtplayerToolIframe with named-type aliases. +build:ts accepts selected package names to regenerate the current package without +rewriting unrelated packages mid-migration. Its old all-package mode remains; +the initial all-package run exposed split MediaBunny media declaration treatment, +now explicitly assigned to MB-10. No hand-editing of generated declarations. + +Isolated Yarn installs cover the real npm archive, a pack made exclusively from +frozen Git inputs, and candidate pack. Every installed file matches its archive; +frozen reinstall preserves the lock and compiler files cannot resolve outside the +consumer except standard libraries. Old workspace NodeNext ESM has 18 exact +diagnostics; candidate accepts the unchanged fixture. Published Function callback +fields and its CJS namespace are existing differences from workspace callable +fields/direct class, not silently rewritten. Old package/helper facade and emitted +consumer compatibility stay under IFRAME-06, with IFRAME-DIST-01 still open. + +The default non-null callback also differs from source's nullable field. Tests pin +exact diagnostic 2419 for the old view and prove the source satisfies the opt-in +RuntimeConstructor. This documents a preserved type boundary, not skipLibCheck. + +No new dependencies, version changes, push or publish. Final checks and hashes +are in baselines/iframe-types-validation.json. Roll back this task's declarations, +manifest and generator changes together, then regenerate the iframe editor types. +Completion subject: `refactor(iframe): [PKG-IFRAME-04] preserve types across module consumers`. + +## Final verification + +Full CI passes 1543 (1365 unit + 14 engineering + 164 baseline), 44 repeated +contracts and 330 production TS files. The four new baseline tests include +positive/negative views, exact historical differences, error-payload runtime +evidence and both-compiler standalone editor checks. Installed consumers verify +16 compiler cases across three archives, including six candidate cases with +81 rejected invalid uses; old workspace ESM errors stay explicit negative controls. +Three import/SSR files pass. Source/declaration/editor hashes, final tarball bytes, +all runtime source modules and byte-identical runtime/docs outputs are verified. +Package-name/helper and full integration gates remain open under 05/06. This is +a declaration/consumer completion, not an npm release or final browser/demo review. diff --git a/refactor/fixtures/consumers/iframe-workspace.ts b/refactor/fixtures/consumers/iframe-workspace.ts new file mode 100644 index 000000000..ea925707b --- /dev/null +++ b/refactor/fixtures/consumers/iframe-workspace.ts @@ -0,0 +1,37 @@ +import Iframe from 'artplayer-tool-iframe' + +type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false +type Assert = T +interface OldMessage { type: string, data: any, id?: number } +type ExactConstructor = Assert, [option: { iframe: HTMLIFrameElement, url: string }]>> +type ExactMessage = Assert, [message: OldMessage]>> +type ExactStaticReceiver = Assert, void>> +type ExactCallbacks = Assert any, reject: (...args: any[]) => any }>>> +type ExactCallback = Assert any>> + +declare const frame: HTMLIFrameElement +const tool: Iframe = new Iframe({ iframe: frame, url: '/iframe.html' }) +const message: OldMessage = { type: 'custom', data: { enabled: true }, id: 1 } +Iframe.postMessage(message) +Iframe.inject() +const staticResult: void = Iframe.onMessage(new MessageEvent('message', { data: message })) +tool.onMessage(new MessageEvent('message', { data: message })) +const custom: Promise = tool.postMessage(message) +const sync: Promise = tool.commit(() => { + return 1 +}) +const nested: Promise> = tool.commit(() => { + return Promise.resolve(1) +}) +const resolver: Promise = tool.commit((resolve: (value: number) => void) => { + resolve(1) +}) +tool.message((value, additional) => [value, additional]) +tool.promises[1] = { resove: (...args: any[]) => args, reject: (...args: any[]) => args } +tool.promises[1].resove(1, 2) +tool.promises[1].reject(new Error('old')) +const replacement: typeof Iframe.onMessage = () => {} +const enabled: boolean = Iframe.iframe +tool.destroy() +void [staticResult, custom, sync, nested, resolver, replacement, enabled] +export type { ExactCallback, ExactCallbacks, ExactConstructor, ExactMessage, ExactStaticReceiver } diff --git a/refactor/plan.md b/refactor/plan.md index 2981ebb55..0412c258a 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -4,7 +4,7 @@ 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。 -状态:todo 105 / doing 9 / blocked 0 / done 108 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 104 / doing 9 / blocked 0 / done 109 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -338,7 +338,7 @@ | PKG-MB-07 | artplayer-proxy-mediabunny
整理 HLS 配对轨道与 selector | PKG-MB-05, PKG-MB-06, CORE-14 | m3u8 配对、质量/音频选择及拓扑清理 | 实际选择高亮、切到无轨道来源清理、无重复 readiness | H | done | | PKG-MB-08 | artplayer-proxy-mediabunny
完成 TS 与媒体能力声明 | PKG-MB-07, ENG-04, ENG-06 | 8 个自有 JS 模块迁移与 Result/shim 类型 | 解码器/Stream/DOM 类型清楚,旧 Option 和 art.mediabunny 使用保持 | H | done | | PKG-MB-09 | artplayer-proxy-mediabunny
完成新旧核心和真实媒体组合 | PKG-MB-08, CORE-22 | 跨浏览器能力、长播放、DPiP、HLS 音轨/质量报告 | 资源释放、事件顺序、AV sync 与支持范围满足基线 | H | doing | -| PKG-MB-10 | artplayer-proxy-mediabunny
完成分发和文档 | PKG-MB-09, ENG-07 | mediabunny.js、README、依赖版本、三产物和许可 | tarball 可消费、无意外依赖升级、旧调用与回退可用 | H | todo | +| PKG-MB-10 | artplayer-proxy-mediabunny
完成分发和文档 | PKG-MB-09, ENG-07 | mediabunny.js、README、依赖版本、三产物和许可; 拆分media声明后的独立编辑器生成/辅助文件过滤与语义检查(IFRAME-04全量生成时暴露) | tarball 可消费、无意外依赖升级、旧调用与回退可用 | H | todo | ## 5 包迁移:artplayer-tool-iframe @@ -347,9 +347,9 @@ | PKG-IFRAME-01 | artplayer-tool-iframe
核对包契约与历史用法 | BASE-05 | constructor/commit/message/inject、postMessage 协议与历史公开拼写 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done | | PKG-IFRAME-02 | artplayer-tool-iframe
建立特有行为与错误测试 | PKG-IFRAME-01, ENG-03, ENG-05 | 跨窗口消息、ID 匹配、请求失败、重复 inject、销毁中请求 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done | | PKG-IFRAME-03 | artplayer-tool-iframe
整理内部职责与资源 | PKG-IFRAME-02, CORE-02, BASE-07 | 请求注册/响应匹配/监听清理分离;origin/source 安全边界独立决策 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done | -| PKG-IFRAME-04 | artplayer-tool-iframe
迁移自有源码和公开类型 | PKG-IFRAME-03, ENG-04, ENG-06, CORE-07 | 消息联合类型、回调/Promise 推导、旧公开字段兼容;旧npm export=与实际namespace、额外helper协议分别核验 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo | +| PKG-IFRAME-04 | artplayer-tool-iframe
迁移自有源码和公开类型 | PKG-IFRAME-03, ENG-04, ENG-06, CORE-07 | 消息联合类型、回调/Promise 推导、旧公开字段兼容;旧npm export=与实际namespace、额外helper协议分别核验 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done | | PKG-IFRAME-05 | artplayer-tool-iframe
验证新旧核心和组合 | PKG-IFRAME-04, CORE-22 | 真实同源/跨源 iframe、既有 commit 协议;安全变化有独立结论;新旧核心/demo、实际 BFCache/设备和外部中断导航验收,旧端无文档标记的限制明确 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo | -| PKG-IFRAME-06 | artplayer-tool-iframe
验证分发并同步文档 | PKG-IFRAME-05, ENG-07 | iframe.js、示例集成和原 script/class 导出验证;旧包名/额外helper深入口与新工具名的迁移结论 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo | +| PKG-IFRAME-06 | artplayer-tool-iframe
验证分发并同步文档 | PKG-IFRAME-05, ENG-07 | iframe.js、示例集成和原 script/class 导出验证;旧包名/额外helper深入口与新工具名的迁移结论; 保留旧npm Function回调及namespace/helper与工具类的区别,核验实际编译后消费而非仅声明通过 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo | ## 5 包迁移:artplayer-tool-thumbnail @@ -533,4 +533,5 @@ - PKG-IFRAME-01: [记录](changes/2026-09-12-PKG-IFRAME-01-contract.md) [记录](baselines/iframe-release.json) [记录](baselines/iframe-contract.md) [记录](baselines/iframe-contract-validation.json) - PKG-IFRAME-02: [记录](changes/2026-09-12-PKG-IFRAME-02-behavior.md) [记录](baselines/iframe-behavior-validation.json) - PKG-IFRAME-03: [记录](changes/2026-09-12-PKG-IFRAME-03-requests-checkpoint.md) [记录](baselines/iframe-requests-checkpoint.json) [记录](changes/2026-09-12-PKG-IFRAME-03-boundaries-checkpoint.md) [记录](baselines/iframe-boundaries-checkpoint.json) [记录](iframe-message-boundary.md) [记录](changes/2026-09-13-PKG-IFRAME-03-navigation.md) [记录](baselines/iframe-navigation-validation.json) [记录](iframe-document-protocol.md) +- PKG-IFRAME-04: [记录](changes/2026-09-13-PKG-IFRAME-04-types.md) [记录](baselines/iframe-types-validation.json) - PKG-FACTORY-01: [记录](baselines/factory-assignment-gaps.json) [记录](baselines/factory-compatibility-proposals.json) [记录](factory-compatibility-decision.md) [记录](changes/2026-09-12-PKG-FACTORY-01-decision.md) diff --git a/refactor/progress.md b/refactor/progress.md index dca175bf4..46a902cba 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,19 @@ # 进度与证据 +## PKG-IFRAME-04 完成:公开类型、模块入口和编辑器 + +保留默认类的required data、void静态接收、readonly、resove及旧commit推导,增加 +可选Runtime/Resolver视图、消息联合与命名类型。CJS/ESM声明分别路由,旧TS通过 +types/legacy入口;无运行时self-default。编辑器使用实际大写类名并语义生成声明。 +旧工作区NodeNext ESM的18条错误已复现并修复;旧编辑器三条错误在两编译器复现。 +独立Yarn安装旧npm、冻结工作区和候选共16编译组合,候选6组/81非法使用被拒绝; +历史Function字段、namespace/helper及nullable回调区别明确保留,05/06仍待验收。 +复查发现非Error抛出可产生非字符串error载荷,已用四历史产物与源码复现并修正类型。 +最终CI1543与44重复契约、3导入/SSR通过,330生产TS。三运行产物及源码字节未变, +沿用03的真实浏览器证据,不冒称新跑浏览器。分包编辑器脚本避免改写迁移中的其他包。 +见[变更](changes/2026-09-13-PKG-IFRAME-04-types.md)和[验证](baselines/iframe-types-validation.json)。 +222项:109 done、9 doing、104 todo;本项独立本地提交,三个Iframe风险仍open。 + ## PKG-IFRAME-03 完成:按文档管理请求与六模块拆分 入口、连接、请求、消息边界、父页导航和子页生命周期拆为六个严格TS模块。 diff --git a/refactor/risks.json b/refactor/risks.json index 7c7fc46ce..dfaceb09c 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -3827,12 +3827,14 @@ ], "status": "open", "confirmation": "reproduced", - "compatibleResolution": "Keep actual old plugin/helper exports distinct from workspace tool and document package-name/type/export migration; do not fabricate a tool release or use a renamed implementation as an old archive.", + "compatibleResolution": "IFRAME-04 adds paired CJS/ESM declarations and verifies three actual isolated installs, including old namespace/helper and existing Function callback differences. Keep old package-name/helper facade, emitted-consumer runtime and final distribution gates with IFRAME-06; the time-scoped tool registry observation is not a release claim.", "closureCriteria": "Actual installed entry/declaration tests and documented old-name/helper/new-tool distribution behavior; registry observation remains time scoped.", "evidence": [ "refactor/baselines/iframe-contract.md", "refactor/baselines/iframe-release.json", - "refactor/changes/2026-09-12-PKG-IFRAME-01-contract.md" + "refactor/changes/2026-09-12-PKG-IFRAME-01-contract.md", + "refactor/changes/2026-09-13-PKG-IFRAME-04-types.md", + "refactor/baselines/iframe-types-validation.json" ] } ] diff --git a/refactor/scripts/iframe-package-types.mjs b/refactor/scripts/iframe-package-types.mjs new file mode 100644 index 000000000..5ef6e9c19 --- /dev/null +++ b/refactor/scripts/iframe-package-types.mjs @@ -0,0 +1,112 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import ts from 'typescript' +import compat from 'typescript-compat' +import { checkFiles, packedFiles } from '../../scripts/package-check.mjs' +import { consumerDirectory, removeConsumer, run, workspace, writeJson } from '../../scripts/package-consumer.mjs' +import { verifyIframeContract } from './iframe-contract.mjs' +import { hash, readMember } from './releases.mjs' + +async function main() { + assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use yarn test:iframe-types-package') + const yarn = process.env.npm_execpath + assert(yarn && fs.existsSync(yarn)) + const output = fs.mkdtempSync(path.join(workspace, 'refactor/.cache/iframe-package-types-')) + console.log(`Iframe installed package evidence: ${output}`) + const contract = await verifyIframeContract() + const frozen = path.join(output, 'frozen-workspace') + fs.mkdirSync(frozen) + const prefix = 'packages/artplayer-tool-iframe/' + for (const [file, bytes] of contract.sources) { + if (!file.startsWith(prefix)) + continue + const target = path.join(frozen, file.slice(prefix.length)) + fs.mkdirSync(path.dirname(target), { recursive: true }) + fs.writeFileSync(target, bytes) + } + const packages = [{ label: 'published', name: contract.baseline.release.name, archive: contract.archive, sha256: contract.baseline.release.sha256, files: contract.baseline.release.files }] + for (const [label, folder] of [['workspace', frozen], ['candidate', path.join(workspace, 'packages/artplayer-tool-iframe')]]) { + const archive = path.join(output, `${label}.tgz`) + fs.writeFileSync(path.join(output, `${label}-pack.log`), run([yarn, 'pack', '--filename', archive], folder)) + const manifest = JSON.parse(readMember(archive, 'package/package.json')) + const files = packedFiles(archive) + checkFiles(manifest, files) + packages.push({ label, name: manifest.name, archive, sha256: hash(fs.readFileSync(archive)), files: Object.fromEntries(files.map(member => [member, hash(readMember(archive, member))])) }) + } + const matrix = [] + const runtime = [] + for (const pkg of packages) { + const consumer = consumerDirectory() + try { + writeJson(path.join(consumer, 'package.json'), { name: 'iframe-isolated-consumer', private: true, dependencies: { [pkg.name]: `file:${pkg.archive.replaceAll('\\', '/')}` } }) + fs.writeFileSync(path.join(output, `${pkg.label}-install.log`), run([yarn, 'install', '--offline', '--ignore-scripts', '--non-interactive'], consumer)) + const lock = fs.readFileSync(path.join(consumer, 'yarn.lock')) + fs.writeFileSync(path.join(output, `${pkg.label}-frozen-install.log`), run([yarn, 'install', '--offline', '--frozen-lockfile', '--ignore-scripts', '--non-interactive'], consumer)) + assert.deepEqual(fs.readFileSync(path.join(consumer, 'yarn.lock')), lock) + fs.writeFileSync(path.join(output, `${pkg.label}-yarn.lock`), lock) + const installed = path.join(consumer, 'node_modules', pkg.name) + assert.equal(fs.realpathSync(installed), installed, 'Consumer must not use workspace links') + for (const [member, expected] of Object.entries(pkg.files)) + assert.equal(hash(fs.readFileSync(path.join(installed, member.slice(8)))), expected, member) + const runtimeFile = path.join(consumer, 'runtime.cjs') + fs.writeFileSync(runtimeFile, `const assert = require('node:assert/strict'); +const value = require('${pkg.name}'); +assert.equal(typeof value, '${pkg.label === 'published' ? 'object' : 'function'}'); +assert.equal(typeof value.default, '${pkg.label === 'published' ? 'function' : 'undefined'}'); +${pkg.label === 'published' ? `for (const suffix of ['js', 'legacy.js']) { const helper = require('${pkg.name}/dist/artplayer-helper-iframe.' + suffix); assert.equal(typeof helper.default, 'function'); assert.equal(typeof helper.default.destroy, 'function'); assert.equal(typeof value.default.destroy, 'undefined'); }` : `const legacy = require('${pkg.name}/legacy'); assert.equal(typeof legacy, 'function'); assert.equal(legacy.default, undefined);`} +import('${pkg.name}').then(module => { + assert.equal(typeof module.default, '${pkg.label === 'published' ? 'object' : 'function'}'); + ${pkg.label === 'published' ? '' : 'assert.deepEqual(Object.keys(module), [\'default\']);'} + console.log(JSON.stringify({ commonjs: typeof value, commonjsDefault: typeof value.default, esmDefault: typeof module.default, helperIsDistinct: ${pkg.label === 'published'}, ssrImport: true })); +}).catch(error => { console.error(error); process.exitCode = 1; });`) + const observation = run([runtimeFile], consumer) + fs.writeFileSync(path.join(output, `${pkg.label}-runtime.log`), observation) + runtime.push({ package: pkg.label, ...JSON.parse(observation.trim()) }) + const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']] + if (pkg.label === 'candidate') + modes.push([ts, 'nodenext-cjs-no-interop']) + for (const [compiler, mode] of modes) { + const next = mode.startsWith('nodenext') + const noInterop = mode.endsWith('no-interop') + const filename = path.join(consumer, `consumer.${next ? mode.includes('-cjs') ? 'cts' : 'mts' : 'ts'}`) + const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: !noInterop, target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], module: next ? compiler.ModuleKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleKind.ESNext : compiler.ModuleKind.CommonJS, moduleResolution: next ? compiler.ModuleResolutionKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleResolutionKind.Bundler : compiler.ModuleResolutionKind.NodeJs } + let source = fs.readFileSync(path.join(workspace, pkg.label === 'candidate' ? noInterop ? 'test/types/iframe-commonjs.cts' : 'test/types/iframe.ts' : 'refactor/fixtures/consumers/iframe-workspace.ts'), 'utf8') + const files = [filename] + if (pkg.label === 'published') + source = source.replaceAll('artplayer-tool-iframe', pkg.name).replace('Record any, reject: (...args: any[]) => any }>', 'Record') + if (pkg.label === 'candidate' && !noInterop) { + const oldFile = path.join(consumer, path.basename(filename).replace('consumer', 'historical')) + fs.copyFileSync(path.join(workspace, 'refactor/fixtures/consumers/iframe-workspace.ts'), oldFile) + files.push(oldFile) + } + function compile(code) { + fs.writeFileSync(filename, code) + const program = compiler.createProgram(files, options) + for (const file of program.getSourceFiles()) { + const real = fs.realpathSync(file.fileName) + assert(real.startsWith(fs.realpathSync(consumer) + path.sep) || (program.isSourceFileDefaultLibrary(file) && path.dirname(real) === fs.realpathSync(path.dirname(compiler.sys.getExecutingFilePath()))), `Type escaped installed consumer: ${real}`) + } + return compiler.getPreEmitDiagnostics(program).map(item => ({ code: item.code, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n').replaceAll(consumer.replaceAll('\\', '/'), '') })) + } + const diagnostics = compile(source) + const expected = pkg.label === 'workspace' && mode === 'nodenext-esm' ? [2344, 2344, 2344, 2709, 2339, 2344, 2709, 2344, 2709, 2709, 2351, 2339, 2339, 2339, 7006, 7006, 2339, 2339] : [] + writeJson(path.join(output, `${pkg.label}-${compiler.version}-${mode}.json`), diagnostics) + assert.deepEqual(diagnostics.map(item => item.code), expected, `${pkg.label} ${compiler.version} ${mode}`) + const negative = pkg.label === 'candidate' ? compile(source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')) : [] + if (pkg.label === 'candidate') + assert.equal(negative.length, noInterop ? 1 : 16) + matrix.push({ package: pkg.label, compiler: compiler.version, mode, diagnostics, negative }) + } + } + finally { removeConsumer(consumer) } + } + writeJson(path.join(output, 'report.json'), { task: 'PKG-IFRAME-04', scope: 'Actual npm archive, frozen Git workspace pack and candidate Yarn tarball installed outside workspace; byte verification, offline/frozen lock and strict positive/negative consumers. Old npm CJS namespace and distinct helper are preserved observations, not claims the candidate provides old package-name/helper aliases. Native player and distribution migration remain 05/06.', packages, matrix, runtime }) + console.log(`Iframe installed types verified: ${matrix.length} compiler cases; ${output}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/refactor/scripts/iframe-types.test.mjs b/refactor/scripts/iframe-types.test.mjs new file mode 100644 index 000000000..361a1fd74 --- /dev/null +++ b/refactor/scripts/iframe-types.test.mjs @@ -0,0 +1,105 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import path from 'node:path' +// eslint-disable-next-line test/no-import-node-test -- Compiler and historical declaration contract runner. +import test from 'node:test' +import { ESLint } from 'eslint' +import ts from 'typescript' +import compat from 'typescript-compat' +import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from '../../scripts/plugin-editor-types.mjs' +import { checkConsumer } from '../../scripts/typecheck.mjs' +import { iframeCandidate, iframeEnvironment, iframeHistorical } from '../../test/helpers/iframe.js' +import { verifyIframeContract } from './iframe-contract.mjs' +import { readMember } from './releases.mjs' + +const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']] +const invalid = code => code.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '') + +test('Iframe error envelopes preserve non-string thrown messages rather than claiming string-only payloads', async () => { + const implementations = [...(await iframeHistorical()).filter(item => !item.name.includes('helper')), await iframeCandidate()] + for (const implementation of implementations) { + for (const [body, expected] of [['throw { message: 7 }', 7], ['throw 3', undefined]]) { + const env = iframeEnvironment(implementation, true) + await assert.rejects(env.Factory.onMessage({ data: { type: 'commit', data: body, id: 1 }, source: env.box.parent }), () => true) + assert.equal(env.sent.length, 1) + assert.equal(env.sent[0].packet.type, 'error') + assert.equal(env.sent[0].packet.data, expected) + assert.equal(env.sent[0].packet.id, 1) + } + } +}) + +test('Iframe public types preserve legacy extraction while precise views reject invalid protocol calls', () => { + const source = fs.readFileSync('test/types/iframe.ts', 'utf8') + for (const [compiler, mode] of modes) { + assert.deepEqual(checkConsumer(compiler, mode, source), [], `${compiler.version} ${mode}`) + assert.equal(checkConsumer(compiler, mode, invalid(source)).length, 16) + assert.deepEqual(checkConsumer(compiler, mode, fs.readFileSync('refactor/fixtures/consumers/iframe-workspace.ts', 'utf8')), []) + } + const commonjs = fs.readFileSync('test/types/iframe-commonjs.cts', 'utf8') + assert.deepEqual(checkConsumer(ts, 'nodenext-cjs', commonjs), []) + assert.equal(checkConsumer(ts, 'nodenext-cjs', invalid(commonjs)).length, 1) + const implementationSource = fs.readFileSync('test/types/iframe-source.ts', 'utf8') + assert.deepEqual(checkConsumer(ts, 'node10-commonjs', implementationSource), []) + const implementation = checkConsumer(ts, 'node10-commonjs', invalid(implementationSource)) + assert.deepEqual(implementation.map(item => item.code), [2419], 'Only the preserved non-null legacy callback differs from the actual constructor') + assert(implementation[0].message.includes('messageCallback') && implementation[0].message.includes('null')) +}) + +test('Iframe frozen workspace keeps exact default signatures and published Function callbacks remain a separate legacy boundary', async () => { + const contract = await verifyIframeContract() + const source = fs.readFileSync('refactor/fixtures/consumers/iframe-workspace.ts', 'utf8') + const filename = path.resolve('test/types/iframe-history.ts') + const declaration = path.resolve('test/types/iframe-history.d.ts') + for (const compiler of [ts, compat]) { + function historical(code, definitions) { + const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: true, module: compiler.ModuleKind.CommonJS, moduleResolution: compiler.ModuleResolutionKind.NodeJs, target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'] } + const host = compiler.createCompilerHost(options) + const original = host.getSourceFile.bind(host) + host.getSourceFile = (file, language, ...rest) => path.resolve(file) === filename + ? compiler.createSourceFile(file, code.replaceAll('artplayer-tool-iframe', './iframe-history'), language, true) + : path.resolve(file) === declaration ? compiler.createSourceFile(file, definitions, language, true) : original(file, language, ...rest) + const exists = host.fileExists.bind(host) + host.fileExists = file => path.resolve(file) === declaration || exists(file) + const program = compiler.createProgram([filename, declaration], options, host) + assert(program.getSourceFile(declaration)) + return compiler.getPreEmitDiagnostics(program).map(item => item.code) + } + const frozen = contract.sources.get('packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.ts') + const published = readMember(contract.archive, 'package/types/artplayer-plugin-iframe.d.ts').toString() + assert.deepEqual(historical(source, frozen), []) + assert.deepEqual(historical(source, published), [2344], 'Published Function versus workspace callable field is a pre-refactor difference') + const broad = `import Iframe from 'artplayer-tool-iframe'; declare const tool: Iframe; declare const callback: Function; tool.promises[1] = { resove: callback, reject: callback };` + assert.deepEqual(historical(broad, published), []) + assert.deepEqual(historical(broad, frozen), [2322, 2322]) + assert.deepEqual(checkConsumer(compiler, 'node10-commonjs', broad).map(item => item.code), [2322, 2322]) + assert.deepEqual(historical(`${source}\nIframe.postMessage({ type: 'custom' });`, frozen), [2345]) + assert.deepEqual(checkConsumer(compiler, 'node10-commonjs', `${source}\nIframe.postMessage({ type: 'custom' });`).map(item => item.code), [2345]) + } +}) + +test('Iframe standalone editor generation fixes the frozen export errors and preserves class plus named types', async () => { + const contract = await verifyIframeContract() + const historical = contract.sources.get('docs/assets/ts/artplayer-tool-iframe.d.ts') + const source = fs.readFileSync('packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.ts', 'utf8') + const generated = generatePluginEditorDeclaration(source, 'ArtplayerToolIframe') + const file = 'docs/assets/ts/artplayer-tool-iframe.d.ts' + const [formatted] = await new ESLint({ fix: true, fixTypes: ['layout'] }).lintText(generated, { filePath: file }) + assert.equal(formatted.errorCount, 0) + assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), formatted.output || generated) + const consumer = `declare const frame: HTMLIFrameElement; +const option: ArtplayerToolIframe.Option = { iframe: frame, url: '/frame' }; +const tool: ArtplayerToolIframe = new ArtplayerToolIframe(option); +const runtime = ArtplayerToolIframe as ArtplayerToolIframe.RuntimeConstructor; +runtime.postMessage({ type: 'custom' }); +const promise: Promise = runtime.onMessage(new MessageEvent('message', { data: { type: 'custom' } })); +// @ts-expect-error No lowercase runtime global exists. +new artplayerToolIframe(option); +// @ts-expect-error URL remains text. +new ArtplayerToolIframe({ iframe: frame, url: 1 });` + for (const compiler of [ts, compat]) { + assert.deepEqual(checkPluginEditorDeclaration(historical, '', '', compiler).map(item => item.code).sort(), [2303, 2309, 2686]) + assert.deepEqual(checkPluginEditorDeclaration(generated, '', consumer, compiler), []) + assert.equal(checkPluginEditorDeclaration(generated, '', invalid(consumer), compiler).length, 2) + } +}) diff --git a/refactor/tasks.json b/refactor/tasks.json index eaf02ee91..485d0cddb 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -3598,7 +3598,7 @@ ], "status": "todo", "risk": "H", - "deliverable": "mediabunny.js、README、依赖版本、三产物和许可", + "deliverable": "mediabunny.js、README、依赖版本、三产物和许可; 拆分media声明后的独立编辑器生成/辅助文件过滤与语义检查(IFRAME-04全量生成时暴露)", "acceptance": "tarball 可消费、无意外依赖升级、旧调用与回退可用", "evidence": [] }, @@ -3684,11 +3684,14 @@ "ENG-06", "CORE-07" ], - "status": "todo", + "status": "done", "risk": "H", "deliverable": "消息联合类型、回调/Promise 推导、旧公开字段兼容;旧npm export=与实际namespace、额外helper协议分别核验", "acceptance": "严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容", - "evidence": [] + "evidence": [ + "changes/2026-09-13-PKG-IFRAME-04-types.md", + "baselines/iframe-types-validation.json" + ] }, { "id": "PKG-IFRAME-05", @@ -3720,7 +3723,7 @@ ], "status": "todo", "risk": "H", - "deliverable": "iframe.js、示例集成和原 script/class 导出验证;旧包名/额外helper深入口与新工具名的迁移结论", + "deliverable": "iframe.js、示例集成和原 script/class 导出验证;旧包名/额外helper深入口与新工具名的迁移结论; 保留旧npm Function回调及namespace/helper与工具类的区别,核验实际编译后消费而非仅声明通过", "acceptance": "tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录", "evidence": [] }, diff --git a/scripts/build-ts.js b/scripts/build-ts.js index 39518e2a2..ed4de81a6 100644 --- a/scripts/build-ts.js +++ b/scripts/build-ts.js @@ -1,5 +1,7 @@ +import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' +import process from 'node:process' import { ESLint } from 'eslint' import { glob } from 'glob' import compat from 'typescript-compat' @@ -18,12 +20,14 @@ function parsePluginInfo(pluginPath) { const file = path.basename(pluginPath) const baseName = file.replace('.d.ts', '') - const name = baseName - .split('-') - .map((word, index) => - index === 0 ? word : word[0].toUpperCase() + word.slice(1), - ) - .join('') + const name = baseName === 'artplayer-tool-iframe' + ? 'ArtplayerToolIframe' + : baseName + .split('-') + .map((word, index) => + index === 0 ? word : word[0].toUpperCase() + word.slice(1), + ) + .join('') return { name, file } } @@ -36,14 +40,19 @@ fs.writeFileSync(artplayerTSoutput, code.trim()) console.log(`✨ Built ${artplayerTSoutput}`); (async function () { - const pluginsTS = glob.sync('packages/artplayer-*-*/types/*.d.ts') + const available = glob.sync('packages/artplayer-*-*/types/*.d.ts') + const selected = process.argv.slice(2) + const packageOf = file => path.basename(path.dirname(path.dirname(file))) + for (const name of selected) + assert(available.some(file => packageOf(file) === name), `Unknown declaration package: ${name}`) + const pluginsTS = selected.length ? available.filter(file => selected.includes(packageOf(file))) : available const pluginFiles = [] for (let index = 0; index < pluginsTS.length; index++) { const type = pluginsTS[index] const { name, file } = parsePluginInfo(type) const source = String(fs.readFileSync(type)) - const semanticPlugin = ['artplayerPluginHlsControl', 'artplayerPluginAudioTrack', 'artplayerPluginDashControl', 'artplayerPluginAds', 'artplayerPluginAmbilight', 'artplayerProxyCanvas', 'artplayerPluginDocumentPip'].includes(name) + const semanticPlugin = ['artplayerPluginHlsControl', 'artplayerPluginAudioTrack', 'artplayerPluginDashControl', 'artplayerPluginAds', 'artplayerPluginAmbilight', 'artplayerProxyCanvas', 'artplayerPluginDocumentPip', 'ArtplayerToolIframe'].includes(name) const code = semanticPlugin ? generatePluginEditorDeclaration(source, name) : `${source.replace(reg, '')}\nexport = ${name};\nexport as namespace ${name};\n` @@ -71,6 +80,8 @@ console.log(`✨ Built ${artplayerTSoutput}`); const formatter = await eslint.loadFormatter('stylish') throw new Error(formatter.format(results)) } + if (selected.length) + return const commonJsPath = path.join('docs/assets/js/common.js') const commonJsContent = fs.readFileSync(commonJsPath, 'utf-8') const newLibUris = allFiles.map(file => `'./assets/ts/${file}'`).join(',\n ') diff --git a/scripts/plugin-editor-types.mjs b/scripts/plugin-editor-types.mjs index 940fc4ce3..1599db66f 100644 --- a/scripts/plugin-editor-types.mjs +++ b/scripts/plugin-editor-types.mjs @@ -16,6 +16,7 @@ export function generatePluginEditorDeclaration(code, name) { const internal = `${name}Definitions` let exported = false let callable = false + let classExport = false for (const node of source.statements) { if (ts.isImportDeclaration(node)) { assert(node.importClause?.isTypeOnly && !node.importClause.namedBindings && node.importClause.name?.text === 'Artplayer' && node.moduleSpecifier.text === 'artplayer', 'Unsupported plugin editor import') @@ -26,12 +27,13 @@ export function generatePluginEditorDeclaration(code, name) { exported = true continue } - assert(ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isFunctionDeclaration(node), 'Unsupported plugin editor declaration') + assert(ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node), 'Unsupported plugin editor declaration') assert(node.name && node.name.text !== internal, 'Invalid plugin declaration name') definitions.push(factory.replaceModifiers(node, [factory.createModifier(ts.SyntaxKind.ExportKeyword)])) - if (ts.isFunctionDeclaration(node)) { + if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) { assert.equal(node.name.text, name, 'Unexpected plugin callable') callable = true + classExport = ts.isClassDeclaration(node) continue } if (node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) { @@ -43,13 +45,14 @@ export function generatePluginEditorDeclaration(code, name) { )) } } - assert(exported && callable, 'Plugin editor requires a default function') + assert(exported && callable, 'Plugin editor requires a default function or class') const namespace = (identifier, statements) => factory.createModuleDeclaration([factory.createModifier(ts.SyntaxKind.DeclareKeyword)], factory.createIdentifier(identifier), factory.createModuleBlock(statements), ts.NodeFlags.Namespace) const statements = [ namespace(internal, definitions), factory.createVariableStatement([factory.createModifier(ts.SyntaxKind.DeclareKeyword)], factory.createVariableDeclarationList([ factory.createVariableDeclaration(name, undefined, factory.createTypeQueryNode(factory.createQualifiedName(factory.createIdentifier(internal), factory.createIdentifier(name)))), ], ts.NodeFlags.Const)), + ...(classExport ? [factory.createTypeAliasDeclaration(undefined, name, undefined, factory.createTypeReferenceNode(factory.createQualifiedName(factory.createIdentifier(internal), factory.createIdentifier(name))))] : []), namespace(name, aliases), factory.createExportAssignment(undefined, true, factory.createIdentifier(name)), factory.createNamespaceExportDeclaration(factory.createIdentifier(name)), diff --git a/scripts/typecheck.mjs b/scripts/typecheck.mjs index 3767c6c25..8879bdce6 100644 --- a/scripts/typecheck.mjs +++ b/scripts/typecheck.mjs @@ -80,6 +80,10 @@ export function runTypechecks() { assert.deepEqual(checkConsumer(compiler, mode, fs.readFileSync(path.join(root, fixture), 'utf8')), [], `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']) + assert.deepEqual(checkConsumer(compiler, mode, fs.readFileSync(path.join(root, fixture), 'utf8')), [], `Iframe consumer failed: TS ${compiler.version} ${mode}`) + if (mode === 'nodenext-cjs') + assert.deepEqual(checkConsumer(compiler, mode, fs.readFileSync(path.join(root, 'test/types/iframe-commonjs.cts'), 'utf8')), [], 'Iframe CommonJS constructor consumer failed') console.log(`Consumers passed: TS ${compiler.version} ${mode}`) } const runtimeFixtures = ['test/types/runtime-leaf-consumer.ts', 'test/types/runtime-public.ts', 'test/types/runtime-construction.ts'] diff --git a/test/types/iframe-commonjs.cts b/test/types/iframe-commonjs.cts new file mode 100644 index 000000000..5afb46ee9 --- /dev/null +++ b/test/types/iframe-commonjs.cts @@ -0,0 +1,16 @@ +/* eslint-disable ts/no-require-imports -- Verify native TypeScript CommonJS constructor imports. */ +import Iframe = require('artplayer-tool-iframe') +import Legacy = require('artplayer-tool-iframe/legacy') + +declare const frame: HTMLIFrameElement +const option: Iframe.Option = { iframe: frame, url: '/frame' } +const tool: Iframe = new Iframe(option) +const old: Legacy = new Legacy(option) +const message: Iframe.Message = { type: 'custom', data: 2 } +tool.postMessage(message) +old.commit(() => { + return 2 +}) +// @ts-expect-error Direct CommonJS constructor has no default namespace. +const wrong = Iframe.default +void wrong diff --git a/test/types/iframe-source.ts b/test/types/iframe-source.ts new file mode 100644 index 000000000..3b50e6bad --- /dev/null +++ b/test/types/iframe-source.ts @@ -0,0 +1,8 @@ +import type { RuntimeConstructor } from 'artplayer-tool-iframe' +import type Public from 'artplayer-tool-iframe' +import Implementation from '../../packages/artplayer-tool-iframe/src/index' + +// @ts-expect-error Historical callback is non-null; actual runtime permits null. Keep this legacy mismatch explicit. +const oldView: typeof Public = Implementation +const actualView: RuntimeConstructor = Implementation +void [oldView, actualView] diff --git a/test/types/iframe.ts b/test/types/iframe.ts new file mode 100644 index 000000000..faa4bfc44 --- /dev/null +++ b/test/types/iframe.ts @@ -0,0 +1,77 @@ +import type { Callbacks, Message, MessageCallback, Notification, Option, OutboundMessage, ProtocolMessage, Resolve, ResolverInstance, RuntimeConstructor } from 'artplayer-tool-iframe' +import Iframe from 'artplayer-tool-iframe' +import Legacy from 'artplayer-tool-iframe/legacy' + +declare const frame: HTMLIFrameElement +const option: Option = { iframe: frame, url: '/iframe.html' } +const tool = new Iframe(option) +const old = new Legacy(option) +const Runtime = Iframe as RuntimeConstructor +const runtime = Runtime +const precise = new Runtime(option) +runtime.postMessage({ type: 'custom' }) +const asyncReceiver: Promise = runtime.onMessage(new MessageEvent('message', { data: { type: 'custom' } })) +const unknownReply: Promise = precise.postMessage({ type: 'custom' }) +const numeric: Promise = precise.postMessage({ type: 'query', data: 2 }) +const callback: MessageCallback = function (message) { + const source: HTMLIFrameElement = this.$iframe + const body: unknown = message.data + void [source, body] +} +precise.message(callback) +precise.messageCallback = null +const resolver = tool as ResolverInstance +const resolved: Promise = resolver.commit((resolve) => { + resolve(3) +}) +const resolve: Resolve = value => void value +resolve(Promise.resolve(1)) +const notification: Notification = { type: 'custom', data: 3 } +const packet: Message = { type: 'custom', data: 3 } +const outgoing: OutboundMessage = { type: 'arbitrary' } +const callbacks: Callbacks = { resove() {}, reject() {} } +function receive(packet: ProtocolMessage) { + if (packet.type === 'response') { + const value: number = packet.data + return value + } + if (packet.type === 'error' && typeof packet.data === 'string') + return packet.data.toUpperCase() + return packet.id +} +receive({ type: 'inject' }) +receive({ type: 'commit', data: 'return 1', id: 1 }) +// @ts-expect-error Constructor options are still required. +void new Iframe() +// @ts-expect-error A string selector is not an iframe element. +void new Iframe({ iframe: '#frame', url: '/frame' }) +// @ts-expect-error URL is text. +void new Iframe({ iframe: frame, url: 1 }) +// @ts-expect-error Default data remains required for old extracted types. +tool.postMessage({ type: 'custom' }) +// @ts-expect-error Message types are strings. +precise.postMessage({ type: 1 }) +// @ts-expect-error Public request ids remain numeric. +runtime.postMessage({ type: 'custom', id: '1' }) +// @ts-expect-error Public flags remain readonly in the default API. +tool.injected = true +// @ts-expect-error The legacy static receiver return is still void. +const notAsync: Promise = Iframe.onMessage(new MessageEvent('message', { data: packet })) +// @ts-expect-error No self-default property exists on the real constructor. +Iframe.default(option) +// @ts-expect-error Runtime iframe is a readonly getter. +runtime.iframe = true +// @ts-expect-error Public notifications do not contain a request id. +const wrongNotification: Notification = { type: 'custom', data: 1, id: 2 } +receive({ type: 'error', data: 1, id: 1 }) +// @ts-expect-error Error packets still require a numeric request id. +receive({ type: 'error', data: 1, id: '1' }) +// @ts-expect-error Known response data follows its protocol parameter. +receive({ type: 'response', data: '1', id: 1 }) +// @ts-expect-error Typed resolver rejects incompatible values. +resolver.commit(resolve => resolve('3')) +// @ts-expect-error Historical misspelling is preserved rather than silently renamed. +callbacks.resolve(1) +// @ts-expect-error Runtime notifications have unknown data until the application narrows it. +precise.message(message => message.data.toFixed()) +void [old, asyncReceiver, unknownReply, numeric, resolved, notification, outgoing, notAsync, wrongNotification]