From 76499e7a3e033c8350ff495ba8bfff4ca5c08d25 Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Mon, 14 Sep 2026 07:24:03 +0800 Subject: [PATCH] refactor(docs): [SITE-02] generate and validate standalone editor types --- docs/assets/ts/artplayer-plugin-chapter.d.ts | 38 +- .../ts/artplayer-plugin-vast.LICENSE.txt | 224 +++ docs/assets/ts/artplayer-plugin-vast.d.ts | 1417 ++++++++++++++++- package.json | 9 +- packages/artplayer-vitepress/README.md | 3 + .../editor-declarations-validation.json | 444 ++++++ refactor/baselines/site-inventory.json | 34 +- .../2026-09-14-SITE-02-editor-declarations.md | 63 + refactor/ci-setup.md | 3 +- refactor/plan.md | 5 +- refactor/progress.md | 15 + refactor/scripts/site-inventory.mjs | 2 +- refactor/site-inventory.md | 5 +- refactor/tasks.json | 8 +- scripts/build-ts.js | 97 +- scripts/editor-declarations/README.md | 70 + scripts/editor-declarations/core.ts | 145 ++ scripts/editor-declarations/dependencies.ts | 23 + scripts/editor-declarations/generate.ts | 90 ++ scripts/editor-declarations/plugin.ts | 185 +++ scripts/editor-declarations/syntax.ts | 18 + scripts/editor-declarations/validation.ts | 24 + scripts/editor-declarations/vast-sdk.ts | 2 + scripts/editor-types.mjs | 145 +- scripts/plugin-editor-types.mjs | 162 +- scripts/tsconfig.docs.json | 19 +- test/README.md | 5 + test/browser/README.md | 6 + test/browser/editor-declarations.spec.js | 82 + test/editor-types.test.js | 71 + 30 files changed, 2955 insertions(+), 459 deletions(-) create mode 100644 docs/assets/ts/artplayer-plugin-vast.LICENSE.txt create mode 100644 refactor/baselines/editor-declarations-validation.json create mode 100644 refactor/changes/2026-09-14-SITE-02-editor-declarations.md create mode 100644 scripts/editor-declarations/README.md create mode 100644 scripts/editor-declarations/core.ts create mode 100644 scripts/editor-declarations/dependencies.ts create mode 100644 scripts/editor-declarations/generate.ts create mode 100644 scripts/editor-declarations/plugin.ts create mode 100644 scripts/editor-declarations/syntax.ts create mode 100644 scripts/editor-declarations/validation.ts create mode 100644 scripts/editor-declarations/vast-sdk.ts create mode 100644 test/browser/editor-declarations.spec.js diff --git a/docs/assets/ts/artplayer-plugin-chapter.d.ts b/docs/assets/ts/artplayer-plugin-chapter.d.ts index 24d82d985..ed0adfa8b 100644 --- a/docs/assets/ts/artplayer-plugin-chapter.d.ts +++ b/docs/assets/ts/artplayer-plugin-chapter.d.ts @@ -1,21 +1,25 @@ -export type Chapters = { - start: number - end: number - title: string -}[] - -export interface Option { - chapters?: Chapters +// 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 artplayerPluginChapterDefinitions { + export type Chapters = { + start: number + end: number + title: string + }[] + export interface Option { + chapters?: Chapters + } + export interface Result { + name: 'artplayerPluginChapter' + update: (option: Option) => void + } + export const artplayerPluginChapter: (option?: Option) => (art: Artplayer) => Result } - -export interface Result { - name: 'artplayerPluginChapter' - update: (option: Option) => void +declare const artplayerPluginChapter: typeof artplayerPluginChapterDefinitions.artplayerPluginChapter +declare namespace artplayerPluginChapter { + export type Chapters = artplayerPluginChapterDefinitions.Chapters + export type Option = artplayerPluginChapterDefinitions.Option + export type Result = artplayerPluginChapterDefinitions.Result } - -declare const artplayerPluginChapter: (option?: Option) => (art: Artplayer) => Result - -export default artplayerPluginChapter - export = artplayerPluginChapter export as namespace artplayerPluginChapter; diff --git a/docs/assets/ts/artplayer-plugin-vast.LICENSE.txt b/docs/assets/ts/artplayer-plugin-vast.LICENSE.txt new file mode 100644 index 000000000..18bf87457 --- /dev/null +++ b/docs/assets/ts/artplayer-plugin-vast.LICENSE.txt @@ -0,0 +1,224 @@ +@glomex/vast-ima-player +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 glomex GmbH + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +@alugha/ima +# The MIT License (MIT) + +**Copyright 2020 Alugha GmbH** + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/assets/ts/artplayer-plugin-vast.d.ts b/docs/assets/ts/artplayer-plugin-vast.d.ts index a2a5fc121..7ad0924bd 100644 --- a/docs/assets/ts/artplayer-plugin-vast.d.ts +++ b/docs/assets/ts/artplayer-plugin-vast.d.ts @@ -1,36 +1,1399 @@ +// 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. */ +/* eslint-disable ts/method-signature-style, ts/consistent-type-definitions -- Preserve upstream SDK declaration shapes. */ +declare namespace artplayerPluginVastDefinitions { + export namespace google { + /** + * The Google IMA SDK for HTML5 V3 allows developers to request and track VAST ads in a HTML5 video environment. For platform compatibility information and a detailed list of the video ad features supported by each of the IMA SDKs, see Support and Compatibility. + * + * Download the code samples to assist with implementing the IMA HTML5 SDK. + */ + namespace ima { + /** + * An ad class that's extended by classes representing different ad types. + */ + interface Ad { + /** + * Ad ID is used to synchronize master ad and companion ads. + * @returns The ID of the ad, or the empty string if this information is unavailable. + */ + getAdId(): string + /** + * Returns the ad's pod information. + * @returns The ad's pod information. + */ + getAdPodInfo(): AdPodInfo + /** + * The source ad server information included in the ad response. + * @returns The source ad server of the ad, or the empty string if this information is unavailable. + */ + getAdSystem(): string + /** + * The advertiser name as defined by the serving party. + * @returns The advertiser name, or the empty string if this information is unavailable. + */ + getAdvertiserName(): string + /** + * Identifies the API needed to execute the ad. This corresponds with the apiFramework specified in vast. + * @returns The API framework need to execute the ad, or null if this information is unavailable. + */ + getApiFramework(): string | null + /** + * Gets the companion ads for this ad based on companion ad slot size. Optionally, advanced selection settings are accepted. Note that this method will only return non-empty array for ad instances acquired on or after STARTED event. Specifically, ads from the LOADED event will return an empty array. + * @param adSlotWidth Width of the companion ad slot. + * @param adSlotHeight Height of the companion ad slot. + * @param settings The selection settings for companion ads. + * @returns Array of companion ads that matches the settings and the slot size. + */ + getCompanionAds(adSlotWidth: number, adSlotHeight: number, settings?: CompanionAdSelectionSettings): CompanionAd[] + /** + * Returns the content type of the currently selected creative, or empty string if no creative is selected or the content type is unavailable. For linear ads, the content type is only going to be available after the START event, when the media file is selected. + * @returns The content type, empty string if not available. + */ + getContentType(): string + /** + * Returns the ISCI (Industry Standard Commercial Identifier) code for an ad, or empty string if the code is unavailable. This is the Ad-ID of the creative in the VAST response. + */ + getCreativeAdId(): string + /** + * Retrieves the ID of the selected creative for the ad. + * @returns The ID of the selected creative for the ad, or the empty string if this information is unavailable. + */ + getCreativeId(): string + /** + * Returns the first deal ID present in the wrapper chain for the current ad, starting from the top. Returns the empty string if unavailable. + */ + getDealId(): string + /** + * Returns the description of this ad from the VAST response. + * @returns The description, empty if not specified. + */ + getDescription(): string + /** + * Returns the duration of the selected creative, or -1 for non-linear creatives. + * @returns The selected creative duration in seconds, -1 if non-linear. + */ + getDuration(): number + /** + * Returns the height of the selected non-linear creative. + * @returns The height of the selected non-linear creative or 0 for a linear creative. + */ + getHeight(): number + /** + * Returns the URL of the media file chosen from the ad based on the media selection settings currently in use. Returns null if this information is unavailable. Available on STARTED event. + */ + getMediaUrl(): string | null + /** + * Returns the minimum suggested duration in seconds that the nonlinear creative should be displayed. Returns -2 if the minimum suggested duration is unknown. For linear creative it returns the entire duration of the ad. + * @returns The minimum suggested duration in seconds that a creative should be displayed. + */ + getMinSuggestedDuration(): number + /** + * The number of seconds of playback before the ad becomes skippable. -1 is returned for non skippable ads or if this is unavailable. + * @returns The offset in seconds, or -1. + */ + getSkipTimeOffset(): number + /** + * Returns the URL associated with the survey for the given ad. Returns null if unavailable. + */ + getSurveyUrl(): string | null + /** + * Returns the title of this ad from the VAST response. + * @returns The title, empty if not specified. + */ + getTitle(): string + /** + * Gets custom parameters associated with the ad at the time of ad trafficking. + * @returns A mapping of trafficking keys to their values, or the empty Object if this information is not available. + */ + getTraffickingParameters(): any + /** + * Gets custom parameters associated with the ad at the time of ad trafficking. Returns a raw string version of the parsed parameters from getTraffickingParameters. + * @returns Trafficking parameters, or the empty string if this information is not available. + */ + getTraffickingParametersString(): string + /** + * Returns the UI elements that are being displayed when this ad is played. Refer to UiElements for possible elements of the array returned. + * @returns The UI elements being displayed. + */ + getUiElements(): UiElements[] + /** + * The registry associated with cataloging the UniversalAdId of the selected creative for the ad. + * @returns Returns the registry value, or "unknown" if unavailable. + */ + getUniversalAdIdRegistry(): string + /** + * The UniversalAdId of the selected creative for the ad. + * @returns Returns the id value or "unknown" if unavailable. + */ + getUniversalAdIdValue(): string + /** + * Returns the VAST media height of the selected creative. + * @returns The VAST media height of the selected creative or 0 if none is selected. + */ + getVastMediaHeight(): number + /** + * Returns the VAST media width of the selected creative. + * @returns The VAST media width of the selected creative or 0 if none is selected. + */ + getVastMediaWidth(): number + /** + * Returns the width of the selected creative. + * @returns The width of the selected non-linear creative or 0 for a linear creative. + */ + getWidth(): number + /** + * Ad IDs used for wrapper ads. The IDs returned starts at the inline ad (innermost) and traverses to the outermost wrapper ad. An empty array is returned if there are no wrapper ads. + * @returns The IDs of the ads, starting at the inline ad, or an empty array if there are no wrapper ads. + */ + getWrapperAdIds(): string[] + /** + * Ad systems used for wrapper ads. The ad systems returned starts at the inline ad and traverses to the outermost wrapper ad. An empty array is returned if there are no wrapper ads. + * @returns The ad systems of the ads, starting at the inline ad, or an empty array if there are no wrapper ads. + */ + getWrapperAdSystems(): string[] + /** + * Selected creative IDs used for wrapper ads. The creative IDs returned starts at the inline ad and traverses to the outermost wrapper ad. An empty array is returned if there are no wrapper ads. + * @returns The IDs of the ads' creatives, starting at the inline ad, or an empty array if there are no wrapper ads. + */ + getWrapperCreativeIds(): string[] + /** + * Indicates whether the ad’s current mode of operation is linear or non-linear. If the value is true, it indicates that the ad is in linear playback mode; if false, it indicates non-linear mode. The player checks the linear property and updates its state according to the details of the ad placement. While the ad is in linear mode, the player pauses the content video. If linear is true initially, and the ad is a pre-roll (defined externally), the player may choose to delay loading the content video until near the end of the ad playback. + * @returns True if the ad is linear, false otherwise. + */ + isLinear(): boolean + } + /** + * This class represents a container for displaying ads. The SDK will automatically create structures inside the containerElement parameter to house video and overlay ads. + * + * When an instance of this class is created, it creates an IFRAME in the containerElement and loads the SDK core. This IFRAME must be preserved in order for the SDK to function properly. Once all ads have been played and the SDK is no longer needed, use the destroy() method to unload the SDK. + * + * The containerElement parameter must be an element that is part of the DOM. It is necessary to correctly position the containerElement in order for the ads to be displayed correctly. It is recommended to position it above the content video player and size it to cover the whole video player. Please refer to the SDK documentation for details about recommended implementations. + */ + class AdDisplayContainer { + /** + * + * @param containerElement The element to display the ads in. The element must be inserted into the DOM before creating ima.AdDisplayContainer. + * @param videoElement Specifies the alternative video ad playback element. We recommend always passing in your content video player. Refer to Custom Ad Playback for more information. + * @param clickTrackingElement Specifies the alternative video ad click element. Leave this null to let the SDK handle clicks. Even if supplied, the SDK will only use the custom click tracking element when non-AdSense/AdX creatives are displayed in environments that do not support UI elements overlaying a video player (e.g. iPhone or pre-4.0 Android). The custom click tracking element should never be rendered over the video player because it can intercept clicks to UI elements that the SDK renders. Also note that the SDK will not modify the visibility of the custom click tracking element. This means that if a custom click tracking element is supplied, it must be properly displayed when the linear ad is played. You can check ima.AdsManager.isCustomClickTrackingUsed when the google.ima.AdEvent.Type.STARTED event is fired to determine whether or not to display your custom click tracking element. If appropriate for your UI, you should hide the click tracking element when the google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED event fires. + */ + constructor(containerElement: HTMLElement, videoElement?: HTMLVideoElement, clickTrackingElement?: HTMLElement) + /** + * Destroys internal state and previously created DOM elements. The IMA SDK will be unloaded and no further calls to any APIs should be made. + */ + public destroy(): void + /** + * Initializes the video playback. On mobile platforms, including iOS and Android browsers, first interaction with video playback is only allowed within a user action (a click or tap) to prevent unexpected bandwidth costs. Call this method as a direct result of a user action before starting the ad playback. This method has no effect on desktop platforms and when custom video playback is used. + */ + public initialize(): void + } + /** + * AdError surfaces information to the user about whether a failure occurred during ad loading or playing. The errorType accessor provides information about whether the error occurred during ad loading or ad playing. + */ + class AdError extends Error { + /** + * Constructs the ad error based on the error data. + * @param data The ad error message data. + * @returns The constructed ad error object. + */ + public static deserialize(data: any): AdError + /** + * @returns The error code, as defined in google.ima.AdError.ErrorCode. + */ + public getErrorCode(): AdError.ErrorCode + /** + * Returns the Error that caused this one. + * @returns Inner error that occurred during processing, or null if this information is unavailable. This error may either be a native error or an google.ima.AdError, a subclass of a native error. This may return null if the error that caused this one is not available. + */ + public getInnerError(): Error | null + /** + * @returns The message for this error. + */ + public getMessage(): string + /** + * @returns The type of this error, as defined in google.ima.AdError.Type. + */ + public getType(): string + /** + * @returns If VAST error code is available, returns it, otherwise returns ima.AdError.ErrorCode.UNKNOWN_ERROR. + */ + public getVastErrorCode(): number + /** + * Serializes an ad to JSON-friendly object for channel transmission. + * @returns The transmittable ad error. + */ + public serialize(): any + public toString(): string + } + namespace AdError { + /** + * The possible error codes raised while loading or playing ads. + */ + enum ErrorCode { + /** + * There was a problem requesting ads from the server. VAST error code 1012 + */ + ADS_REQUEST_NETWORK_ERROR = 1012, + /** + * There was an error with asset fallback. VAST error code 1021 + */ + ASSET_FALLBACK_FAILED = 1021, + /** + * The browser prevented playback initiated without user interaction. VAST error code 1205 + */ + AUTOPLAY_DISALLOWED = 1205, + /** + * A companion ad failed to load or render. VAST error code 603 + */ + COMPANION_AD_LOADING_FAILED = 603, + /** + * Unable to display one or more required companions. The master ad is discarded since the required companions could not be displayed. VAST error code 602 + */ + COMPANION_REQUIRED_ERROR = 602, + /** + * There was a problem requesting ads from the server. VAST error code 1005 + */ + FAILED_TO_REQUEST_ADS = 1005, + /** + * The ad tag url specified was invalid. It needs to be properly encoded. VAST error code 1013 + */ + INVALID_AD_TAG = 1013, + /** + * An invalid AdX extension was found. VAST error code 1105 + */ + INVALID_ADX_EXTENSION = 1105, + /** + * Invalid arguments were provided to SDK methods. VAST error code 1101 + */ + INVALID_ARGUMENTS = 1101, + /** + * Unable to display NonLinear ad because creative dimensions do not align with creative display area (i.e. creative dimension too large). VAST error code 501 + */ + NONLINEAR_DIMENSIONS_ERROR = 501, + /** + * An overlay ad failed to load. VAST error code 502 + */ + OVERLAY_AD_LOADING_FAILED = 502, + /** + * An overlay ad failed to render. VAST error code 500 + */ + OVERLAY_AD_PLAYING_FAILED = 500, + /** + * There was an error with stream initialization during server side ad insertion. VAST error code 1020 + */ + STREAM_INITIALIZATION_FAILED = 1020, + /** + * The ad response was not understood and cannot be parsed. VAST error code 1010 + */ + UNKNOWN_AD_RESPONSE = 1010, + /** + * An unexpected error occurred and the cause is not known. Refer to the inner error for more information. VAST error code 900 + */ + UNKNOWN_ERROR = 900, + /** + * Locale specified for the SDK is not supported. VAST error code 1011 + */ + UNSUPPORTED_LOCALE = 1011, + /** + * No assets were found in the VAST ad response. VAST error code 1007 + */ + VAST_ASSET_NOT_FOUND = 1007, + /** + * Empty VAST response. VAST error code 1009 + */ + VAST_EMPTY_RESPONSE = 1009, + /** + * Assets were found in the VAST ad response for linear ad, but none of them matched the video player's capabilities. VAST error code 403 + */ + VAST_LINEAR_ASSET_MISMATCH = 403, + /** + * The VAST URI provided, or a VAST URI provided in a subsequent wrapper element, was either unavailable or reached a timeout, as defined by the video player. The timeout is 5 seconds for initial VAST requests and each subsequent wrapper. VAST error code 301 + */ + VAST_LOAD_TIMEOUT = 301, + /** + * The ad response was not recognized as a valid VAST ad. VAST error code 100 + */ + VAST_MALFORMED_RESPONSE = 100, + /** + * Failed to load media assets from a VAST response. The default timeout for media loading is 8 seconds. VAST error code 402 + */ + VAST_MEDIA_LOAD_TIMEOUT = 402, + /** + * No Ads VAST response after one or more wrappers. VAST error code 303 + */ + VAST_NO_ADS_AFTER_WRAPPER = 303, + /** + * Assets were found in the VAST ad response for nonlinear ad, but none of them matched the video player's capabilities. VAST error code 503 + */ + VAST_NONLINEAR_ASSET_MISMATCH = 503, + /** + * Problem displaying MediaFile. Currently used if video playback is stopped due to poor playback quality. VAST error code 405 + */ + VAST_PROBLEM_DISPLAYING_MEDIA_FILE = 405, + /** + * VAST schema validation error. VAST error code 101 + */ + VAST_SCHEMA_VALIDATION_ERROR = 101, + /** + * The maximum number of VAST wrapper redirects has been reached. VAST error code 302 + */ + VAST_TOO_MANY_REDIRECTS = 302, + /** + * Trafficking error. Video player received an ad type that it was not expecting and/or cannot display. VAST error code 200 + */ + VAST_TRAFFICKING_ERROR = 200, + /** + * VAST duration is different from the actual media file duration. VAST error code 202 + */ + VAST_UNEXPECTED_DURATION_ERROR = 202, + /** + * Ad linearity is different from what the video player is expecting. VAST error code 201 + */ + VAST_UNEXPECTED_LINEARITY = 201, + /** + * The ad response contained an unsupported VAST version. VAST error code 102 + */ + VAST_UNSUPPORTED_VERSION = 102, + /** + * General VAST wrapper error. VAST error code 300 + */ + VAST_WRAPPER_ERROR = 300, + /** + * There was an error playing the video ad. VAST error code 400 + */ + VIDEO_PLAY_ERROR = 400, + /** + * A VPAID error occurred. Refer to the inner error for more information. VAST error code 901 + */ + VPAID_ERROR = 901, + } + /** + * The possible error types for ad loading and playing. + */ + enum Type { + /** + * Indicates that the error was encountered when the ad was being loaded. Possible causes: there was no response from the ad server, malformed ad response was returned, or ad request parameters failed to pass validation. + */ + AD_LOAD = 'adLoadError', + /** + * Indicates that the error was encountered after the ad loaded, during ad play. Possible causes: ad assets could not be loaded, etc. + */ + AD_PLAY = 'adPlayError', + } + } + /** + * This event is raised when an error occurs when loading an ad from the Google or DoubleClick servers. The types on which you can register for the event are AdsLoader and AdsManager. + */ + class AdErrorEvent { + /** + * @returns The AdError that caused this event. + */ + public getError(): AdError + /** + * During ads load request it is possible to provide an object that is available once the ads load is complete or fails. One possible use case: relate ads response to a specific request and use user request content object as the key for identifying the response. If an error occurred during ads load, you can find out which request caused this failure. + * @returns Object that was provided during ads request. + */ + public getUserRequestContext(): any + } + namespace AdErrorEvent { + /** + * Types of AdErrorEvents + */ + enum Type { + /** + * Fired when an error occurred while the ad was loading or playing. + */ + AD_ERROR = 'adError', + } + type Listener = (event: AdErrorEvent) => void + } + /** + * This event type is raised by the ad as a notification when the ad state changes and when users interact with the ad. For example, when the ad starts playing, is clicked on, etc. You can register for the various state changed events on AdsManager. + */ + class AdEvent { + /** + * Get the current ad that is playing or just played. + * @returns The ad associated with the event, or null if there is no relevant ad. + */ + public getAd(): Ad | null + /** + * Allows extra data to be passed from the ad. + * @returns Extra data for the event. Log events raised for error carry object of type 'google.ima.AdError' which can be accessed using 'adError' key. + */ + public getAdData(): any + } + namespace AdEvent { + /** + * Types of AdEvents + */ + enum Type { + /** + * Fired when an ad rule or a VMAP ad break would have played if autoPlayAdBreaks is false. + */ + AD_BREAK_READY = 'adBreakReady', + /** + * Fired when the ad has stalled playback to buffer. + */ + AD_BUFFERING = 'adBuffering', + /** + * Fired when an ads list is loaded. + */ + AD_METADATA = 'adMetadata', + /** + * Fired when the ad's current time value changes. Calling getAdData() on this event will return an AdProgressData object. + */ + AD_PROGRESS = 'adProgress', + /** + * Fired when the ads manager is done playing all the ads. + */ + ALL_ADS_COMPLETED = 'allAdsCompleted', + /** + * Fired when the ad is clicked. + */ + CLICK = 'click', + /** + * Fired when the ad completes playing. + */ + COMPLETE = 'complete', + /** + * Fired when content should be paused. This usually happens right before an ad is about to cover the content. + */ + CONTENT_PAUSE_REQUESTED = 'contentPauseRequested', + /** + * Fired when content should be resumed. This usually happens when an ad finishes or collapses. + */ + CONTENT_RESUME_REQUESTED = 'contentResumeRequested', + /** + * Fired when the ad's duration changes. + */ + DURATION_CHANGE = 'durationChange', + /** + * Fired when the ad playhead crosses first quartile. + */ + FIRST_QUARTILE = 'firstQuartile', + /** + * Fired when the impression URL has been pinged. + */ + IMPRESSION = 'impression', + /** + * Fired when an ad triggers the interaction callback. Ad interactions contain an interaction ID string in the ad data. + */ + INTERACTION = 'interaction', + /** + * Fired when the displayed ad changes from linear to nonlinear, or vice versa. + */ + LINEAR_CHANGED = 'linearChanged', + /** + * Fired when ad data is available. + */ + LOADED = 'loaded', + /** + * Fired when a non-fatal error is encountered. The user need not take any action since the SDK will continue with the same or next ad playback depending on the error situation. + */ + LOG = 'log', + /** + * Fired when the ad playhead crosses midpoint. + */ + MIDPOINT = 'midpoint', + /** + * Fired when the ad is paused. + */ + PAUSED = 'pause', + /** + * Fired when the ad is resumed. + */ + RESUMED = 'resume', + /** + * Fired when the displayed ads skippable state is changed. + */ + SKIPPABLE_STATE_CHANGED = 'skippableStateChanged', + /** + * Fired when the ad is skipped by the user. + */ + SKIPPED = 'skip', + /** + * Fired when the ad starts playing. + */ + STARTED = 'start', + /** + * Fired when the ad playhead crosses third quartile. + */ + THIRD_QUARTILE = 'thirdQuartile', + /** + * Fired when the ad is closed by the user. + */ + USER_CLOSE = 'userClose', + /** + * Fired when the ad volume has changed. + */ + VOLUME_CHANGED = 'volumeChange', + /** + * Fired when the ad volume has been muted. + */ + VOLUME_MUTED = 'mute', + } + type Listener = (event: AdEvent) => void + } + /** + * An ad may be part of a pod of ads. This object exposes metadata related to that pod, such as the number of ads in the pod and ad position within the pod. + * + * The getTotalAds API contained within this object is often correct, but in certain scenarios, it represents the SDK's best guess. See that method's documentation for more information. + */ + interface AdPodInfo { + /** + * Returns the position of the ad. + * @returns The position of the ad within the pod. The value returned is one-based, i.e. 1 of 2, 2 of 2, etc. + */ + getAdPosition(): number + /** + * Returns true if the ad is a bumper ad. Bumper ads are short linear ads that can indicate to a user when the user is entering into or exiting from an ad break. + * @returns Whether the ad is a bumper ad. + */ + getIsBumper(): boolean + /** + * The maximum duration of the pod in seconds. For unknown duration, -1 is returned. + * @returns The maximum duration of the ads in this pod in seconds. + */ + getMaxDuration(): number + /** + * Returns the index of the ad pod. + * + * For preroll pod, 0 is returned. For midrolls, 1, 2, ... N is returned. For postroll, -1 is returned. + * + * For pods in VOD streams with dynamically inserted ads, 0...N is returned regardless of whether the ad is a pre-, mid-, or post-roll. + * + * Defaults to 0 if this ad is not part of a pod, or the pod is not part of an ad playlist. + * + * @returns The index of the pod in the ad playlist. + */ + getPodIndex(): number + /** + * Returns the content time offset at which the current ad pod was scheduled. For pods in VOD streams with dynamically inserted ads, stream time is returned. + * + * For preroll pod, 0 is returned. For midrolls, the scheduled time is returned. For postroll, -1 is returned. + * + * Defaults to 0 if this ad is not part of a pod, or the pod is not part of an ad playlist. + * + * @returns The time offset for the current ad pod. + */ + getTimeOffset(): number + /** + * The total number of ads contained within this pod, including bumpers. Bumper ads are short linear ads that can indicate to a user when the user is entering into or exiting from an ad break. + * + * Defaults to 1 if this ad is not part of a pod. + * + * In certain scenarios, the SDK does not know for sure how many ads are contained within this ad pod. These scenarios include ad pods, which are multiple ads within a single ad tag. In these scenarios, the first few AdEvents fired (AD_METADATA, LOADED, etc.) may have just the total number of ad tags from the playlist response. We recommend using the STARTED event as the event in which publishers pull information from this object and update the visual elements of the player, if any. + * + * @returns Total number of ads in the pod. + */ + getTotalAds(): number + } + /** + * AdsLoader allows clients to request ads from ad servers. To do so, users must register for the AdsManagerLoadedEvent event and then request ads. + */ + class AdsLoader { + /** + * @param container The display container for ads. + */ + constructor(container: AdDisplayContainer) + /** + * Adds an event listener for the specified type. + * @param type The event type to listen to. + * @param listener The function to call when the event is triggered. + * @param useCapture Optional + */ + public addEventListener(type: AdsManagerLoadedEvent.Type, listener: AdsManagerLoadedEvent.Listener, useCapture?: boolean): void + /** + * Adds an event listener for the specified type. + * @param type The event type to listen to. + * @param listener The function to call when the event is triggered. + * @param useCapture Optional + */ + public addEventListener(type: AdErrorEvent.Type, listener: AdErrorEvent.Listener, useCapture?: boolean): void + /** + * Removes an event listener for the specified type. + * @param type The event type for which to remove an event listener. + * @param listener The function of the event handler to remove from the event target. + * @param useCapture Optional + */ + public removeEventListener(type: AdsManagerLoadedEvent.Type, listener: AdsManagerLoadedEvent.Listener, useCapture?: boolean): void + /** + * Removes an event listener for the specified type. + * @param type The event type for which to remove an event listener. + * @param listener The function of the event handler to remove from the event target. + * @param useCapture Optional + */ + public removeEventListener(type: AdErrorEvent.Type, listener: AdErrorEvent.Listener, useCapture?: boolean): void + /** + * Signals to the SDK that the content is finished. This will allow the SDK to play post-roll ads, if any are loaded via ad rules. + */ + public contentComplete(): void + /** + * Cleans up the internal state. + */ + public destroy(): void + /** + * Returns the IMA SDK settings instance. To change the settings, just call the methods on the instance. The changes will apply for all the ad requests made with this ads loader. + * @returns The settings instance. + */ + public getSettings(): ImaSdkSettings + /** + * Request ads from a server. + * @param adsRequest AdsRequest instance containing data for the ads request. + * @param userRequestContext User-provided object that is associated with the ads request. It can be retrieved when the ads are loaded. + */ + public requestAds(adsRequest: AdsRequest, userRequestContext?: any): void + } + /** + * This class is responsible for playing ads. + */ + interface AdsManager { + /** + * Adds an event listener for the specified type. + * @param type The event type to listen to + * @param listener The function to call when the event is triggered + * @param useCapture Optional + */ + addEventListener(type: AdEvent.Type, listener: AdEvent.Listener, useCapture?: boolean): void + /** + * Adds an event listener for the specified type. + * @param type The event type to listen to + * @param listener The function to call when the event is triggered + * @param useCapture Optional + */ + addEventListener(type: AdErrorEvent.Type, listener: AdErrorEvent.Listener, useCapture?: boolean): void + /** + * Removes an event listener for the specified type. + * @param type The event type for which to remove an event listener. + * @param listener The function of the event handler to remove from the event target. + * @param useCapture Optional + */ + removeEventListener(type: AdEvent.Type, listener: AdEvent.Listener, useCapture?: boolean): void + /** + * Removes an event listener for the specified type. + * @param type The event type for which to remove an event listener. + * @param listener The function of the event handler to remove from the event target. + * @param useCapture Optional + */ + removeEventListener(type: AdErrorEvent.Type, listener: AdErrorEvent.Listener, useCapture?: boolean): void + /** + * Collapse the current ad. This is no-op for HTML5 SDK. + */ + collapse(): void + /** + * Removes ad assets loaded at runtime that need to be properly removed at the time of ad completion and stops the ad and all tracking. + */ + destroy(): void + /** + * If an ad break is currently playing, discard it and resume content. Otherwise, ignore the next scheduled ad break. For example, this can be called immediately after the ads manager loads to ignore a preroll without losing future midrolls or postrolls. This is a no-op unless the ad request returned a playlist or VMAP response. + */ + discardAdBreak(): void + /** + * Expand the current ad. This is no-op for HTML5 SDK. + */ + expand(): void + /** + * Returns true if the ad can currently be skipped. When this value changes, the AdsManager fires an AdEvent.SKIPPABLE_STATE_CHANGED event. + * @returns True if the ad can currently be skipped, false otherwise. + */ + getAdSkippableState(): boolean + /** + * Returns an array of offsets in seconds indicating when a scheduled ad break will play. A preroll is represented by 0, and a postroll is represented by -1. An empty array indicates the ad or ad pod has no schedule and can be played at any time. + * @returns List of time offsets in seconds. + */ + getCuePoints(): number[] + /** + * Get the remaining time of the current ad that is playing. If the ad is not loaded yet or has finished playing, the API would return -1. + * @returns Returns the time remaining for current ad. If the remaining time is undefined for the current ad (for example custom ads), the value returns -1. + */ + getRemainingTime(): number + /** + * Get the volume for the current ad. + * @returns The volume of the current ad, from 0 (muted) to 1 (loudest). + */ + getVolume(): number + /** + * Call init to initialize the ad experience on the ads manager. + * @param width The desired width of the ad. + * @param height The desired height of the ad. + * @param viewMode The desired view mode. + * @param videoElement The video element for custom playback. This video element overrides the one provided in the AdDisplayContainer constructor. Only use this property if absolutely necessary - otherwise we recommend specifying this video element while creating the AdDisplayContainer. + */ + init(width: number, height: number, viewMode: ViewMode, videoElement?: HTMLVideoElement): void + /** + * Returns true if a custom click tracking element is being used for click tracking on the current ad. Custom click tracking is only used when an optional click tracking element is provided to the AdDisplayContainer, custom playback is used, and the current ad is not an AdSense/AdX ad. + * @returns Whether custom click tracking is used. + */ + isCustomClickTrackingUsed(): boolean + /** + * Returns true if a custom video element is being used to play the current ad. Custom playback occurs when an optional video element is provided to the AdDisplayContainer on platforms where a custom video element would provide a more seamless ad viewing experience. + * @returns Whether custom playback is used. + */ + isCustomPlaybackUsed(): boolean + /** + * Pauses the current ad that is playing. This function will be no-op when a static overlay is being shown or if the ad is not loaded yet or is done playing. + */ + pause(): void + /** + * Resizes the current ad. + * @param width New ad slot width. + * @param height New ad slot height. + * @param viewMode The new view mode. + */ + resize(width: number, height: number, viewMode: ViewMode): void + /** + * Resumes the current ad that is loaded and paused. This function will be no-op when a static overlay is being shown or if the ad is not loaded yet or is done playing. + */ + resume(): void + /** + * Set the volume for the current ad. + * @param volume The volume to set, from 0 (muted) to 1 (loudest). + */ + setVolume(volume: number): void + /** + * Skips the current ad when AdsManager.getAdSkippableState() is true. When called under other circumstances, skip has no effect. After the skip is completed the AdsManager fires an AdEvent.SKIPPED event. + */ + skip(): void + /** + * Start playing the ads. + */ + start(): void + /** + * Stop playing the ads. Calling this will get publisher back to the content. + */ + stop(): void + /** + * Updates the ads rendering settings. This should be used specifically for VMAP use cases between ad breaks when ads rendering settings such as bitrate need to be updated. + * @param adsRenderingSettings The updated ads rendering settings. + */ + updateAdsRenderingSettings(adsRenderingSettings: Partial): void + } + /** + * This event is raised when ads are successfully loaded from the Google or DoubleClick ad servers via an AdsLoader. You can register for this event on AdsLoader. + */ + class AdsManagerLoadedEvent { + /** + * After ads are loaded from the Google or DoubleClick ad servers, the publisher needs to play these ads either in their own video player or in the Google-provided video player. This method returns an AdsManager object. The AdsManager supports playing ads and allows the publisher to subscribe to various events during ad playback. + * @param contentPlayback Player that plays back publisher's content. This must be an object that contains the property currentTime, which allows the SDK to query playhead position to properly display midrolls in case ad server responds with an ad rule, and the duration property. The HMTL5 video element fulfills these requirements. You may optionally implement your own playhead tracker, as long as it fulfills the above requirements. + * @param adsRenderingSettings Optional settings to control the rendering of ads. + * @returns AdsManager that manages and plays ads. + */ + public getAdsManager(contentPlayback: { + currentTime: number + duration: number + }, adsRenderingSettings?: Partial): AdsManager + /** + * @returns During ads load request it is possible to provide an object that is available once the ads load is complete. One possible use case: relate ads response to a specific request and use user request content object as a key for identifying the response. + */ + public getUserRequestContext(): any + } + namespace AdsManagerLoadedEvent { + /** + * Types of AdsManagerLoadedEvents. + */ + enum Type { + /** + * Fired when the ads have been loaded and an AdsManager is available. + */ + ADS_MANAGER_LOADED = 'adsManagerLoaded', + } + type Listener = (event: AdsManagerLoadedEvent) => void + } + /** + * Defines parameters that control the rendering of ads. + */ + class AdsRenderingSettings { + /** + * Set to false if you wish to have fine grained control over the positioning of all non-linear ads. If this value is true, the ad is positioned in the bottom center. If this value is false, the ad is positioned in the top left corner. The default value is true. + */ + public autoAlign: boolean + /** + * Maximum recommended bitrate. The value is in kbit/s. The SDK will pick media with bitrate below the specified max, or the closest bitrate if there is no media with lower bitrate found. Default value, -1, means the bitrate will be selected by the SDK. + */ + public bitrate: number + /** + * Enables preloading of video assets. For more info see our guide to preloading media. + */ + public enablePreloading: boolean + /** + * Timeout (in milliseconds) when loading a video ad media file. If loading takes longer than this timeout, the ad playback is canceled and the next ad in the pod plays, if available. Use -1 for the default of 8 seconds. + */ + public loadVideoTimeout: number + /** + * Only supported for linear video mime types. If specified, the SDK will include media that matches the MIME type(s) specified in the list and exclude media that does not match the specified MIME type(s). The format is a list of strings, e.g., [ 'video/mp4', 'video/webm', ... ] If not specified, the SDK will pick the media based on player capabilities. + */ + public mimeTypes: string[] + /** + * For VMAP and ad rules playlists, only play ad breaks scheduled after this time (in seconds). This setting is strictly after - e.g. setting playAdsAfterTime to 15 will cause IMA to ignore an ad break scheduled to play at 15s. + */ + public playAdsAfterTime: number + /** + * Specifies whether or not the SDK should restore the custom playback state after an ad break completes. This is setting is used primarily when the publisher passes in its content player to use for custom ad playback. + */ + public restoreCustomPlaybackStateOnAdBreakComplete: boolean + /** + * Specifies whether the UI elements that should be displayed. The elements in this array are ignored for AdSense/AdX ads. + */ + public uiElements: UiElements[] + /** + * Render linear ads with full UI styling. This setting does not apply to AdSense/AdX ads or ads played in a mobile context that already use full UI styling by default. + */ + public useStyledLinearAds: boolean + /** + * Render non-linear ads with a close and recall button. + */ + public useStyledNonLinearAds: boolean + } + /** + * A class for specifying properties of the ad request. + */ + class AdsRequest { + /** + * Specifies a VAST 2.0 document to be used as the ads response instead of making a request via an ad tag url. This can be useful for debugging and other situations where a VAST response is already available. + * + * This parameter is optional. + */ + public adsResponse?: string + /** + * Specifies the ad tag url that is requested from the ad server. For details on constructing the ad tag url, see Create a master video tag manually. + * + * This parameter is required. + */ + public adTagUrl: string + /** + * Specifies the duration of the content in seconds to be shown. Used in AdX requests. + * + * This parameter is optional. + */ + public contentDuration?: number + /** + * Specifies the keywords used to describe the content to be shown. Used in AdX requests. + * + * This parameter is optional. + */ + public contentKeywords?: string[] + /** + * Specifies the title of the content to be shown. Used in AdX requests. + * + * This parameter is optional. + */ + public contentTitle?: string + /** + * Forces non-linear AdSense ads to render as linear fullslot. If set, the content video will be paused and the non-linear text or image ad will be rendered as fullslot. The content video will resume once the ad has been skipped or closed. + */ + public forceNonLinearFullSlot?: boolean + /** + * Specifies the height of the rectangular area within which a linear ad is displayed. This value is used as one of the criteria for ads selection. This value does not need to match actual ad's height. + * + * This parameter is required. + */ + public linearAdSlotHeight: number + /** + * Specifies the width of the rectangular area within which a linear ad is displayed. This value is used as one of the criteria for ads selection. This value does not need to match actual ad's width. + * + * This parameter is required. + */ + public linearAdSlotWidth: number + /** + * Specifies the maximum amount of time to wait in seconds, after calling requestAds, before requesting the ad tag URL. This can be used to stagger requests during a live-stream event, in order to mitigate spikes in the number of requests. + */ + public liveStreamPrefetchSeconds?: number + /** + * Specifies the height of the rectangular area within which a non linear ad is displayed. This value is used as one of the criteria for ads selection. This value does not need to match actual ad's height. + * + * This parameter is required. + */ + public nonLinearAdSlotHeight: number + /** + * Specifies the width of the rectangular area within which a non linear ad is displayed. This value is used as one of the criteria for ads selection. This value does not need to match actual ad's width. + * + * This parameter is required. + */ + public nonLinearAdSlotWidth: number + /** + * Specifies the full url of the page that will be included in the Google ad request for targeting purposes. The url needs to be a valid url. If specified, this value will be used for the [PAGEURL] VAST macro. + * + * This parameter is optional. + */ + public pageUrl?: string + /** + * Override for default VAST load timeout in milliseconds for a single wrapper. The default timeout is 5000ms. + * + * This parameter is optional. + */ + public vastLoadTimeout?: number + /** + * Notifies the SDK whether the player intends to start the content and ad in response to a user action or whether it will be automatically played. Changing this setting will have no impact on ad playback. + * @param autoPlay Whether the content and the ad will be autoplayed or whether it will be started by a user action. + */ + public setAdWillAutoPlay(autoPlay: boolean): void + /** + * Notifies the SDK whether the player intends to start ad while muted. Changing this setting will have no impact on ad playback, but will send the appropriate signal in the ad request to allow buyers to bid on muted inventory. + * @param muted Whether the ad will be played while muted. + */ + public setAdWillPlayMuted(muted: boolean): void + /** + * Notifies the SDK whether the player intends to continuously play the content videos one after another similar to TV broadcast. Changing this setting will have no impact on the ad playback, but will send the appropriate signal in this ad request to allow buyers to bid on the type of ad inventory. + * @param continuousPlayback Whether the content video is played one after another continuously. + */ + public setContinuousPlayback(continuousPlayback: boolean): void + } + /** + * A companion ad class that is extended by companion ads of different ad types. + */ + interface CompanionAd { + /** + * @returns Returns the ad slot id for this companion. + */ + getAdSlotId(): string + /** + * Returns the HTML content for the companion ad that can be added to the publisher page. + * @returns The HTML content. + */ + getContent(): string + /** + * @returns The content type of the Companion Ad. This may return null if the content type is not known (such as in the case of a VAST HTMLResource or IFrameResource). + */ + getContentType(): string | null + /** + * @returns Returns the height of the companion in pixels. + */ + getHeight(): number + /** + * @returns Returns the width of the companion in pixels. + */ + getWidth(): number + } + /** + * CompanionAdSelectionSettings object is used to define the selection criteria when calling the ima.Ad.getCompanionAds function. + */ + class CompanionAdSelectionSettings { + /** + * The companion ad slot ids to be used for matching set by the user. + */ + public adSlotIds: string[] + /** + * Creative type setting set by the user. + */ + public creativeType: CompanionAdSelectionSettings.CreativeType + /** + * The near fit percent set by the user. + */ + public nearMatchPercent: number + /** + * Resource type setting set by the user. + */ + public resourceType: CompanionAdSelectionSettings.ResourceType + /** + * Size criteria setting set by the user. + */ + public sizeCriteria: CompanionAdSelectionSettings.SizeCriteria + } + namespace CompanionAdSelectionSettings { + /** + * Available choices for creative type of a companion ad. The user can specify any of these choices as a criterion for selecting companion ads. + */ + enum CreativeType { + /** + * Specifies all creative types. + */ + ALL = 'All', + /** + * Specifies Flash creatives. + */ + FLASH = 'Flash', + /** + * Specifies image creatives (such as JPEG, PNG, GIF, etc). + */ + IMAGE = 'Image', + } + /** + * Available choices for resource type of a companion ad. The user can specify any of these choices as a criterion for selecting companion ads. + */ + enum ResourceType { + /** + * Specifies that the resource can be any type of resource. + */ + ALL = 'All', + /** + * Specifies that the resource should be an HTML snippet. + */ + HTML = 'Html', + /** + * Specifies that the resource should be a URL that should be used as the source of an iframe. + */ + IFRAME = 'IFrame', + /** + * Specifies that the resource should be a static file (usually the URL of an image of SWF). + */ + STATIC = 'Static', + } + /** + * Available choices for size selection criteria. The user can specify any of these choices for selecting companion ads. + */ + enum SizeCriteria { + /** + * Specifies that size should be ignored when choosing companions. + */ + IGNORE = 'IgnoreSize', + /** + * Specifies that only companions that match the size of the companion ad slot exactly should be chosen. + */ + SELECT_EXACT_MATCH = 'SelectExactMatch', + /** + * Specifies that any companion close to the size of the companion ad slot should be chosen. + */ + SELECT_NEAR_MATCH = 'SelectNearMatch', + } + } + /** + * This class contains SDK-wide settings. + */ + class ImaSdkSettings { + /** + * Returns the current companion backfill mode. + * @returns The current value. + */ + public getCompanionBackfill(): ImaSdkSettings.CompanionBackfillMode + /** + * Gets whether to disable custom playback on iOS 10+ browsers. The default value is false. + */ + public getDisableCustomPlaybackForIOS10Plus(): boolean + /** + * @returns Whether flash ads should be disabled. + */ + public getDisableFlashAds(): boolean + /** + * Returns the publisher provided locale. + * @returns Publisher provided locale. + */ + public getLocale(): string + /** + * Returns the maximum number of redirects for subsequent redirects will be denied. + * @returns The maximum number of redirects. + */ + public getNumRedirects(): number + /** + * Returns the partner provided player type. + * @returns Partner player type. + */ + public getPlayerType(): string + /** + * Returns the partner provided player version. + * @returns Partner player version. + */ + public getPlayerVersion(): string + /** + * Returns the publisher provided id. + * @returns Publisher provided id. + */ + public getPpid(): string + /** + * Sets whether VMAP and ad rules ad breaks are automatically played + * @param autoPlayAdBreaks Whether to autoPlay the ad breaks. + */ + public setAutoPlayAdBreaks(autoPlayAdBreaks: boolean): void + /** + * Sets the companion backfill mode. Please see the various modes available in google.ima.ImaSdkSettings.CompanionBackfillMode. + * + * The default mode is ima.ImaSdkSettings.CompanionBackfillMode.ALWAYS. + * + * @param mode The desired companion backfill mode. + */ + public setCompanionBackfill(mode: ImaSdkSettings.CompanionBackfillMode): void + /** + * Sets whether to disable custom playback on iOS 10+ browsers. If true, ads will play inline if the content video is inline. This enables TrueView skippable ads. However, the ad will stay inline and not support iOS's native fullscreen. When false, ads will play in the same player as your content. The value set here when an AdDisplayContainer is created is used for the lifetime of the container. The default value is false. + * @param disable Whether or not to disable custom playback. + */ + public setDisableCustomPlaybackForIOS10Plus(disable: boolean): void + /** + * Sets whether flash ads should be disabled. + * @param disableFlashAds Whether flash ads should be disabled. + */ + public setDisableFlashAds(disableFlashAds: boolean): void + /** + * Sets the publisher provided locale. Must be called before creating AdsLoader or AdDisplayContainer. The locale specifies the language in which to display UI elements and can be any two-letter ISO 639-1 code. + * @param locale Publisher-provided locale. + */ + public setLocale(locale: string): void + /** + * Specifies the maximum number of redirects before the subsequent redirects will be denied, and the ad load aborted. The number of redirects directly affects latency and thus user experience. This applies to all VAST wrapper ads. + * @param numRedirects The maximum number of redirects. + */ + public setNumRedirects(numRedirects: number): void + /** + * Sets the partner provided player type. This setting should be used to specify the name of the player being integrated with the SDK. Player type greater than 20 characters will be truncated. The player type specified should be short and unique. This is an optional setting used to improve SDK usability by tracking player types. + * @param playerType The type of the partner player. + */ + public setPlayerType(playerType: string): void + /** + * Sets the partner provided player version. This setting should be used to specify the version of the partner player being integrated with the SDK. Player versions greater than 20 characters will be truncated. This is an optional setting used to improve SDK usability by tracking player version. + * @param playerVersion The version of the partner player. + */ + public setPlayerVersion(playerVersion: string): void + /** + * Sets the publisher provided id. + * @param ppid Publisher provided id. + */ + public setPpid(ppid: string): void + /** + * Sets whether VPAID creatives are allowed. + * @param allowVpaid Whether to allow VPAID creatives. + * @deprecated Please use setVpaidMode. + */ + public setVpaidAllowed(allowVpaid: boolean): void + /** + * Sets VPAID playback mode. + * @param vpaidMode Sets how VPAID ads will be played. Default is to not allow VPAID ads. + */ + public setVpaidMode(vpaidMode: ImaSdkSettings.VpaidMode): void + } + namespace ImaSdkSettings { + /** + * Defines a set of constants for the companion backfill setting. This setting indicates whether companions should be backfilled in various scenarios. + * + * The default value is ALWAYS. + * + * Note that client-side companion backfill requires tagging your companions properly with a Google Publisher Tag (GPT). + */ + enum CompanionBackfillMode { + /** + * If the value is ALWAYS, companion backfill will be attempted in all situations, even when there is no master ad returned. + */ + ALWAYS = 'always', + /** + * If the value is ON_MASTER_AD, companion backfill will be attempted if there is a master ad with fewer companions than there are companion slots. The missing companions will be backfilled. + */ + ON_MASTER_AD = 'on_master_ad', + } + /** + * A set of constants for enabling VPAID functionality. + */ + enum VpaidMode { + /** + * VPAID ads will not play and an error will be returned. + */ + DISABLED = 0, + /** + * VPAID ads are enabled using a cross domain iframe. The VPAID ad cannot access the site. VPAID ads that depend on friendly iframe access may error. This is the default. + */ + ENABLED = 1, + /** + * VPAID ads are enabled using a friendly iframe. This allows the ad access to the site via JavaScript. + */ + INSECURE = 2, + } + } + /** + * Enum specifying different UI elements that can be configured to be displayed or hidden. These settings may be ignored for AdSense and ADX ads. + */ + enum UiElements { + /** + * Displays the "Ad" text in the ad UI. Must be present to show the countdown timer. + */ + AD_ATTRIBUTION = 'adAttribution', + /** + * Ad attribution is required for a countdown timer to be displayed. Both UiElements.COUNTDOWN and UiElements.AD_ATTRIBUTION must be present in AdsRenderingSettings.uiElements. + */ + COUNTDOWN = 'countdown', + } + /** + * Enum specifying different VPAID view modes for ads. + */ + enum ViewMode { + /** + * Fullscreen ad view mode. Indicates to the ads manager that the publisher considers the current AdDisplayContainer arrangement as fullscreen (i.e. simulated fullscreen). This does not cause the ads manager to enter fullscreen. + */ + FULLSCREEN = 'fullscreen', + /** + * Normal ad view mode. + */ + NORMAL = 'normal', + } + /** + * A string containing the full version of the SDK. + */ + const VERSION: string + /** + * Settings for the Google IMA SDK. + */ + const settings: ImaSdkSettings + } + } + export type ImaSdk = typeof google.ima + export class DelegatedEventTarget implements EventTarget { + private delegate + addEventListener(...args: any): void + dispatchEvent(...args: any): boolean + removeEventListener(...args: any): void + } + export class PlayerOptions { + /** Sets whether to disable custom playback on iOS 10+ browsers. If true, ads will play inline if the content video is inline. This enables TrueView skippable ads. However, the ad will stay inline and not support iOS's native fullscreen. */ + disableCustomPlaybackForIOS10Plus: boolean + /** Enables or disables auto resizing of adsManager. If enabled it also resizes non-linear ads. */ + autoResize: boolean + /** Allows to have a separate 'Learn More' click tracking element on mobile. */ + clickTrackingElement?: HTMLElement + } + export type StartAd = { + start: () => void + startWithoutReset: () => void + ad?: google.ima.Ad + adBreakTime?: number + } + export type StartAdCallback = (startAd: StartAd) => void + /** + * Convenience player wrapper for the Google IMA HTML5 SDK + */ + export class Player extends DelegatedEventTarget { + #private + constructor(ima: ImaSdk, mediaElement: HTMLVideoElement, adElement: HTMLElement, adsRenderingSettings?: google.ima.AdsRenderingSettings, options?: PlayerOptions) + /** + * This allows synchronous activation of the media element + * and the Google IMA ad-display-container. Useful when you + * have to do async work before calling "playAds". + */ + activate(): void + /** + * This is the entry point to start ad playback. It can be used + * as such: + * + * - With a single VAST at the beginning to play a preroll + * - Anyhwere during content playback with a single VAST + * - With a single VMAP at the beginning + */ + playAds(adsRequest: google.ima.AdsRequest): void + /** + * Similar to "playAds" method but with the difference + * that it allows to first load the ad and start it separately + * within the given callback. + * + * When a VAST or a VMAP ad break is given the callback is called + * with a "start" method which either starts playing the individual + * VAST ad or starts the VMAP ad break. If "start" method is not called + * it won't play the ad. + */ + loadAds(adsRequest: google.ima.AdsRequest, startAdCallback: StartAdCallback): void + private _mediaElementPlay + private _requestAds + private _setupIma + skipAd(): void + discardAdBreak(): void + /** + * Starts playback of either content or ad element. + */ + play(): void + /** + * Pauses playback of either content or ad element. + */ + pause(): void + /** + * Sets volume of either content or ad element. + */ + set volume(volume: number) + /** + * Returns volume of either content or ad element. + */ + get volume(): number + /** + * Sets muted state on either content or ad element. + */ + set muted(muted: boolean) + /** + * Returns muted state of either content or ad element. + */ + get muted(): boolean + /** + * Sets current time of content element when not in ad playback mode. + */ + set currentTime(currentTime: number) + /** + * Returns current playhead time of either content or ad element. + */ + get currentTime(): number + /** + * Returns current duration of either content or ad element. + */ + get duration(): number + /** + * Returns list of ad break cue points that weren't played yet. + * Only available after "AdMetadata" event when VMAP is passed in playAds. + */ + get cuePoints(): number[] + private _setCuePoints + /** + * Remove already played cuepoints + * + * @param timeOffset offset in seconds as defined in VMAP or 0 for preroll and -1 for postroll + */ + private _adjustCuePoints + /** + * Allows resizing the ad element. Useful when options.autoResize = false. + */ + resizeAd(width: number, height: number): void + /** + * Cleans up current ad and ad manager session or the complete IMA (via force). + * + * Externally call this function with "force = true" when you want to switch + * the content source or move the player to another DOM node before doing + * another "playAds" or "loadAds", so that it does a full cleanup. + * + * @param force - enforce a full cleanup + * @returns a promise which resolves after all the cleanup work is done + */ + reset(force?: boolean): void + private _resetIma + /** + * Completely destroys this instance. It is unusable after that. + */ + destroy(): void + isCustomPlaybackUsed(): boolean + private _resetAd + private _handleMediaElementEvents + private _handleAdsManagerEvents + private _onAdsLoaderError + private _onAdsManagerLoaded + private _startAdsManager + private _mediaStop + private _resizeObserverCallback + private _resizeAdsManager + private _getViewMode + private _playContent + private _createPlayerErrorFromImaErrorEvent + private _onAdError + } +} +/* eslint-enable ts/method-signature-style, ts/consistent-type-definitions */ declare global { interface Window { artplayerPluginVast?: typeof artplayerPluginVast } } - -type PlayUrlFn = (url: string, config?: any) => void -type PlayResFn = (res: string, config?: any) => void - -interface VastPluginContext { - art: Artplayer - ima: any - imaPlayer: Player | null - playUrl: PlayUrlFn - playRes: PlayResFn - init: () => Player - adsRenderingSettings: any - playerOptions: PlayerOptions - container: HTMLDivElement | null +declare namespace artplayerPluginVastDefinitions { + export type PlayUrlFn = (url: string, config?: any) => void + export type PlayResFn = (res: string, config?: any) => void + export interface VastPluginContext { + art: Artplayer + ima: any + imaPlayer: Player | null + playUrl: PlayUrlFn + playRes: PlayResFn + init: () => Player + adsRenderingSettings: any + playerOptions: PlayerOptions + container: HTMLDivElement | null + } + export type ArtplayerPluginVastOption = (params: VastPluginContext) => void | Promise + export interface ArtplayerPluginVastInstance { + name: 'artplayerPluginVast' + destroy?: () => void + } + export function artplayerPluginVast(option: ArtplayerPluginVastOption): (art: Artplayer) => ArtplayerPluginVastInstance } - -export type ArtplayerPluginVastOption = (params: VastPluginContext) => void | Promise - -export interface ArtplayerPluginVastInstance { - name: 'artplayerPluginVast' - destroy?: () => void +declare const artplayerPluginVast: typeof artplayerPluginVastDefinitions.artplayerPluginVast +declare namespace artplayerPluginVast { + export type ArtplayerPluginVastOption = artplayerPluginVastDefinitions.ArtplayerPluginVastOption + export type ArtplayerPluginVastInstance = artplayerPluginVastDefinitions.ArtplayerPluginVastInstance } - -declare function artplayerPluginVast( - option: ArtplayerPluginVastOption, -): (art: Artplayer) => ArtplayerPluginVastInstance - -export default artplayerPluginVast - export = artplayerPluginVast export as namespace artplayerPluginVast; diff --git a/package.json b/package.json index 904aa2ae2..31f808c96 100644 --- a/package.json +++ b/package.json @@ -40,14 +40,14 @@ "test:dash-control": "node --test test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js", "dev": "npx cross-env NODE_ENV=development node ./scripts/dev.js", "build": "npx cross-env NODE_ENV=production node ./scripts/build.js", - "lint": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"scripts/docs-smoke/**/*.ts\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --no-fix", + "lint": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"scripts/{docs-smoke,editor-declarations}/**/*.ts\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --no-fix", "build:all": "yarn ci:build && yarn lint", "check:toolchain": "node scripts/check-toolchain.mjs", - "lint:fix": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"scripts/docs-smoke/**/*.ts\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --fix", + "lint:fix": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"scripts/{docs-smoke,editor-declarations}/**/*.ts\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --fix", "check:plan": "node refactor/scripts/plan.mjs --check", "test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js test/package-check.test.js test/declarations.test.js test/editor-types.test.js test/coverage.test.js test/performance-report.test.js test/media-gate.test.js test/ci-summary.test.js test/package-runtime.test.js test/library-build.test.js", "test:baseline": "node --test refactor/scripts/*.test.mjs", - "ci:check": "yarn check:toolchain --strict && yarn check:commits --report && yarn check:impact --report && yarn check:ci && yarn test:contracts && yarn check:contracts --report && yarn check:plan && yarn lint && yarn typecheck:docs-tools && yarn check:docs-smoke && yarn check:types && yarn typecheck && yarn typecheck:react && yarn lint:react && yarn typecheck:vue && yarn lint:vue && yarn test", + "ci:check": "yarn check:toolchain --strict && yarn check:commits --report && yarn check:impact --report && yarn check:ci && yarn test:contracts && yarn check:contracts --report && yarn check:plan && yarn lint && yarn typecheck:docs-tools && yarn check:docs-smoke && yarn check:editor-types && yarn check:types && yarn typecheck && yarn typecheck:react && yarn lint:react && yarn typecheck:vue && yarn lint:vue && yarn test", "ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:test && yarn build:docs && yarn test:imports", "test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js test/asr-distribution.test.js", "typecheck": "node scripts/typecheck.mjs", @@ -120,7 +120,8 @@ "test:vue-consumer": "node refactor/scripts/vue-consumer.mjs", "lint:vue": "eslint example/vue.js --no-fix", "typecheck:docs-tools": "node node_modules/typescript/bin/tsc -p scripts/tsconfig.docs.json --noEmit", - "check:docs-smoke": "node scripts/build-test.js --check" + "check:docs-smoke": "node scripts/build-test.js --check", + "check:editor-types": "node scripts/build-ts.js --check" }, "browserslist": "last 1 Chrome version", "devDependencies": { diff --git a/packages/artplayer-vitepress/README.md b/packages/artplayer-vitepress/README.md index f0a274fc0..730ec2f01 100644 --- a/packages/artplayer-vitepress/README.md +++ b/packages/artplayer-vitepress/README.md @@ -42,6 +42,9 @@ The repository generators have different ownership: - `scripts/build-types.mjs`: core public declaration sources to existing `types/`. - `scripts/build-ts.js`: standalone editor declarations and the editor library list. + Checked TS modules in `scripts/editor-declarations/` validate all selected + declarations together with current and historical compilers before writing. + Use `yarn check:editor-types` for drift checks; see that module's README. - `scripts/build-test.js`: extracts Chinese Run Code blocks into `docs/test/test.js`. The TS implementation in `scripts/docs-smoke/` produces deterministic readiness smoke cases with owned frames, error observation and cleanup. It does not prove diff --git a/refactor/baselines/editor-declarations-validation.json b/refactor/baselines/editor-declarations-validation.json new file mode 100644 index 000000000..8478be78e --- /dev/null +++ b/refactor/baselines/editor-declarations-validation.json @@ -0,0 +1,444 @@ +{ + "task": "SITE-02", + "baseline": "1a5440a67f50c8c1c5c695099bea5ee5d4b0cc77", + "node": "24.21.0", + "yarn": "1.22.22", + "compilers": [ + "5.9.3", + "4.3.5" + ], + "scope": { + "generatedDeclarations": 22, + "changedDeclarations": [ + "docs/assets/ts/artplayer-plugin-chapter.d.ts", + "docs/assets/ts/artplayer-plugin-vast.d.ts" + ], + "unchangedDeclarations": 20, + "totalOutputs": 24, + "rootLintExistingWarnings": 1, + "packageRuntimeChanged": false, + "packagePublicTypesChanged": false, + "lockUnchanged": true + }, + "logs": { + "reproduction": { + "file": "refactor/.cache/editor-before.log", + "sha256": "13639bee172bc86c2b8787f325fa251be989c16c1bbf00a0a77636fe591da7fc" + }, + "targeted": { + "file": "refactor/.cache/editor-tests-final.log", + "sha256": "bdcc879dc19dc25a086442c982f7e9fa091ec011d80e7878dfa9bab222733f85", + "exitCode": 0, + "pass": 4 + }, + "baseline": { + "file": "refactor/.cache/editor-baseline.log", + "sha256": "c92fcf9cfe1c64dfecb23d44bb37542519b78f7da4318b1c80322a81948949f4", + "exitCode": 0, + "pass": 522 + }, + "browserBefore": { + "file": "refactor/.cache/editor-browser.log", + "sha256": "07ed1bbb4fdc0ba2f45e8ef659ad8d73bbb1a6ffb1cfee0c212f7bc4829996a3", + "exitCode": 1, + "pass": 0 + }, + "browserAccepted": { + "file": "refactor/.cache/editor-browser-verified.log", + "sha256": "72784c4ce8f6ce298abc93529d1c2d2dfde1bb4bc582ed889b02ba27ab856b93", + "exitCode": 0, + "pass": 3 + }, + "buildAll": { + "file": "refactor/.cache/editor-all-build.log", + "sha256": "b3c6a01e75ce9e33ac43e63d90a79c5b3924294985d054ba1ef267b8ab683baf", + "exitCode": 0 + }, + "readonlyCheck": { + "file": "refactor/.cache/editor-readonly-check.log", + "sha256": "91e7a45d96d8a86b96d49822ee6284e70e9c4577cc7ac8bc730fbfd2087b3517", + "exitCode": 0 + }, + "typecheckTools": { + "file": "refactor/.cache/editor-types-check.log", + "sha256": "713f8f9c2ea18e5da3e50f5e3a943515b4e93e7adb8172025ee31a7d7b566cff", + "exitCode": 0 + }, + "rootLint": { + "file": "refactor/.cache/editor-root-lint.log", + "sha256": "5d7ca80241e65fed23117d779a3e1fba0b2281003a304005ea014a0e3cb75cdf", + "exitCode": 0 + }, + "targetedLint": { + "file": "refactor/.cache/editor-final-lint.log", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "exitCode": 0 + }, + "ci": { + "file": "refactor/.cache/editor-ci-tests.log", + "sha256": "ebc514dfc48afff766aeba32c689a922efa36e8c85fc5d2552ca36f22248c0e5", + "exitCode": 0, + "pass": 50 + }, + "strictToolchainBefore": { + "file": "refactor/.cache/editor-toolchain-direct-before.log", + "sha256": "23de6d33eafd3123528d9a4f7b3d82131a32ee90e0724eaab8f2446bca9b48da", + "exitCode": 1 + }, + "strictToolchain": { + "file": "refactor/.cache/editor-toolchain.log", + "sha256": "3da883e18990c25a4eee9f46febb386fc33b0ea6a25ff748d5c129bb8d16ea85", + "exitCode": 0 + } + }, + "browser": { + "file": "refactor/.cache/editor-browser-accepted/report.json", + "sha256": "20de4582ac416faa3e69d7a1444018a99e7ba0f00f60a114c5905cee5fdf6ff7", + "stats": { + "startTime": "2026-09-13T23:16:22.090Z", + "duration": 23649.482000000004, + "expected": 3, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "Monaco checks all editor declarations together and runs the Chapter consumer", + "project": "chromium", + "status": "expected", + "attempts": 1, + "browser": "153.0.8010.12", + "platform": "win32", + "errors": [], + "result": { + "syntax": [], + "semantic": [], + "declarations": [], + "bad": [ + 2322, + 2322, + 2322 + ], + "name": "artplayerPluginChapter", + "ready": true, + "instances": 0 + }, + "declarationInputs": { + "artplayer-plugin-ads.d.ts": "bf128828d5b104fc844a4f134a02162e57472d8c927a50df7b3878f4c5ed7652", + "artplayer-plugin-ambilight.d.ts": "9f06ccb7398053dca642ca369aa47f54cbb3e4dd4d5e252e999e59e496d8eee8", + "artplayer-plugin-asr.d.ts": "5096f097636c018bc05c497324c271af3fefaebccda7fa3e9cd43b63477704ad", + "artplayer-plugin-audio-track.d.ts": "604af2a3790eb7c583ba150c402dca0a3e458469cc74913b51fee2f3d3882541", + "artplayer-plugin-auto-thumbnail.d.ts": "f66bba7f633202c2ef359a989269c643b75d91594364f7ed6fb67f9898aeac03", + "artplayer-plugin-chapter.d.ts": "d5bd68579659afa44158c894e12372cedd6f958415ced370a65c294550aa828d", + "artplayer-plugin-chromecast.d.ts": "0a0b932081cac0efbcedea28dc1077484963b7f406808378704062661a4c0338", + "artplayer-plugin-danmuku-mask.d.ts": "06c6e938876edcc0ac2c8dcd1e1be4596b0f056685a3523d4c9836f2f2f90362", + "artplayer-plugin-danmuku.d.ts": "e3421bdbdfc6bea1b0b633300350fff1a745d8bc21c1430b6154a23fa3ad0415", + "artplayer-plugin-dash-control.d.ts": "c7e3c83f3377b615e503a270af12915dcc82fe3f2cfabdae396632855a27f73f", + "artplayer-plugin-document-pip.d.ts": "42faa0a6549149efd01b46c3c59246bd4dc33f2383b63b04e9f5376de29fdb8e", + "artplayer-plugin-hls-control.d.ts": "6a623bb6ba4cc0598a3dd1aab5ef7ca4fbae11973a787885eccba10cc7aa2c7c", + "artplayer-plugin-jassub.d.ts": "ce713e0a61a81bfc31087ea1a880a99398ef7c2b833e567c91e4e98f5ae6f621", + "artplayer-plugin-multiple-subtitles.d.ts": "edb15207df69a695311e849555cf989bfc32da20c2456731b6814de5400567bb", + "artplayer-plugin-vast.d.ts": "b21ed9a852b3ec3cc9b409cce08e91905e42abdc1d642acc9b412c33c83287b7", + "artplayer-plugin-vtt-thumbnail.d.ts": "d82c0fe08bcf9c60e2a634e9f1fd8ccc75dc8e0ddbfa8262cccaefc9604aa61b", + "artplayer-proxy-canvas.d.ts": "83c408e0724a598f9ff32d68d752f39e03feb69ba4f6a49ba02f9d4df898453e", + "artplayer-proxy-mediabunny.d.ts": "acbaa0a25f20ac15b3d79db00b3a16167f84452ab58933110f42dcb3262fbd3e", + "artplayer-tool-iframe.d.ts": "c06c6819146ba64bd6ee498871d9e19ab5a9e88c9cc7bed2c0c4968fa39dfed7", + "artplayer-tool-thumbnail.d.ts": "9b72d10a1fe964d54fcf4774770074058ea739b4f734b52b8f4adebb37fc83e6", + "artplayer.d.ts": "652c0fdf605e87203526777a05851bd69c95c1660d5ff29eab19c00967c9a985", + "artplayer-i18n.d.ts": "60851e22ea50ed540f1be0f5a110077cdd5e6ab87345a675838f6c6333517405" + } + }, + { + "title": "Monaco checks all editor declarations together and runs the Chapter consumer", + "project": "firefox", + "status": "expected", + "attempts": 1, + "browser": "155.0", + "platform": "win32", + "errors": [], + "result": { + "syntax": [], + "semantic": [], + "declarations": [], + "bad": [ + 2322, + 2322, + 2322 + ], + "name": "artplayerPluginChapter", + "ready": true, + "instances": 0 + }, + "declarationInputs": { + "artplayer-plugin-ads.d.ts": "bf128828d5b104fc844a4f134a02162e57472d8c927a50df7b3878f4c5ed7652", + "artplayer-plugin-ambilight.d.ts": "9f06ccb7398053dca642ca369aa47f54cbb3e4dd4d5e252e999e59e496d8eee8", + "artplayer-plugin-asr.d.ts": "5096f097636c018bc05c497324c271af3fefaebccda7fa3e9cd43b63477704ad", + "artplayer-plugin-audio-track.d.ts": "604af2a3790eb7c583ba150c402dca0a3e458469cc74913b51fee2f3d3882541", + "artplayer-plugin-auto-thumbnail.d.ts": "f66bba7f633202c2ef359a989269c643b75d91594364f7ed6fb67f9898aeac03", + "artplayer-plugin-chapter.d.ts": "d5bd68579659afa44158c894e12372cedd6f958415ced370a65c294550aa828d", + "artplayer-plugin-chromecast.d.ts": "0a0b932081cac0efbcedea28dc1077484963b7f406808378704062661a4c0338", + "artplayer-plugin-danmuku-mask.d.ts": "06c6e938876edcc0ac2c8dcd1e1be4596b0f056685a3523d4c9836f2f2f90362", + "artplayer-plugin-danmuku.d.ts": "e3421bdbdfc6bea1b0b633300350fff1a745d8bc21c1430b6154a23fa3ad0415", + "artplayer-plugin-dash-control.d.ts": "c7e3c83f3377b615e503a270af12915dcc82fe3f2cfabdae396632855a27f73f", + "artplayer-plugin-document-pip.d.ts": "42faa0a6549149efd01b46c3c59246bd4dc33f2383b63b04e9f5376de29fdb8e", + "artplayer-plugin-hls-control.d.ts": "6a623bb6ba4cc0598a3dd1aab5ef7ca4fbae11973a787885eccba10cc7aa2c7c", + "artplayer-plugin-jassub.d.ts": "ce713e0a61a81bfc31087ea1a880a99398ef7c2b833e567c91e4e98f5ae6f621", + "artplayer-plugin-multiple-subtitles.d.ts": "edb15207df69a695311e849555cf989bfc32da20c2456731b6814de5400567bb", + "artplayer-plugin-vast.d.ts": "b21ed9a852b3ec3cc9b409cce08e91905e42abdc1d642acc9b412c33c83287b7", + "artplayer-plugin-vtt-thumbnail.d.ts": "d82c0fe08bcf9c60e2a634e9f1fd8ccc75dc8e0ddbfa8262cccaefc9604aa61b", + "artplayer-proxy-canvas.d.ts": "83c408e0724a598f9ff32d68d752f39e03feb69ba4f6a49ba02f9d4df898453e", + "artplayer-proxy-mediabunny.d.ts": "acbaa0a25f20ac15b3d79db00b3a16167f84452ab58933110f42dcb3262fbd3e", + "artplayer-tool-iframe.d.ts": "c06c6819146ba64bd6ee498871d9e19ab5a9e88c9cc7bed2c0c4968fa39dfed7", + "artplayer-tool-thumbnail.d.ts": "9b72d10a1fe964d54fcf4774770074058ea739b4f734b52b8f4adebb37fc83e6", + "artplayer.d.ts": "652c0fdf605e87203526777a05851bd69c95c1660d5ff29eab19c00967c9a985", + "artplayer-i18n.d.ts": "60851e22ea50ed540f1be0f5a110077cdd5e6ab87345a675838f6c6333517405" + } + }, + { + "title": "Monaco checks all editor declarations together and runs the Chapter consumer", + "project": "webkit", + "status": "expected", + "attempts": 1, + "browser": "26.6", + "platform": "win32", + "errors": [], + "result": { + "syntax": [], + "semantic": [], + "declarations": [], + "bad": [ + 2322, + 2322, + 2322 + ], + "name": "artplayerPluginChapter", + "ready": true, + "instances": 0 + }, + "declarationInputs": { + "artplayer-plugin-ads.d.ts": "bf128828d5b104fc844a4f134a02162e57472d8c927a50df7b3878f4c5ed7652", + "artplayer-plugin-ambilight.d.ts": "9f06ccb7398053dca642ca369aa47f54cbb3e4dd4d5e252e999e59e496d8eee8", + "artplayer-plugin-asr.d.ts": "5096f097636c018bc05c497324c271af3fefaebccda7fa3e9cd43b63477704ad", + "artplayer-plugin-audio-track.d.ts": "604af2a3790eb7c583ba150c402dca0a3e458469cc74913b51fee2f3d3882541", + "artplayer-plugin-auto-thumbnail.d.ts": "f66bba7f633202c2ef359a989269c643b75d91594364f7ed6fb67f9898aeac03", + "artplayer-plugin-chapter.d.ts": "d5bd68579659afa44158c894e12372cedd6f958415ced370a65c294550aa828d", + "artplayer-plugin-chromecast.d.ts": "0a0b932081cac0efbcedea28dc1077484963b7f406808378704062661a4c0338", + "artplayer-plugin-danmuku-mask.d.ts": "06c6e938876edcc0ac2c8dcd1e1be4596b0f056685a3523d4c9836f2f2f90362", + "artplayer-plugin-danmuku.d.ts": "e3421bdbdfc6bea1b0b633300350fff1a745d8bc21c1430b6154a23fa3ad0415", + "artplayer-plugin-dash-control.d.ts": "c7e3c83f3377b615e503a270af12915dcc82fe3f2cfabdae396632855a27f73f", + "artplayer-plugin-document-pip.d.ts": "42faa0a6549149efd01b46c3c59246bd4dc33f2383b63b04e9f5376de29fdb8e", + "artplayer-plugin-hls-control.d.ts": "6a623bb6ba4cc0598a3dd1aab5ef7ca4fbae11973a787885eccba10cc7aa2c7c", + "artplayer-plugin-jassub.d.ts": "ce713e0a61a81bfc31087ea1a880a99398ef7c2b833e567c91e4e98f5ae6f621", + "artplayer-plugin-multiple-subtitles.d.ts": "edb15207df69a695311e849555cf989bfc32da20c2456731b6814de5400567bb", + "artplayer-plugin-vast.d.ts": "b21ed9a852b3ec3cc9b409cce08e91905e42abdc1d642acc9b412c33c83287b7", + "artplayer-plugin-vtt-thumbnail.d.ts": "d82c0fe08bcf9c60e2a634e9f1fd8ccc75dc8e0ddbfa8262cccaefc9604aa61b", + "artplayer-proxy-canvas.d.ts": "83c408e0724a598f9ff32d68d752f39e03feb69ba4f6a49ba02f9d4df898453e", + "artplayer-proxy-mediabunny.d.ts": "acbaa0a25f20ac15b3d79db00b3a16167f84452ab58933110f42dcb3262fbd3e", + "artplayer-tool-iframe.d.ts": "c06c6819146ba64bd6ee498871d9e19ab5a9e88c9cc7bed2c0c4968fa39dfed7", + "artplayer-tool-thumbnail.d.ts": "9b72d10a1fe964d54fcf4774770074058ea739b4f734b52b8f4adebb37fc83e6", + "artplayer.d.ts": "652c0fdf605e87203526777a05851bd69c95c1660d5ff29eab19c00967c9a985", + "artplayer-i18n.d.ts": "60851e22ea50ed540f1be0f5a110077cdd5e6ab87345a675838f6c6333517405" + } + } + ], + "beforeArchive": "refactor/.cache/editor-browser-before", + "beforeReason": "Directory has 23 .d.ts assets; actual libUris references 22, excluding historical WebSR. Browser test now follows actual loader list." + }, + "sources": [ + { + "file": "scripts/build-ts.js", + "sha256Lf": "1ff2d13f4247bba1818a187eb945a86969f4c4d60245ba5cac8fc0b2b8edd3ba" + }, + { + "file": "scripts/editor-types.mjs", + "sha256Lf": "62c6ef8affb5f28b9a9caa9810e3c5e565ca0d25fb031fe0a169beba6a3a1125" + }, + { + "file": "scripts/plugin-editor-types.mjs", + "sha256Lf": "09b6bdc419e92d6052ba8575aa9b726e7231ca2579578cd91f9b34d1f99b44bc" + }, + { + "file": "scripts/tsconfig.docs.json", + "sha256Lf": "4398157a60f8be091484d332455745708b999753cad3d7e0dd4a216c818da79e" + }, + { + "file": "scripts/tsconfig.editor.json", + "sha256Lf": "ce57cedc75093d62f9d62efe1ff24c1d5d0fd157cbe830d082b8cebcf5f0e6a1" + }, + { + "file": "package.json", + "sha256Lf": "0726e24a97b8107dd7c8effe37202b194d4211a3f06ea9c8c22058029a6d0871" + }, + { + "file": "test/editor-types.test.js", + "sha256Lf": "2c431f49ca5b7428171ee7f494b48ac72df787c9a0824362855c49fc0c4cdbca" + }, + { + "file": "test/browser/editor-declarations.spec.js", + "sha256Lf": "33e824e96b2fb385fa99fd384325f9cf660c49eff0afd4fb0234a3a5f4048e00" + }, + { + "file": "scripts/editor-declarations/core.ts", + "sha256Lf": "0cda0fe093299099c62ae907a6f517f1cb5c49be9085d4c8fb20aed6579a0058" + }, + { + "file": "scripts/editor-declarations/dependencies.ts", + "sha256Lf": "aa697e095f3e9ea476a2ae625714542f62db47e1257cb69cc5e249277db67f8c" + }, + { + "file": "scripts/editor-declarations/generate.ts", + "sha256Lf": "6cf6ee27b2032e9e3a4aef949f2b54ba7bf12e40bed473478f63eb82759c8e5a" + }, + { + "file": "scripts/editor-declarations/plugin.ts", + "sha256Lf": "02dd90a19b75235ac6080aa2088114f3ae51c45567789c7daf2653be63ceee7d" + }, + { + "file": "scripts/editor-declarations/README.md", + "sha256Lf": "968b7d6b558a5dde76bc004517fa02eaa33d81a1051e34b33f21a112d85aa8a0" + }, + { + "file": "scripts/editor-declarations/syntax.ts", + "sha256Lf": "d5c7d8f400b3053e1c1a588dab809f5d17e95be88ecb985cbc90f9ffaea3a0e6" + }, + { + "file": "scripts/editor-declarations/validation.ts", + "sha256Lf": "1b5524844d030ba32df8bb47f1f3963e7df1f0843db61ad1cc7a983db2546517" + }, + { + "file": "scripts/editor-declarations/vast-sdk.ts", + "sha256Lf": "6da9176284b7a697a922b90befa64263e016f12eef483793cc524bdfc028ad05" + } + ], + "outputs": [ + { + "file": "docs/assets/ts/artplayer-i18n.d.ts", + "sha256Lf": "60851e22ea50ed540f1be0f5a110077cdd5e6ab87345a675838f6c6333517405" + }, + { + "file": "docs/assets/ts/artplayer-plugin-ads.d.ts", + "sha256Lf": "bf128828d5b104fc844a4f134a02162e57472d8c927a50df7b3878f4c5ed7652" + }, + { + "file": "docs/assets/ts/artplayer-plugin-ambilight.d.ts", + "sha256Lf": "9f06ccb7398053dca642ca369aa47f54cbb3e4dd4d5e252e999e59e496d8eee8" + }, + { + "file": "docs/assets/ts/artplayer-plugin-asr.d.ts", + "sha256Lf": "5096f097636c018bc05c497324c271af3fefaebccda7fa3e9cd43b63477704ad" + }, + { + "file": "docs/assets/ts/artplayer-plugin-audio-track.d.ts", + "sha256Lf": "604af2a3790eb7c583ba150c402dca0a3e458469cc74913b51fee2f3d3882541" + }, + { + "file": "docs/assets/ts/artplayer-plugin-auto-thumbnail.d.ts", + "sha256Lf": "f66bba7f633202c2ef359a989269c643b75d91594364f7ed6fb67f9898aeac03" + }, + { + "file": "docs/assets/ts/artplayer-plugin-chapter.d.ts", + "sha256Lf": "d5bd68579659afa44158c894e12372cedd6f958415ced370a65c294550aa828d" + }, + { + "file": "docs/assets/ts/artplayer-plugin-chromecast.d.ts", + "sha256Lf": "0a0b932081cac0efbcedea28dc1077484963b7f406808378704062661a4c0338" + }, + { + "file": "docs/assets/ts/artplayer-plugin-danmuku-mask.d.ts", + "sha256Lf": "06c6e938876edcc0ac2c8dcd1e1be4596b0f056685a3523d4c9836f2f2f90362" + }, + { + "file": "docs/assets/ts/artplayer-plugin-danmuku.d.ts", + "sha256Lf": "e3421bdbdfc6bea1b0b633300350fff1a745d8bc21c1430b6154a23fa3ad0415" + }, + { + "file": "docs/assets/ts/artplayer-plugin-dash-control.d.ts", + "sha256Lf": "c7e3c83f3377b615e503a270af12915dcc82fe3f2cfabdae396632855a27f73f" + }, + { + "file": "docs/assets/ts/artplayer-plugin-document-pip.d.ts", + "sha256Lf": "42faa0a6549149efd01b46c3c59246bd4dc33f2383b63b04e9f5376de29fdb8e" + }, + { + "file": "docs/assets/ts/artplayer-plugin-hls-control.d.ts", + "sha256Lf": "6a623bb6ba4cc0598a3dd1aab5ef7ca4fbae11973a787885eccba10cc7aa2c7c" + }, + { + "file": "docs/assets/ts/artplayer-plugin-jassub.d.ts", + "sha256Lf": "ce713e0a61a81bfc31087ea1a880a99398ef7c2b833e567c91e4e98f5ae6f621" + }, + { + "file": "docs/assets/ts/artplayer-plugin-multiple-subtitles.d.ts", + "sha256Lf": "edb15207df69a695311e849555cf989bfc32da20c2456731b6814de5400567bb" + }, + { + "file": "docs/assets/ts/artplayer-plugin-vast.d.ts", + "sha256Lf": "b21ed9a852b3ec3cc9b409cce08e91905e42abdc1d642acc9b412c33c83287b7" + }, + { + "file": "docs/assets/ts/artplayer-plugin-vtt-thumbnail.d.ts", + "sha256Lf": "d82c0fe08bcf9c60e2a634e9f1fd8ccc75dc8e0ddbfa8262cccaefc9604aa61b" + }, + { + "file": "docs/assets/ts/artplayer-proxy-canvas.d.ts", + "sha256Lf": "83c408e0724a598f9ff32d68d752f39e03feb69ba4f6a49ba02f9d4df898453e" + }, + { + "file": "docs/assets/ts/artplayer-proxy-mediabunny.d.ts", + "sha256Lf": "acbaa0a25f20ac15b3d79db00b3a16167f84452ab58933110f42dcb3262fbd3e" + }, + { + "file": "docs/assets/ts/artplayer-tool-iframe.d.ts", + "sha256Lf": "c06c6819146ba64bd6ee498871d9e19ab5a9e88c9cc7bed2c0c4968fa39dfed7" + }, + { + "file": "docs/assets/ts/artplayer-tool-thumbnail.d.ts", + "sha256Lf": "9b72d10a1fe964d54fcf4774770074058ea739b4f734b52b8f4adebb37fc83e6" + }, + { + "file": "docs/assets/ts/artplayer.d.ts", + "sha256Lf": "652c0fdf605e87203526777a05851bd69c95c1660d5ff29eab19c00967c9a985" + }, + { + "file": "docs/assets/js/common.js", + "sha256Lf": "cc9f7d780cf4f0ffb9aaee75da69e2b2b062f3bac07b4ffc9a75956b2e7cd7af" + }, + { + "file": "docs/assets/ts/artplayer-plugin-vast.LICENSE.txt", + "sha256Lf": "967db6e5026a2fd3cc24d8a04e5c78859f543d0c0f6d4f9599c103e4e93c9b29" + } + ], + "sdk": [ + { + "name": "@glomex/vast-ima-player", + "version": "1.21.2", + "manifest": { + "file": "node_modules\\@glomex\\vast-ima-player\\package.json", + "sha256": "c7361321ce792f6830af9eb40e70b7508a75f68290a3c0077e26a6f36daa7097" + }, + "license": { + "file": "node_modules\\@glomex\\vast-ima-player\\LICENSE", + "sha256": "68b35bb64e6269277318ff4e4ee66a738bdeb83bbcdc7e3c3af85a0bdded8d0b" + } + }, + { + "name": "@alugha/ima", + "version": "2.1.0", + "manifest": { + "file": "node_modules\\@alugha\\ima\\package.json", + "sha256": "0c0f9d2a4cc8ea3f96239b44e4ef764630421998e5de91d05d04e42582b24852" + }, + "license": { + "file": "node_modules\\@alugha\\ima\\LICENSE.md", + "sha256": "0fe0f6dd9671aecf84ea082cdcd7b3297b75ca942d082b485b94dd3b4dfd648a" + } + } + ], + "limitations": [ + "Browser uses repository Monaco, not an entire editor UI/route acceptance.", + "VAST is type-only; no ad SDK request, no runtime default behavior decision.", + "No full production/runtime or publication matrix rerun, no remote CI or manual Chrome/iab.", + "No new dependencies, push or publication." + ] +} diff --git a/refactor/baselines/site-inventory.json b/refactor/baselines/site-inventory.json index 9889b2b13..9a7bf6bf9 100644 --- a/refactor/baselines/site-inventory.json +++ b/refactor/baselines/site-inventory.json @@ -18101,15 +18101,15 @@ }, { "file": "scripts/build-ts.js", - "sha256Lf": "338f3b5b4e4bdcb42c878997d13da6f71ce9842d730f57efece7566124dae9f6" + "sha256Lf": "1ff2d13f4247bba1818a187eb945a86969f4c4d60245ba5cac8fc0b2b8edd3ba" }, { "file": "scripts/editor-types.mjs", - "sha256Lf": "951d3d40dc7f90034561fd1a3b3015bdb1449593121bb410e3f15e4415547b8b" + "sha256Lf": "62c6ef8affb5f28b9a9caa9810e3c5e565ca0d25fb031fe0a169beba6a3a1125" }, { "file": "scripts/plugin-editor-types.mjs", - "sha256Lf": "429c304ce7c801836a311e76e85f2e6dc0328d599103e39d071a915164b94102" + "sha256Lf": "09b6bdc419e92d6052ba8575aa9b726e7231ca2579578cd91f9b34d1f99b44bc" }, { "file": "scripts/build-test.js", @@ -18166,6 +18166,34 @@ { "file": "scripts/docs-smoke/runtime.ts", "sha256Lf": "9fae3aa1bc6a6ef9728d9a677de9e283d40314022b710b2ecf19a9b951f6291d" + }, + { + "file": "scripts/editor-declarations/core.ts", + "sha256Lf": "0cda0fe093299099c62ae907a6f517f1cb5c49be9085d4c8fb20aed6579a0058" + }, + { + "file": "scripts/editor-declarations/dependencies.ts", + "sha256Lf": "aa697e095f3e9ea476a2ae625714542f62db47e1257cb69cc5e249277db67f8c" + }, + { + "file": "scripts/editor-declarations/generate.ts", + "sha256Lf": "6cf6ee27b2032e9e3a4aef949f2b54ba7bf12e40bed473478f63eb82759c8e5a" + }, + { + "file": "scripts/editor-declarations/plugin.ts", + "sha256Lf": "02dd90a19b75235ac6080aa2088114f3ae51c45567789c7daf2653be63ceee7d" + }, + { + "file": "scripts/editor-declarations/syntax.ts", + "sha256Lf": "d5c7d8f400b3053e1c1a588dab809f5d17e95be88ecb985cbc90f9ffaea3a0e6" + }, + { + "file": "scripts/editor-declarations/validation.ts", + "sha256Lf": "1b5524844d030ba32df8bb47f1f3963e7df1f0843db61ad1cc7a983db2546517" + }, + { + "file": "scripts/editor-declarations/vast-sdk.ts", + "sha256Lf": "6da9176284b7a697a922b90befa64263e016f12eef483793cc524bdfc028ad05" } ], "generationCommands": { diff --git a/refactor/changes/2026-09-14-SITE-02-editor-declarations.md b/refactor/changes/2026-09-14-SITE-02-editor-declarations.md new file mode 100644 index 000000000..c85dc858f --- /dev/null +++ b/refactor/changes/2026-09-14-SITE-02-editor-declarations.md @@ -0,0 +1,63 @@ +# SITE-02 编辑器声明生成链 + +起点 `1a5440a67f50c8c1c5c695099bea5ee5d4b0cc77`。SITE-SMOKE-01 已完成示例 +生成半项,本次完成声明生成半项,保留全部父任务范围。原 build:ts 命令、按包 +参数、声明 URL、i18n 入口和工具 MJS 导入路径继续可用。生产包源码、公开声明、 +运行时事件/DOM、分发入口与 yarn.lock 未修改。 + +## 结构与修复 + +- `scripts/editor-declarations/` 包含 TS 的 core/plugin AST 转换、语法检查、 + VAST SDK 类型闭包、虚拟编译器 host 和生成编排。旧 JS/MJS 文件仅转发,同属 + 严格 checkJs 范围。新模块维护地图、依赖方向和修改命令见该目录 README。 +- 原 Chapter 文本回退产生 TS2309(default 与 export assignment 混用); + VAST 同时产生 TS2304/2552(删除 import 后丢失 Player/PlayerOptions)。 + 测试固定原转换表达式复现,不靠跳过声明检查或 any 替换解决。 +- 所有 20 个生态库现在统一走语义转换;加 core/i18n 共 22 个加载声明。格式化 + 后以主 TS 5.9.3、旧 TS 4.3.5 整组检查,同时验证命名空间冲突和未解析依赖。 + 生成期间仅作 layout 格式修复;SDK 原方法与 type alias 形状保留,相关 lint + 豁免限定在自动生成的 SDK 命名空间块。其余 20 份声明内容与起点一致。 +- VAST 通过单独 type-only 入口收集 @glomex/vast-ima-player 1.21.2 和 + @alugha/ima 2.1.0。直接 bundle VAST 根声明会因 workspace 链接一并内联核心, + 因此只 bundle SDK 闭包,再用 AST 放入模块内部 namespace,避免重复核心。 + 原 Window 可选属性、必需回调、SDK 方法/私有成员约束保留;不新增 SDK 全局 + 值或运行时加载。自动生成邻接 LICENSE.txt 保存上游 notices。 +- 新增 `check:editor-types` 并加入 ci:check;所有选择、声明检查、格式化、 + notices 和 libUris 生成成功后才写结果。只读检查拒绝缺失/漂移,不写文件。 + libUris 用 AST 定位唯一数组声明,避免找不到旧正则时静默漏更新;按包构建 + 保持不更新总列表的旧行为。多文件最终写盘本身不承诺事务性。 +- 无新增依赖或锁变更;复用已固定 dts-bundle-generator 9.5.1、TS、ESLint、glob。 + 两个真实编译器模块的名义类型不同,仅在工具边界适配,测试实际执行旧编译器。 + +## 验证与修正过程 + +- 4 项编辑器 Node 测试通过:core/ASR 原消费者;Chapter/VAST 旧红新绿、模块 + 导入与全局消费;6 个类型误用分别被拒绝;全部 24 个输出与重生成一致; + 未知/重复选包及缺失/重复/非数组 libUris 拒绝。 +- 完整 baseline 522 通过,0 失败/跳过,覆盖旧 MJS 路径上的包类型回归。 + CI 50 项回归通过;严格 docs-tools 检查、全仓 lint(0 error/1 既有生成 + 声明 warning)、目标 lint 与严格工具链通过。 +- 三浏览器实际 Monaco 各 1 项,共 3 通过,0 跳过/flaky:实际 common.js 中 + 22 份加载声明一起检查,正例无诊断、三个坏参数各报 TS2322,并运行实际 + emit 的 Chapter 代码,真实受控媒体 ready 后销毁、实例归零。没有执行 VAST + 广告 SDK。完整 browser report 和版本/结果摘要见验证 JSON。 +- 首次浏览器检查失败在 23 vs 22 文件数:目录遗留 artplayer-plugin-websr.d.ts + 未列入实际 libUris。测试改为按实际加载列表取文件,原资产保留,未通过删除 + 声明消除失败。失败报告已归档。旧资产的整体处置仍属 SITE-07。 +- 新虚拟 host 的模块正例最初失败,原因是 Windows 根路径尾部分隔符使目录 + 比较不匹配;用 path.resolve 统一后相对导入和全部负例通过。SDK 声明初次 + layout lint 指出上游签名风格,采用限定范围保留原类型形状,未作语义格式替换。 +- 一次直接 node 调严格工具链检查被 Yarn 身份检查拒绝;按规定使用 pinned + Yarn 重新执行成功,未绕过该门槛。原失败日志保留。 + +## 兼容与后续 + +本次兼容修正针对编辑器生成声明,生产根类型和 /runtime 类型均保持原内容。 +Chapter/VAST 编辑器从无效声明变成可严格消费声明,原合法回调和配置不需改写。 +VAST 初始化默认行为、Auto Thumbnail 首帧、真实设备/SDK、完整文档交互与三轮 +发布复盘仍开放。SITE-03/04/05 与 EX-03 接续文档产品流程和完整验收。 + +未重新执行全仓生产运行时套件或全局发布矩阵;本次变化没有生产源码/锁变更。 +没有手动 Chrome/iab 会话、远端 CI、推送或发布。构建/验证只证明上述工具与 +消费者范围。任务单独提交后审计;回退该提交会恢复旧生成链及 Chapter/VAST +声明错误,需重新 build:ts,优先对具体回归单独修复。 diff --git a/refactor/ci-setup.md b/refactor/ci-setup.md index d4369c2c1..618d7fba0 100644 --- a/refactor/ci-setup.md +++ b/refactor/ci-setup.md @@ -10,7 +10,8 @@ | `yarn lint:fix` | 显式自动修复相同范围 | | `yarn typecheck` | 根/迁移包严格检查、当前与兼容 TS 消费;历史 NodeNext ESM 错误单独核对,见 typechecking.md | | `yarn typecheck:react` / `yarn typecheck:vue` | 原 React TSX / Vue SFC 示例严格检查,ci:check 同时执行对应 lint | -| `yarn typecheck:docs-tools` / `yarn check:docs-smoke` | 严格检查 TS 示例生成器及 JS 命令门面;只读核对确定性生成的 readiness smoke,ci:check 执行 | +| `yarn typecheck:docs-tools` / `yarn check:docs-smoke` | 严格检查 TS 示例/声明生成器及 JS/MJS 门面;只读核对确定性生成的 readiness smoke,ci:check 执行 | +| `yarn check:editor-types` | 只读核对全部编辑器声明、SDK notices 和实际 libUris;主 TS 5.9.3/历史 4.3.5 整组语义检查,ci:check 执行 | | `yarn test:react-consumer` / `yarn test:vue-consumer` | 仓库外 tarball 安装、原框架示例、开发/生产三引擎;browser-smoke 执行并上传独立目录,本地证据不代替远端矩阵 | | `yarn test:unit` | 原播放/DASH 回归、同夹具的新旧公共契约与 JS/TS loader 验证 | | `yarn test:node` | test:unit 加工具链/文档构建回归,保留原 test:playback/test:dash-control 入口 | diff --git a/refactor/plan.md b/refactor/plan.md index ab027f62a..80b3f7a25 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -4,7 +4,7 @@ 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 240 项,范围 22 个包及工作区/示例。 -状态:todo 60 / doing 15 / blocked 0 / done 165 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 59 / doing 15 / blocked 0 / done 166 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -382,7 +382,7 @@ | --- | --- | --- | --- | --- | --- | --- | | SITE-01 | artplayer-vitepress, workspace
清点文档/示例/生成链 | BASE-04, BASE-05 | 中英文文档、插件页面、demo URL、编辑器声明与生成目录清单;六类 HTML 入口、prod/libs/code/example 加载行为及移动重定向 | 所有公开 API/插件有对应页面或明确补充任务;核实 BASE-DEMO-01 的历史 thumbnail 插件来源及 29 示例/36 HTML 路径;明确 BASE-05 所记录站点实际分发与未设 private 的 manifest 意图;接续 VENDOR-05/06/07/08 和 BASE-SITE-01/BASE-MEDIA-01,核对字体、Monaco、vConsole、console bundle 与样本来源及分发范围 | M | done | | SITE-SMOKE-01 | artplayer-vitepress, workspace
迁移示例生成器并验证真实就绪与清理 | SITE-01 | TS Markdown 解析/生成与浏览器运行模块;旧命令和 URL、稳定实例就绪 smoke、错误与资源清理 | 复现旧 malformed 死循环并保证有限失败;保留全部既有示例内容、生成确定且 check 只读;不再以 100ms 作为成功,验证原生就绪、异常、取消与清理;三浏览器检查,明确延迟交互与全部 233 示例仍需 EX-03 | M | done | -| SITE-02 | artplayer-vitepress
整理声明与示例生成器 | SITE-01, ENG-04, ENG-06, SITE-SMOKE-01 | build-ts/build-test 生成链的可验证 TS 脚本 | 不靠字符串拼接掩盖声明错误,生成示例有真实断言或仅标 smoke;替换固定 100ms 成功判定,明确异步错误、清理和生成覆盖限制;坏代码块解析必须终止,覆盖 malformed 分支不推进 regexp 的回归 | M | todo | +| SITE-02 | artplayer-vitepress
整理声明与示例生成器 | SITE-01, ENG-04, ENG-06, SITE-SMOKE-01 | build-ts/build-test 生成链的可验证 TS 脚本 | 不靠字符串拼接掩盖声明错误,生成示例有真实断言或仅标 smoke;替换固定 100ms 成功判定,明确异步错误、清理和生成覆盖限制;坏代码块解析必须终止,覆盖 malformed 分支不推进 regexp 的回归 | M | done | | SITE-03 | artplayer-vitepress
整理 i18n/文档/LLM 生成流程 | SITE-02 | build-i18n/build-docs/build-llm/trans-docs 的任务边界和错误处理 | 原命令兼容、生成可复现,翻译步骤不隐式运行远程服务;覆盖移动 loader 失败恢复 define、脚本依赖顺序、localhost/127.0.0.1 Run Code 目标和语言重定向 | M | todo | | SITE-04 | artplayer-vitepress
交叉核对逐包持续维护的文档 | CORE-21, SITE-03, PKG-CHAPTER-04, PKG-AMBILIGHT-04, PKG-AUDIO-04, PKG-AUTO-THUMB-04, PKG-VTT-THUMB-04, PKG-HLS-04, PKG-DASH-04, PKG-MULTI-SUB-04, PKG-JASSUB-04, PKG-MASK-04, PKG-ASR-04, PKG-ADS-04, PKG-VAST-04, PKG-CAST-04, PKG-DPIP-04, PKG-CANVAS-04, PKG-IFRAME-04, PKG-TOOL-THUMB-04, PKG-DANMUKU-06, PKG-MB-08 | 已随实现更新的中文/英文 API、包内实现地图、旧 JS 示例及已知能力限制的全包核对 | 未把缺环境的能力写成已验证,静态核对不等待设备任务;最终 demo 仍由 EX-03 验收;按 SITE-01 声明成员/候选标题清单逐项语义核对,补齐缺失的双语插件说明和 Danmuku 英文入口 | M | todo | | SITE-05 | artplayer-vitepress
构建文档站和验证链接/示例 | SITE-04, EX-01, EX-02, SITE-07 | VitePress 构建、链接与嵌入 demo 检查 | 文档构建、链接、嵌入路径与声明注入通过;真实完整 demo 保留 EX-03 独立门槛;核对 ENG-PM-01 登记的搜索 peer 范围和真实搜索行为 | M | todo | @@ -608,6 +608,7 @@ - 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) - SITE-01: [记录](site-inventory.md) [记录](baselines/site-inventory.json) [记录](baselines/site-provenance.json) [记录](baselines/demo-additions.json) [记录](changes/2026-09-14-SITE-01-site-inventory.md) [记录](baselines/site-inventory-validation.json) - SITE-SMOKE-01: [记录](changes/2026-09-14-SITE-SMOKE-01-documentation-smoke.md) [记录](baselines/docs-smoke-validation.json) [记录](scripts/docs-smoke.test.mjs) +- SITE-02: [记录](changes/2026-09-14-SITE-02-editor-declarations.md) [记录](baselines/editor-declarations-validation.json) [记录](changes/2026-09-14-SITE-SMOKE-01-documentation-smoke.md) - SITE-07: [记录](site-inventory.md) [记录](baselines/site-provenance.json) - EX-01: [记录](changes/2026-09-14-EX-01-react-consumer.md) [记录](baselines/react-consumer-validation.json) [记录](scripts/react-consumer.mjs) - EX-02: [记录](changes/2026-09-14-EX-02-vue-consumer.md) [记录](baselines/vue-consumer-validation.json) [记录](scripts/vue-consumer.mjs) diff --git a/refactor/progress.md b/refactor/progress.md index b7bd9b9f6..234567408 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,20 @@ # 进度与证据 +## SITE-02 编辑器声明生成链完成 + +build:ts 与 core/plugin 转换器完成 TS 模块拆分,原 JS/MJS 命令/导入路径保留。 +修复 Chapter/VAST 文本回退产生的冲突导出与 SDK 类型丢失;22 份实际加载声明 +在内存格式化后用主 TS 5.9.3/旧 TS 4.3.5 整组检查,通过才写文件。其余 20 份 +声明与起点一致,生产源码/公开类型/锁不变。VAST SDK 类型保留私有命名空间与 +原签名,生成上游 notices;新增只读 check:editor-types 接入 CI。4 项编辑器 +测试、522 项 baseline、50 项 CI 回归、严格工具链/工具类型/lint 通过(根 lint +1 条既有 warning)。三浏览器实际 Monaco 检查 22 份声明、拒绝坏参数并执行 +Chapter emit 的 ready/destroy,3 项通过。VAST 仅验类型,未执行广告。 +见[变更](changes/2026-09-14-SITE-02-editor-declarations.md)和 +[验证](baselines/editor-declarations-validation.json)。当前 240 项:166 done、 +15 doing、59 todo。下一步 SITE-03 站点/移动加载与编辑器流程;VAST 默认行为、 +Auto Thumbnail 首帧及插件最终验收保持开放。独立本地提交,无推送或发布。 + ## SITE-SMOKE-01 示例生成器与就绪检查完成 从 SITE-02 拆出文档示例工具,TS parser/generator/runtime 与严格检查的原 JS diff --git a/refactor/scripts/site-inventory.mjs b/refactor/scripts/site-inventory.mjs index 93a64fb5d..22ddb6d9f 100644 --- a/refactor/scripts/site-inventory.mjs +++ b/refactor/scripts/site-inventory.mjs @@ -78,7 +78,7 @@ export function captureSiteInventory() { pages: demo.pages, baselinePaths: { examplesAdded: demo.examples.filter(row => !historical.examples.some(old => old.source === row.source)).map(row => row.source), examplesRemoved: historical.examples.filter(row => !demo.examples.some(now => now.source === row.source)).map(row => row.source), htmlAdded: demo.pages.filter(row => !historical.pages.some(old => old.source === row.source)).map(row => row.source), htmlRemoved: historical.pages.filter(row => !demo.pages.some(now => now.source === row.source)).map(row => row.source) }, editorDeclarations: [...read('docs/assets/js/common.js').matchAll(/'\.\/assets\/ts\/([^']+\.d\.ts)'/g)].map(match => ({ file: `docs/assets/ts/${match[1]}`, exists: fs.existsSync(path.join(root, 'docs/assets/ts', match[1])), owner: 'SITE-02' })), - scripts: [...scriptFiles, ...files('scripts/docs-smoke').filter(file => file.endsWith('.ts'))].map(file => ({ file, sha256Lf: hash(read(file)) })), + scripts: [...scriptFiles, ...files('scripts/docs-smoke').filter(file => file.endsWith('.ts')), ...files('scripts/editor-declarations').filter(file => file.endsWith('.ts'))].map(file => ({ file, sha256Lf: hash(read(file)) })), generationCommands: Object.fromEntries(Object.entries(json('package.json').scripts).filter(([name]) => /^(?:build:(?:types|ts|test|i18n|docs|llm|all)|ci:build)$/.test(name))), siteManifest: json('packages/artplayer-vitepress/package.json'), assets, diff --git a/refactor/site-inventory.md b/refactor/site-inventory.md index 26d6875fa..404e596f1 100644 --- a/refactor/site-inventory.md +++ b/refactor/site-inventory.md @@ -3,7 +3,10 @@ SITE-SMOKE-01 后续已将 build:test 拆为 TS 解析、生成和浏览器运行模块,新增 确定性 examples.json 和 readonly check,替换 100ms done 为实例 ready/错误/清理。 以下 SITE-01 的旧生成器问题属于历史发现;当前维护入口见 -[docs-smoke README](../scripts/docs-smoke/README.md)。声明生成仍归 SITE-02。 +[docs-smoke README](../scripts/docs-smoke/README.md)。SITE-02 后续已将声明生成 +迁到 `scripts/editor-declarations/`:全部 22 份加载声明整组验证,Chapter/VAST +不再走删除 import 的文本回退。见[模块说明](../scripts/editor-declarations/README.md)。 +以下 SITE-01 对这些旧生成问题的描述是历史发现,站点/编辑器交互仍需后续验收。 SITE-01 在 `d62ab13a35c756ea567c426d4fbe00d893dd7e2b` 后核对当前源码。 可重跑清单见 [site-inventory.json](baselines/site-inventory.json);它登记 diff --git a/refactor/tasks.json b/refactor/tasks.json index d6908aea4..22e05dcdc 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -4138,11 +4138,15 @@ "ENG-06", "SITE-SMOKE-01" ], - "status": "todo", + "status": "done", "risk": "M", "deliverable": "build-ts/build-test 生成链的可验证 TS 脚本", "acceptance": "不靠字符串拼接掩盖声明错误,生成示例有真实断言或仅标 smoke;替换固定 100ms 成功判定,明确异步错误、清理和生成覆盖限制;坏代码块解析必须终止,覆盖 malformed 分支不推进 regexp 的回归", - "evidence": [] + "evidence": [ + "changes/2026-09-14-SITE-02-editor-declarations.md", + "baselines/editor-declarations-validation.json", + "changes/2026-09-14-SITE-SMOKE-01-documentation-smoke.md" + ] }, { "id": "SITE-03", diff --git a/scripts/build-ts.js b/scripts/build-ts.js index 1b2aeab3b..195132691 100644 --- a/scripts/build-ts.js +++ b/scripts/build-ts.js @@ -1,97 +1,4 @@ -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' -import { generateCoreEditorDeclaration } from './editor-types.mjs' -import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from './plugin-editor-types.mjs' +import { runEditorDeclarations } from './editor-declarations/generate.ts' -function ensureDirExists(filePath) { - const dir = path.dirname(filePath) - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }) - } -} - -function parsePluginInfo(pluginPath) { - // Use path.basename to handle both Unix and Windows paths correctly - const file = path.basename(pluginPath) - const baseName = file.replace('.d.ts', '') - - const classNames = { 'artplayer-tool-iframe': 'ArtplayerToolIframe', 'artplayer-tool-thumbnail': 'ArtplayerToolThumbnail' } - const name = classNames[baseName] - ? classNames[baseName] - : baseName - .split('-') - .map((word, index) => - index === 0 ? word : word[0].toUpperCase() + word.slice(1), - ) - .join('') - - return { name, file } -} - -const reg = /^import.*$/gim -const artplayerTSoutput = path.join('docs/assets/ts/artplayer.d.ts') -const code = generateCoreEditorDeclaration() -ensureDirExists(artplayerTSoutput) -fs.writeFileSync(artplayerTSoutput, code.trim()) -console.log(`✨ Built ${artplayerTSoutput}`); - -(async function () { - const packageOf = file => path.basename(path.dirname(path.dirname(file))) - const available = glob.sync('packages/artplayer-*-*/types/*.d.ts').filter(file => path.basename(file) === `${packageOf(file)}.d.ts`) - const selected = process.argv.slice(2) - for (const name of selected) - assert(available.some(file => packageOf(file) === name), `Unknown declaration package: ${name}`) - const pluginsTS = selected.length ? available.filter(file => selected.includes(packageOf(file))) : available - 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 = ['artplayerPluginAutoThumbnail', 'artplayerPluginJassub', 'artplayerPluginDanmukuMask', 'artplayerPluginDanmuku', 'artplayerPluginChromecast', 'artplayerPluginAsr', 'artplayerPluginMultipleSubtitles', 'artplayerPluginVttThumbnail', 'artplayerPluginHlsControl', 'artplayerPluginAudioTrack', 'artplayerPluginDashControl', 'artplayerPluginAds', 'artplayerPluginAmbilight', 'artplayerProxyCanvas', 'artplayerProxyMediabunny', 'artplayerPluginDocumentPip', 'ArtplayerToolIframe', 'ArtplayerToolThumbnail'].includes(name) - const localTypes = name === 'artplayerProxyMediabunny' ? { './media': fs.readFileSync(path.join(path.dirname(type), 'media.d.ts'), 'utf8') } : {} - const code = semanticPlugin - ? generatePluginEditorDeclaration(source, name, localTypes) - : `${source.replace(reg, '')}\nexport = ${name};\nexport as namespace ${name};\n` - if (semanticPlugin) { - const core = fs.readFileSync(artplayerTSoutput, 'utf8') - const diagnostics = [...checkPluginEditorDeclaration(code, core), ...checkPluginEditorDeclaration(code, core, '', compat)] - if (diagnostics.length) - throw new Error(`Invalid ${name} editor declaration: ${JSON.stringify(diagnostics)}`) - } - const output = path.join('docs/assets/ts', file) - ensureDirExists(output) - fs.writeFileSync(output, code.trim()) - console.log(`✨ Built ${output}`) - pluginFiles.push(file) - } - - pluginFiles.sort() - const languageFile = 'artplayer-i18n.d.ts' - fs.writeFileSync(path.join('docs/assets/ts', languageFile), `declare module 'artplayer/i18n/*' {\n const language: NonNullable\n export default language\n}\n`) - const allFiles = [...pluginFiles, 'artplayer.d.ts', languageFile] - const eslint = new ESLint({ fix: true, fixTypes: ['layout'] }) - const results = await eslint.lintFiles(allFiles.map(file => path.join('docs/assets/ts', file))) - await ESLint.outputFixes(results) - if (results.some(result => result.errorCount)) { - 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 ') - const newContent = commonJsContent.replace( - /let libUris = \[([\s\S]*?)\]/, - `let libUris = [\n ${newLibUris},\n ]`, - ) - ensureDirExists(commonJsPath) - fs.writeFileSync(commonJsPath, newContent) - console.log(`✨ Updated libUris in ${commonJsPath}`) -})() +await runEditorDeclarations(process.argv.slice(2)) diff --git a/scripts/editor-declarations/README.md b/scripts/editor-declarations/README.md new file mode 100644 index 000000000..58eed46fc --- /dev/null +++ b/scripts/editor-declarations/README.md @@ -0,0 +1,70 @@ +# Editor declarations + +Run `yarn build:ts` from the repository root using Node 24.21.0 and Yarn 1.22.22. +Existing declaration URLs and package selection arguments remain supported. +`yarn build:ts artplayer-plugin-chapter` selects that package plus shared core +and language declarations. `yarn check:editor-types` checks the complete output +without writing. Unknown/duplicate selections fail before generating or writing. + +| Module | Responsibility | +| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `../build-ts.js` | Existing CLI path, checked JavaScript shim | +| `generate.ts` | Discovery, names, formatting, whole-set validation, writes/checks and editor libUris | +| `core.ts` | Core public source bundling, constructor and named/generic aliases | +| `plugin.ts` | AST conversion of default exports, callable CommonJS namespaces, type aliases and Window augmentations | +| `syntax.ts` | Public compiler syntax diagnostics and supported dependency definition kinds | +| `dependencies.ts`, `vast-sdk.ts` | Type-only VAST SDK dependency closure; no advertising scripts | +| `validation.ts` | Standalone virtual compiler host, relative imports and rejection of hidden external dependencies | +| `../editor-types.mjs`, `../plugin-editor-types.mjs` | Existing tooling import paths forwarding to TypeScript | + +`yarn typecheck:docs-tools` checks these modules and the original JS/MJS shims. +Orchestration depends on conversion and validation; conversion does not write. +Public package declarations remain the source of truth. Do not hand-edit +`docs/assets/ts/*.d.ts`. + +All selected declarations are formatted in memory using layout-only ESLint fixes, +then checked together with TypeScript 5.9.3 and 4.3.5, without `skipLibCheck` or +ambient dependency discovery. This checks cross-package globals too. SDK methods +and type aliases retain upstream shapes; the generated SDK block has a narrowly +scoped lint exception for those two style rules. The two compiler packages have +nominally distinct AST types: the historical module is adapted at the boundary +and exercised as the real older compiler. Node native type stripping runs tools. + +Legacy MJS helpers retain synchronous functions and diagnostic shapes; existing +package consumer tests still use them. Core retains its existing bundler and +private definitions namespace. Plugins preserve callable/default aliases and +exported type namespaces. Unsupported imports/re-exports and invalid syntax fail. + +VAST's `Player` and `PlayerOptions` come from the installed, lockfile-pinned +`@glomex/vast-ima-player` 1.21.2 and `@alugha/ima` 2.1.0 declarations. A dedicated +type-only SDK entry avoids bundling a second copy of the linked workspace core. +SDK types stay in the plugin's module-private definitions namespace, not global +runtime values. The optional Window hook and required callback remain unchanged. +Adjacent generated `artplayer-plugin-vast.LICENSE.txt` preserves upstream notices. +This does not settle VAST's runtime default-behavior compatibility decision. + +The editor loads 22 declarations: core, 20 ecosystem libraries and i18n. The +unreferenced legacy `artplayer-plugin-websr.d.ts` asset is not in the `common.js` +library list and is not owned or deleted here. Monaco tests follow the actual +list. AST updates require exactly one array-valued `libUris` declaration; missing +or ambiguous declarations fail. Package-only builds leave that list alone. + +Generation validates all selected results before any writes, but does not provide +a multi-file filesystem transaction. `--check` normalizes CRLF for comparison, +writes nothing and rejects missing/stale output. No timestamps are generated. +Rebuild then check after changes to public types, SDK versions or formatting. + +```sh +yarn typecheck:docs-tools +yarn build:ts +yarn check:editor-types +node --test test/editor-types.test.js +yarn test:baseline +yarn test:browser test/browser/editor-declarations.spec.js --workers=1 +``` + +Browser coverage uses the actual repository Monaco assets, all loaded declarations, +positive/negative consumers and emitted Chapter code against controlled media. +VAST is type-checked only: no ad request or SDK/network/device acceptance. Full +editor UI, routes, example coverage and release reviews remain SITE-03/04/05, +EX-03 and release tasks. SITE-02 installs no new dependencies. diff --git a/scripts/editor-declarations/core.ts b/scripts/editor-declarations/core.ts new file mode 100644 index 000000000..b46896468 --- /dev/null +++ b/scripts/editor-declarations/core.ts @@ -0,0 +1,145 @@ +import assert from 'node:assert/strict' +import { createRequire } from 'node:module' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { generateDtsBundle } from 'dts-bundle-generator' +import ts from 'typescript' +import compat from 'typescript-compat' + +const root = fileURLToPath(new URL('../../', import.meta.url)) +const factory = ts.factory + +/** Put flattened definitions in a private scope so public aliases cannot refer to themselves. */ +export function asGlobalDeclaration(code: string, globalName: string) { + assert(/^[A-Z_$][\w$]*$/i.test(globalName), 'Invalid editor global name') + const source = ts.createSourceFile('bundle.d.ts', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS) + const exports = new Map() + const declarations = new Map() + const definitions: ts.Statement[] = [] + for (const node of source.statements) { + if (ts.isExportDeclaration(node)) { + assert(!node.moduleSpecifier && node.exportClause && ts.isNamedExports(node.exportClause), 'Editor bundle must contain no unresolved exports') + for (const item of node.exportClause.elements) + exports.set(item.name.text, (item.propertyName || item.name).text) + continue + } + assert(ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isClassDeclaration(node), `Unsupported editor declaration: ${ts.SyntaxKind[node.kind]}`) + assert(node.name, 'Editor definitions must be named') + const name = node.name.text + assert(!declarations.has(name), `Duplicate bundled declaration: ${name}`) + declarations.set(name, node) + if (node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) + exports.set(name, name) + definitions.push(factory.replaceModifiers(node, [factory.createModifier(ts.SyntaxKind.ExportKeyword)])) + } + const defaultName = exports.get('default') + const defaultDeclaration = defaultName && declarations.get(defaultName) + assert(defaultName && defaultDeclaration && ts.isClassDeclaration(defaultDeclaration), 'Core editor requires the actual default constructor class') + const definitionName = `${globalName}Definitions` + assert(!declarations.has(definitionName), 'Editor definition namespace collides with source') + const qualified = (name: string) => factory.createQualifiedName(factory.createIdentifier(definitionName), factory.createIdentifier(name)) + const aliases: ts.TypeAliasDeclaration[] = [] + for (const [name, local] of exports) { + if (name === 'default') + continue + const declaration = declarations.get(local) + assert(declaration, `Missing bundled export: ${local}`) + const parameters = declaration.typeParameters + aliases.push(factory.createTypeAliasDeclaration( + [factory.createModifier(ts.SyntaxKind.ExportKeyword)], + name, + parameters, + factory.createTypeReferenceNode(qualified(local), parameters?.map(parameter => factory.createTypeReferenceNode(parameter.name))), + )) + } + const namespace = (name: string, nodes: ts.Statement[]) => factory.createModuleDeclaration( + [factory.createModifier(ts.SyntaxKind.DeclareKeyword)], + factory.createIdentifier(name), + factory.createModuleBlock(nodes), + ts.NodeFlags.Namespace, + ) + const statements = [ + namespace(definitionName, definitions), + factory.createVariableStatement([factory.createModifier(ts.SyntaxKind.DeclareKeyword)], factory.createVariableDeclarationList([ + factory.createVariableDeclaration(globalName, undefined, factory.createTypeQueryNode(qualified(defaultName))), + ], ts.NodeFlags.Const)), + factory.createTypeAliasDeclaration(undefined, globalName, undefined, factory.createTypeReferenceNode(qualified(defaultName))), + namespace(globalName, aliases), + factory.createExportAssignment(undefined, true, factory.createIdentifier(globalName)), + factory.createNamespaceExportDeclaration(factory.createIdentifier(globalName)), + ] + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }) + return `// Generated from packages/artplayer/public/artplayer.ts by yarn build:ts. Do not edit.\n/* eslint-disable ts/no-redeclare, ts/no-namespace -- UMD constructor and named types share the global export. */\n${printer.printFile(factory.updateSourceFile(source, statements))}` +} + +export function generateCoreEditorDeclaration() { + const require = createRequire(import.meta.url) + const bundlerRequire = createRequire(require.resolve('dts-bundle-generator')) + assert.equal(bundlerRequire('typescript'), ts, 'Editor bundler must use the pinned workspace compiler') + const [code] = generateDtsBundle([{ + filePath: path.join(root, 'packages/artplayer/public/artplayer.ts'), + output: { exportReferencedTypes: false, noBanner: true }, + }], { preferredConfigPath: path.join(root, 'scripts/tsconfig.editor.json') }) + assert(code, 'Core bundle is empty') + const declaration = asGlobalDeclaration(code, 'Artplayer') + for (const compiler of [ts, compat]) + assert.deepEqual(checkCoreEditorDeclaration(declaration, compiler as unknown as typeof ts), [], `Invalid editor declaration: TS ${compiler.version}`) + return declaration +} + +export function checkCoreEditorDeclaration(code: string, compiler: typeof ts = ts) { + const folder = path.join(root, 'refactor/.cache/editor-semantic') + const declaration = path.join(folder, 'artplayer.d.ts') + const sources = new Map([ + [declaration, code], + [path.join(folder, 'global.ts'), ` +const option: Artplayer.OptionInput = { container: '#player' } +const player: Artplayer = new Artplayer(option, function (art) { + const same: Artplayer = this + const other: Artplayer = art + void [same, other] +}) +const factory: Artplayer.PluginFactory = function (art) { + return { name: String(art.id + this.id) } +} +const plugins: Promise = player.plugins.add(factory) +const emitter: Artplayer.Emitter<{ value: [number] }> = new Artplayer.Emitter() +emitter.on('value', count => count.toFixed()).emit('value', 2) +const language: NonNullable = { Play: 'Play' } +const configuration: Artplayer.Config = Artplayer.config +// @ts-expect-error Private flattened definitions must not leak into the editor global scope. +const leaked: ArtplayerDefinitions.Config = Artplayer.config +// @ts-expect-error Required container is not optional. +new Artplayer({ url: '' }) +// @ts-expect-error Named aliases must not degrade to any through circular definitions. +const invalidOption: Artplayer.Option = { container: 123, url: '' } +// @ts-expect-error Plugin callback results retain the selected result type. +const invalidFactory: Artplayer.PluginFactory = () => 'bad' +// @ts-expect-error Generic event payloads stay checked. +emitter.emit('value', 'bad') +void [plugins, language, configuration, invalidOption, invalidFactory, leaked] +`], + [path.join(folder, 'module.ts'), ` +import Player = require('./artplayer') +const player: Player = new Player({ container: '#player' }) +const option: Player.OptionInput = { container: '#player' } +// @ts-expect-error The UMD export is the constructor, not a default wrapper object. +Player.default +void [player, option] +`], + ]) + const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], module: compiler.ModuleKind.CommonJS, moduleResolution: compiler.ModuleResolutionKind.NodeJs } + const host = compiler.createCompilerHost(options) + const getSourceFile = host.getSourceFile.bind(host) + const exists = host.fileExists.bind(host) + const directoryExists = host.directoryExists?.bind(host) + host.fileExists = file => sources.has(path.resolve(file)) || exists(file) + host.directoryExists = directory => path.resolve(directory) === folder || !!directoryExists?.(directory) + host.getSourceFile = (file, version, onError, create) => sources.has(path.resolve(file)) + ? compiler.createSourceFile(file, sources.get(path.resolve(file))!, version, true) + : getSourceFile(file, version, onError, create) + const program = compiler.createProgram([...sources.keys()], options, host) + for (const file of program.getSourceFiles()) + assert(sources.has(path.resolve(file.fileName)) || program.isSourceFileDefaultLibrary(file), `Editor types escaped the standalone bundle: ${file.fileName}`) + return compiler.getPreEmitDiagnostics(program).map(diagnostic => ({ code: diagnostic.code, message: compiler.flattenDiagnosticMessageText(diagnostic.messageText, '\n') })) +} diff --git a/scripts/editor-declarations/dependencies.ts b/scripts/editor-declarations/dependencies.ts new file mode 100644 index 000000000..b9519f56d --- /dev/null +++ b/scripts/editor-declarations/dependencies.ts @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { generateDtsBundle } from 'dts-bundle-generator' +import ts from 'typescript' +import { parseDeclaration } from './syntax.ts' + +export function vastSdkDeclarations(): ts.Statement[] { + const root = fileURLToPath(new URL('../../', import.meta.url)) + const [code] = generateDtsBundle([{ + filePath: path.join(root, 'scripts/editor-declarations/vast-sdk.ts'), + libraries: { inlinedLibraries: ['@glomex/vast-ima-player', '@alugha/ima'], allowedTypesLibraries: [] }, + output: { noBanner: true, exportReferencedTypes: false }, + }], { preferredConfigPath: path.join(root, 'scripts/tsconfig.editor.json') }) + assert(code, 'SDK bundle is empty') + return [...parseDeclaration(code, 'vast-sdk.d.ts').statements].filter((node) => { + if (!ts.isExportDeclaration(node)) + return true + assert(!node.moduleSpecifier && node.exportClause && ts.isNamedExports(node.exportClause) + && node.exportClause.elements.length === 0, 'Unexpected SDK re-export') + return false + }) +} diff --git a/scripts/editor-declarations/generate.ts b/scripts/editor-declarations/generate.ts new file mode 100644 index 000000000..dbb69cfc5 --- /dev/null +++ b/scripts/editor-declarations/generate.ts @@ -0,0 +1,90 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import path from 'node:path' +import { ESLint } from 'eslint' +import { globSync } from 'glob' +import ts from 'typescript' +import compat from 'typescript-compat' +import { generateCoreEditorDeclaration } from './core.ts' +import { vastSdkDeclarations } from './dependencies.ts' +import { generatePluginEditorDeclaration } from './plugin.ts' +import { checkStandaloneDeclarations } from './validation.ts' + +export function pluginIdentity(file: string): { name: string, file: string, packageName: string } { + const packageName = path.basename(path.dirname(path.dirname(file))) + const names: Record = { 'artplayer-tool-iframe': 'ArtplayerToolIframe', 'artplayer-tool-thumbnail': 'ArtplayerToolThumbnail' } + const name = names[packageName] || packageName.split('-').map((word, index) => index ? word.charAt(0).toUpperCase() + word.slice(1) : word).join('') + return { name, file: path.basename(file), packageName } +} + +export function editorLibUris(code: string, files: string[]): string { + const source = ts.createSourceFile('common.js', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS) + const lists: ts.ArrayLiteralExpression[] = [] + const visit = (node: ts.Node): void => { + if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === 'libUris') { + assert(node.initializer && ts.isArrayLiteralExpression(node.initializer), 'libUris must be an array initializer') + lists.push(node.initializer) + } + ts.forEachChild(node, visit) + } + visit(source) + assert(lists.length === 1 && lists[0], 'Expected exactly one libUris declaration') + const list = lists[0] + const value = `[\n ${files.map(file => `'./assets/ts/${file}'`).join(',\n ')},\n ]` + return code.slice(0, list.getStart(source)) + value + code.slice(list.end) +} + +export async function generateEditorDeclarations(selected: string[] = []): Promise> { + const available = globSync('packages/artplayer-*-*/types/*.d.ts').filter(file => path.basename(file) === `${pluginIdentity(file).packageName}.d.ts`).sort() + for (const name of selected) + assert(available.some(file => pluginIdentity(file).packageName === name), `Unknown declaration package: ${name}`) + assert(new Set(selected).size === selected.length, 'Duplicate declaration package selection') + const sources = selected.length ? available.filter(file => selected.includes(pluginIdentity(file).packageName)) : available + const core = generateCoreEditorDeclaration() + const outputs = new Map([['docs/assets/ts/artplayer.d.ts', core]]) + const pluginFiles: string[] = [] + for (const sourceFile of sources) { + const { name, file, packageName } = pluginIdentity(sourceFile) + const localTypes: Record = packageName === 'artplayer-proxy-mediabunny' ? { './media': fs.readFileSync(path.join(path.dirname(sourceFile), 'media.d.ts'), 'utf8') } : {} + const dependencies = packageName === 'artplayer-plugin-vast' ? vastSdkDeclarations() : [] + const code = generatePluginEditorDeclaration(fs.readFileSync(sourceFile, 'utf8'), name, localTypes, dependencies) + outputs.set(path.join('docs/assets/ts', file), code) + pluginFiles.push(file) + } + const languageFile = 'artplayer-i18n.d.ts' + outputs.set(path.join('docs/assets/ts', languageFile), `declare module 'artplayer/i18n/*' {\n const language: NonNullable\n export default language\n}\n`) + const eslint = new ESLint({ fix: true, fixTypes: ['layout'] }) + for (const [file, code] of outputs) { + const [result] = await eslint.lintText(code, { filePath: file }) + assert(result && !result.errorCount, `Invalid formatted editor declaration ${file}: ${JSON.stringify(result?.messages)}`) + outputs.set(file, result.output || code) + } + // Both actual compilers check the formatted files together, including global collisions. + const declarations = new Map([...outputs].map(([file, code]) => [path.basename(file), code])) + for (const compiler of [ts, compat as unknown as typeof ts]) + assert.deepEqual(checkStandaloneDeclarations(declarations, compiler), [], `Invalid editor declarations (TS ${compiler.version})`) + if (!selected.length) { + const file = 'docs/assets/js/common.js' + outputs.set(file, editorLibUris(fs.readFileSync(file, 'utf8'), [...pluginFiles.sort(), 'artplayer.d.ts', languageFile])) + } + if (sources.some(file => pluginIdentity(file).packageName === 'artplayer-plugin-vast')) { + const notices = [['@glomex/vast-ima-player', 'LICENSE'], ['@alugha/ima', 'LICENSE.md']].map(([name, file]) => `${name}\n${fs.readFileSync(path.join('node_modules', name!, file!), 'utf8').replaceAll('\r\n', '\n').trim()}\n`).join('\n') + outputs.set('docs/assets/ts/artplayer-plugin-vast.LICENSE.txt', notices) + } + return outputs +} + +export async function runEditorDeclarations(args: string[]): Promise { + assert(args.every(arg => arg === '--check' || !arg.startsWith('-')), 'Use yarn build:ts [--check] [package ...]') + const outputs = await generateEditorDeclarations(args.filter(arg => arg !== '--check')) + for (const [file, code] of outputs) { + if (args.includes('--check')) { + assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), code.replaceAll('\r\n', '\n'), `Editor declaration drift: ${file}`) + } + else { + fs.mkdirSync(path.dirname(file), { recursive: true }) + fs.writeFileSync(file, code) + } + } + console.log(`Editor declarations ${args.includes('--check') ? 'checked' : 'generated'}: ${outputs.size} outputs; all selected declarations checked with current and compatibility compilers`) +} diff --git a/scripts/editor-declarations/plugin.ts b/scripts/editor-declarations/plugin.ts new file mode 100644 index 000000000..e9fab0dbd --- /dev/null +++ b/scripts/editor-declarations/plugin.ts @@ -0,0 +1,185 @@ +import assert from 'node:assert/strict' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import ts from 'typescript' +import { exportedDefinition, parseDeclaration } from './syntax.ts' + +// Migrate each package explicitly; unknown imports/exports must not disappear silently. +export function generatePluginEditorDeclaration(code: string, name: string, localTypes: Record = {}, dependencies: ts.Statement[] = []) { + assert(/^[a-z_$][\w$]*$/i.test(name), 'Invalid plugin global') + let source = parseDeclaration(code) + const expanded: ts.Statement[] = [] + for (const node of source.statements) { + if (!ts.isExportDeclaration(node) || !node.moduleSpecifier) { + expanded.push(node) + continue + } + assert(node.isTypeOnly, 'Unsupported plugin editor declaration: runtime re-export') + assert(ts.isStringLiteral(node.moduleSpecifier), 'Expected a literal type module') + const dependency = localTypes[node.moduleSpecifier.text] + assert(typeof dependency === 'string' && node.exportClause && ts.isNamedExports(node.exportClause), 'Unsupported editor type re-export') + const names = node.exportClause.elements.map((item) => { + assert(!item.propertyName, 'Renamed editor type re-export is unsupported') + return item.name.text + }) + const types = parseDeclaration(dependency, 'dependency.d.ts') + const definitions = types.statements.filter(item => !ts.isImportDeclaration(item)) + assert(definitions.every(item => ts.isInterfaceDeclaration(item) || ts.isTypeAliasDeclaration(item)), 'Editor dependencies must contain only types') + assert.deepEqual(definitions.map(item => item.name.text).sort(), names.sort(), 'Editor re-export must explicitly cover its local type definitions') + expanded.push(...types.statements) + } + // Reparse combined statements so comments and text positions belong to one file. + source = ts.createSourceFile('plugin.d.ts', expanded.map(node => ts.createPrinter().printNode(ts.EmitHint.Unspecified, node, node.getSourceFile())).join('\n'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS) + if (source.statements.some(node => ts.isExportAssignment(node) && node.isExportEquals)) + return generateCommonJSPluginEditor(source, name) + const factory = ts.factory + const definitions: ts.Statement[] = [] + const globals: ts.ModuleDeclaration[] = [] + const aliases: ts.TypeAliasDeclaration[] = [] + const internal = `${name}Definitions` + let exported = false + let callable = false + let classExport = false + for (const node of source.statements) { + if (ts.isImportDeclaration(node)) { + assert(ts.isStringLiteral(node.moduleSpecifier), 'Expected a literal import') + if (dependencies.length && node.moduleSpecifier.text === '@glomex/vast-ima-player') { + const bindings = node.importClause?.namedBindings + assert(node.importClause?.isTypeOnly && !node.importClause.name && bindings && ts.isNamedImports(bindings), 'Expected type-only SDK imports') + assert.deepEqual(bindings.elements.map((item) => { + assert(!item.propertyName) + return item.name.text + }).sort(), ['Player', 'PlayerOptions']) + continue + } + assert(node.importClause?.isTypeOnly && !node.importClause.namedBindings && node.importClause.name?.text === 'Artplayer' && node.moduleSpecifier.text === 'artplayer', 'Unsupported plugin editor import') + continue + } + if (ts.isModuleDeclaration(node)) { + assert(node.flags & ts.NodeFlags.GlobalAugmentation, 'Only explicit global augmentations are supported') + assert(node.body && ts.isModuleBlock(node.body) && node.body.statements.every(item => ts.isInterfaceDeclaration(item) && item.name.text === 'Window'), 'Only Window augmentations are supported') + globals.push(node) + continue + } + if (ts.isExportAssignment(node)) { + assert(!node.isExportEquals && ts.isIdentifier(node.expression) && node.expression.text === name, 'Unexpected plugin default export') + exported = true + continue + } + if (ts.isVariableStatement(node)) { + const values = node.declarationList.declarations + const value = values[0] + assert(values.length === 1 && value && ts.isIdentifier(value.name) && value.name.text === name && value.type && ts.isFunctionTypeNode(value.type), 'Unexpected plugin callable variable') + definitions.push(factory.replaceModifiers(node, [factory.createModifier(ts.SyntaxKind.ExportKeyword)])) + callable = true + continue + } + assert(ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node), 'Unsupported plugin editor declaration') + assert(node.name && node.name.text !== internal, 'Invalid plugin declaration name') + definitions.push(factory.replaceModifiers(node, [factory.createModifier(ts.SyntaxKind.ExportKeyword)])) + 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)) { + aliases.push(factory.createTypeAliasDeclaration( + [factory.createModifier(ts.SyntaxKind.ExportKeyword)], + node.name, + node.typeParameters, + factory.createTypeReferenceNode(factory.createQualifiedName(factory.createIdentifier(internal), node.name), node.typeParameters?.map(parameter => factory.createTypeReferenceNode(parameter.name))), + )) + } + } + assert(exported && callable, 'Plugin editor requires a default function or class') + const namespace = (identifier: string, statements: ts.Statement[]) => factory.createModuleDeclaration([factory.createModifier(ts.SyntaxKind.DeclareKeyword)], factory.createIdentifier(identifier), factory.createModuleBlock(statements), ts.NodeFlags.Namespace) + const statements = [ + ...globals, + 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)), + ] + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }) + const dependencySource = dependencies[0]?.getSourceFile() + assert(dependencies.every(node => node.getSourceFile() === dependencySource), 'SDK definitions must share one source file') + const dependencyNamespace = dependencySource + ? `/* eslint-disable ts/method-signature-style, ts/consistent-type-definitions -- Preserve upstream SDK declaration shapes. */\n${printer.printNode(ts.EmitHint.Unspecified, namespace(internal, dependencies.map(exportedDefinition)), dependencySource)}\n/* eslint-enable ts/method-signature-style, ts/consistent-type-definitions */\n` + : '' + return `// Generated from the package public declaration by yarn build:ts. Do not edit.\n/* eslint-disable ts/no-redeclare -- Callable and public type namespace intentionally merge. */\n${dependencyNamespace}${statements.map(node => printer.printNode(ts.EmitHint.Unspecified, node, source)).join('\n')}\n` +} + +function generateCommonJSPluginEditor(source: ts.SourceFile, name: string) { + const statements: ts.Statement[] = [] + let namespace = false + let callable = false + let exported = false + let global = false + let classExport = false + for (const node of source.statements) { + if (ts.isImportDeclaration(node)) { + assert(ts.isStringLiteral(node.moduleSpecifier) && node.importClause?.isTypeOnly && !node.importClause.namedBindings && node.importClause.name?.text === 'Artplayer' && node.moduleSpecifier.text === 'artplayer', 'Unsupported CommonJS plugin editor import') + continue + } + if (ts.isModuleDeclaration(node)) { + assert.equal(node.name.text, name, 'Unexpected plugin namespace') + assert(node.body && ts.isModuleBlock(node.body) && node.body.statements.every(item => ts.isInterfaceDeclaration(item) || ts.isTypeAliasDeclaration(item)), 'Editor namespace must contain only public type declarations') + namespace = true + } + else if (ts.isClassDeclaration(node)) { + assert.equal(node.name?.text, name, 'Unexpected CommonJS editor class') + callable = true + classExport = true + } + else if (ts.isVariableStatement(node)) { + const definitions = node.declarationList.declarations + assert.equal(definitions.length, 1) + const definition = definitions[0] + assert(definition && ts.isIdentifier(definition.name) && definition.type) + assert.equal(definition.name.text, name) + assert(ts.isTypeReferenceNode(definition.type) && ts.isQualifiedName(definition.type.typeName) + && ts.isIdentifier(definition.type.typeName.left) && definition.type.typeName.left.text === name && definition.type.typeName.right.text === 'Factory', 'Editor export must use its public Factory interface') + callable = true + } + else if (ts.isExportAssignment(node)) { + assert(node.isExportEquals && ts.isIdentifier(node.expression) && node.expression.text === name, 'Unexpected CommonJS plugin export') + exported = true + } + else if (ts.isNamespaceExportDeclaration(node)) { + assert.equal(node.name.text, name) + global = true + } + else { + assert.fail('Unsupported CommonJS plugin editor declaration') + } + statements.push(node) + } + assert(namespace && callable && exported && global, 'Incomplete CommonJS plugin editor declaration') + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }) + const bridge = classExport ? '' : '/* eslint-disable ts/no-redeclare -- Callable and public type namespace intentionally merge. */\n' + return `// Generated from the package public declaration by yarn build:ts. Do not edit.\n${bridge}${statements.map(node => printer.printNode(ts.EmitHint.Unspecified, node, source)).join('\n')}\n` +} + +export function checkPluginEditorDeclaration(code: string, core: string, consumer = '', compiler: typeof ts = ts) { + const folder = fileURLToPath(new URL('../../refactor/.cache/plugin-editor-semantic/', import.meta.url)) + const sources = new Map([ + [path.join(folder, 'artplayer.d.ts'), core], + [path.join(folder, 'plugin.d.ts'), code], + [path.join(folder, 'consumer.ts'), consumer], + ]) + const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], module: compiler.ModuleKind.CommonJS, moduleResolution: compiler.ModuleResolutionKind.NodeJs } + const host = compiler.createCompilerHost(options) + const getSourceFile = host.getSourceFile.bind(host) + host.getSourceFile = (file, version, onError, create) => sources.has(path.resolve(file)) + ? compiler.createSourceFile(file, sources.get(path.resolve(file))!, version, true) + : getSourceFile(file, version, onError, create) + const program = compiler.createProgram([...sources.keys()], options, host) + for (const file of program.getSourceFiles()) + assert(sources.has(path.resolve(file.fileName)) || program.isSourceFileDefaultLibrary(file), `Plugin editor types escaped the standalone bundle: ${file.fileName}`) + return compiler.getPreEmitDiagnostics(program).map(diagnostic => ({ code: diagnostic.code, message: compiler.flattenDiagnosticMessageText(diagnostic.messageText, '\n') })) +} diff --git a/scripts/editor-declarations/syntax.ts b/scripts/editor-declarations/syntax.ts new file mode 100644 index 000000000..9a2d58e00 --- /dev/null +++ b/scripts/editor-declarations/syntax.ts @@ -0,0 +1,18 @@ +import assert from 'node:assert/strict' +import ts from 'typescript' + +export function parseDeclaration(code: string, file = 'plugin.d.ts'): ts.SourceFile { + const source = ts.createSourceFile(file, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS) + const options: ts.CompilerOptions = { noLib: true, noResolve: true, types: [] } + const host = ts.createCompilerHost(options) + host.getSourceFile = name => name === file ? source : undefined + const program = ts.createProgram([file], options, host) + assert.equal(program.getSyntacticDiagnostics(source).length, 0, 'Invalid plugin declaration syntax') + return source +} + +export function exportedDefinition(node: ts.Statement): ts.Statement { + assert(ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isClassDeclaration(node) + || ts.isModuleDeclaration(node) || ts.isEnumDeclaration(node), 'Unsupported SDK declaration kind') + return ts.factory.replaceModifiers(node, [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)]) +} diff --git a/scripts/editor-declarations/validation.ts b/scripts/editor-declarations/validation.ts new file mode 100644 index 000000000..f955c4460 --- /dev/null +++ b/scripts/editor-declarations/validation.ts @@ -0,0 +1,24 @@ +import assert from 'node:assert/strict' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import ts from 'typescript' + +export function checkStandaloneDeclarations(files: Map, compiler: typeof ts = ts) { + const folder = path.resolve(fileURLToPath(new URL('../../refactor/.cache/editor-check/', import.meta.url))) + const sources = new Map([...files].map(([file, code]) => [path.join(folder, file), code])) + const options: ts.CompilerOptions = { strict: true, noEmit: true, skipLibCheck: false, types: [], target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], module: compiler.ModuleKind.CommonJS, moduleResolution: compiler.ModuleResolutionKind.NodeJs } + const host = compiler.createCompilerHost(options) + const getSourceFile = host.getSourceFile.bind(host) + const exists = host.fileExists.bind(host) + const directoryExists = host.directoryExists?.bind(host) + host.fileExists = file => sources.has(path.resolve(file)) || exists(file) + host.directoryExists = directory => path.resolve(directory) === folder || !!directoryExists?.(directory) + host.getSourceFile = (file, version, onError, create) => { + const code = sources.get(path.resolve(file)) + return code === undefined ? getSourceFile(file, version, onError, create) : compiler.createSourceFile(file, code, version, true) + } + const program = compiler.createProgram([...sources.keys()], options, host) + for (const file of program.getSourceFiles()) + assert(sources.has(path.resolve(file.fileName)) || program.isSourceFileDefaultLibrary(file), `Editor types escaped the standalone bundle: ${file.fileName}`) + return compiler.getPreEmitDiagnostics(program).map(diagnostic => ({ code: diagnostic.code, file: diagnostic.file && path.basename(diagnostic.file.fileName), message: compiler.flattenDiagnosticMessageText(diagnostic.messageText, '\n') })) +} diff --git a/scripts/editor-declarations/vast-sdk.ts b/scripts/editor-declarations/vast-sdk.ts new file mode 100644 index 000000000..e4452853e --- /dev/null +++ b/scripts/editor-declarations/vast-sdk.ts @@ -0,0 +1,2 @@ +// Types only: the editor does not fetch or execute the advertising SDK. +export type { Player, PlayerOptions } from '@glomex/vast-ima-player' diff --git a/scripts/editor-types.mjs b/scripts/editor-types.mjs index fc30cc8c3..1176b4c95 100644 --- a/scripts/editor-types.mjs +++ b/scripts/editor-types.mjs @@ -1,143 +1,2 @@ -import assert from 'node:assert/strict' -import { createRequire } from 'node:module' -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { generateDtsBundle } from 'dts-bundle-generator' -import ts from 'typescript' -import compat from 'typescript-compat' - -const root = fileURLToPath(new URL('../', import.meta.url)) -const factory = ts.factory - -/** Put flattened definitions in a private scope so public aliases cannot refer to themselves. */ -export function asGlobalDeclaration(code, globalName) { - assert(/^[A-Z_$][\w$]*$/i.test(globalName), 'Invalid editor global name') - const source = ts.createSourceFile('bundle.d.ts', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS) - const exports = new Map() - const declarations = new Map() - const definitions = [] - for (const node of source.statements) { - if (ts.isExportDeclaration(node)) { - assert(!node.moduleSpecifier && node.exportClause && ts.isNamedExports(node.exportClause), 'Editor bundle must contain no unresolved exports') - for (const item of node.exportClause.elements) - exports.set(item.name.text, (item.propertyName || item.name).text) - continue - } - assert(ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isClassDeclaration(node), `Unsupported editor declaration: ${ts.SyntaxKind[node.kind]}`) - assert(node.name, 'Editor definitions must be named') - const name = node.name.text - assert(!declarations.has(name), `Duplicate bundled declaration: ${name}`) - declarations.set(name, node) - if (node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) - exports.set(name, name) - definitions.push(factory.replaceModifiers(node, [factory.createModifier(ts.SyntaxKind.ExportKeyword)])) - } - const defaultName = exports.get('default') - assert(defaultName && ts.isClassDeclaration(declarations.get(defaultName)), 'Core editor requires the actual default constructor class') - const definitionName = `${globalName}Definitions` - assert(!declarations.has(definitionName), 'Editor definition namespace collides with source') - const qualified = name => factory.createQualifiedName(factory.createIdentifier(definitionName), factory.createIdentifier(name)) - const aliases = [] - for (const [name, local] of exports) { - if (name === 'default') - continue - const declaration = declarations.get(local) - assert(declaration, `Missing bundled export: ${local}`) - const parameters = declaration.typeParameters - aliases.push(factory.createTypeAliasDeclaration( - [factory.createModifier(ts.SyntaxKind.ExportKeyword)], - name, - parameters, - factory.createTypeReferenceNode(qualified(local), parameters?.map(parameter => factory.createTypeReferenceNode(parameter.name))), - )) - } - const namespace = (name, nodes) => factory.createModuleDeclaration( - [factory.createModifier(ts.SyntaxKind.DeclareKeyword)], - factory.createIdentifier(name), - factory.createModuleBlock(nodes), - ts.NodeFlags.Namespace, - ) - const statements = [ - namespace(definitionName, definitions), - factory.createVariableStatement([factory.createModifier(ts.SyntaxKind.DeclareKeyword)], factory.createVariableDeclarationList([ - factory.createVariableDeclaration(globalName, undefined, factory.createTypeQueryNode(qualified(defaultName))), - ], ts.NodeFlags.Const)), - factory.createTypeAliasDeclaration(undefined, globalName, undefined, factory.createTypeReferenceNode(qualified(defaultName))), - namespace(globalName, aliases), - factory.createExportAssignment(undefined, true, factory.createIdentifier(globalName)), - factory.createNamespaceExportDeclaration(factory.createIdentifier(globalName)), - ] - const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }) - return `// Generated from packages/artplayer/public/artplayer.ts by yarn build:ts. Do not edit.\n/* eslint-disable ts/no-redeclare, ts/no-namespace -- UMD constructor and named types share the global export. */\n${printer.printFile(factory.updateSourceFile(source, statements))}` -} - -export function generateCoreEditorDeclaration() { - const require = createRequire(import.meta.url) - const bundlerRequire = createRequire(require.resolve('dts-bundle-generator')) - assert.equal(bundlerRequire('typescript'), ts, 'Editor bundler must use the pinned workspace compiler') - const [code] = generateDtsBundle([{ - filePath: path.join(root, 'packages/artplayer/public/artplayer.ts'), - output: { exportReferencedTypes: false, noBanner: true }, - }], { preferredConfigPath: path.join(root, 'scripts/tsconfig.editor.json') }) - const declaration = asGlobalDeclaration(code, 'Artplayer') - for (const compiler of [ts, compat]) - assert.deepEqual(checkCoreEditorDeclaration(declaration, compiler), [], `Invalid editor declaration: TS ${compiler.version}`) - return declaration -} - -export function checkCoreEditorDeclaration(code, compiler = ts) { - const folder = path.join(root, 'refactor/.cache/editor-semantic') - const declaration = path.join(folder, 'artplayer.d.ts') - const sources = new Map([ - [declaration, code], - [path.join(folder, 'global.ts'), ` -const option: Artplayer.OptionInput = { container: '#player' } -const player: Artplayer = new Artplayer(option, function (art) { - const same: Artplayer = this - const other: Artplayer = art - void [same, other] -}) -const factory: Artplayer.PluginFactory = function (art) { - return { name: String(art.id + this.id) } -} -const plugins: Promise = player.plugins.add(factory) -const emitter: Artplayer.Emitter<{ value: [number] }> = new Artplayer.Emitter() -emitter.on('value', count => count.toFixed()).emit('value', 2) -const language: NonNullable = { Play: 'Play' } -const configuration: Artplayer.Config = Artplayer.config -// @ts-expect-error Private flattened definitions must not leak into the editor global scope. -const leaked: ArtplayerDefinitions.Config = Artplayer.config -// @ts-expect-error Required container is not optional. -new Artplayer({ url: '' }) -// @ts-expect-error Named aliases must not degrade to any through circular definitions. -const invalidOption: Artplayer.Option = { container: 123, url: '' } -// @ts-expect-error Plugin callback results retain the selected result type. -const invalidFactory: Artplayer.PluginFactory = () => 'bad' -// @ts-expect-error Generic event payloads stay checked. -emitter.emit('value', 'bad') -void [plugins, language, configuration, invalidOption, invalidFactory, leaked] -`], - [path.join(folder, 'module.ts'), ` -import Player = require('./artplayer') -const player: Player = new Player({ container: '#player' }) -const option: Player.OptionInput = { container: '#player' } -// @ts-expect-error The UMD export is the constructor, not a default wrapper object. -Player.default -void [player, option] -`], - ]) - const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], module: compiler.ModuleKind.CommonJS, moduleResolution: compiler.ModuleResolutionKind.NodeJs } - const host = compiler.createCompilerHost(options) - const getSourceFile = host.getSourceFile.bind(host) - const exists = host.fileExists.bind(host) - const directoryExists = host.directoryExists.bind(host) - host.fileExists = file => sources.has(path.resolve(file)) || exists(file) - host.directoryExists = directory => path.resolve(directory) === folder || directoryExists(directory) - host.getSourceFile = (file, version, onError, create) => sources.has(path.resolve(file)) - ? compiler.createSourceFile(file, sources.get(path.resolve(file)), version, true) - : getSourceFile(file, version, onError, create) - const program = compiler.createProgram([...sources.keys()], options, host) - for (const file of program.getSourceFiles()) - assert(sources.has(path.resolve(file.fileName)) || program.isSourceFileDefaultLibrary(file), `Editor types escaped the standalone bundle: ${file.fileName}`) - return compiler.getPreEmitDiagnostics(program).map(diagnostic => ({ code: diagnostic.code, message: compiler.flattenDiagnosticMessageText(diagnostic.messageText, '\n') })) -} +// Preserve existing tooling imports while implementation lives in checked TypeScript. +export { asGlobalDeclaration, checkCoreEditorDeclaration, generateCoreEditorDeclaration } from './editor-declarations/core.ts' diff --git a/scripts/plugin-editor-types.mjs b/scripts/plugin-editor-types.mjs index ef9955076..1f6119833 100644 --- a/scripts/plugin-editor-types.mjs +++ b/scripts/plugin-editor-types.mjs @@ -1,160 +1,2 @@ -import assert from 'node:assert/strict' -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import ts from 'typescript' - -// Migrate each package explicitly; unknown imports/exports must not disappear silently. -export function generatePluginEditorDeclaration(code, name, localTypes = {}) { - assert(/^[a-z_$][\w$]*$/i.test(name), 'Invalid plugin global') - let source = ts.createSourceFile('plugin.d.ts', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS) - assert.equal(source.parseDiagnostics.length, 0, 'Invalid plugin declaration syntax') - const expanded = [] - for (const node of source.statements) { - if (!ts.isExportDeclaration(node) || !node.moduleSpecifier) { - expanded.push(node) - continue - } - assert(node.isTypeOnly, 'Unsupported plugin editor declaration: runtime re-export') - const dependency = localTypes[node.moduleSpecifier.text] - assert(typeof dependency === 'string' && node.exportClause && ts.isNamedExports(node.exportClause), 'Unsupported editor type re-export') - const names = node.exportClause.elements.map((item) => { - assert(!item.propertyName, 'Renamed editor type re-export is unsupported') - return item.name.text - }) - const types = ts.createSourceFile('dependency.d.ts', dependency, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS) - assert.equal(types.parseDiagnostics.length, 0) - const definitions = types.statements.filter(item => !ts.isImportDeclaration(item)) - assert(definitions.every(item => ts.isInterfaceDeclaration(item) || ts.isTypeAliasDeclaration(item)), 'Editor dependencies must contain only types') - assert.deepEqual(definitions.map(item => item.name.text).sort(), names.sort(), 'Editor re-export must explicitly cover its local type definitions') - expanded.push(...types.statements) - } - // Reparse combined statements so comments and text positions belong to one file. - source = ts.createSourceFile('plugin.d.ts', expanded.map(node => ts.createPrinter().printNode(ts.EmitHint.Unspecified, node, node.getSourceFile())).join('\n'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS) - if (source.statements.some(node => ts.isExportAssignment(node) && node.isExportEquals)) - return generateCommonJSPluginEditor(source, name) - const factory = ts.factory - const definitions = [] - const aliases = [] - 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') - continue - } - if (ts.isExportAssignment(node)) { - assert(!node.isExportEquals && ts.isIdentifier(node.expression) && node.expression.text === name, 'Unexpected plugin default export') - exported = true - continue - } - if (ts.isVariableStatement(node)) { - const values = node.declarationList.declarations - assert(values.length === 1 && values[0].name.text === name && ts.isFunctionTypeNode(values[0].type), 'Unexpected plugin callable variable') - definitions.push(factory.replaceModifiers(node, [factory.createModifier(ts.SyntaxKind.ExportKeyword)])) - callable = true - continue - } - assert(ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node), 'Unsupported plugin editor declaration') - assert(node.name && node.name.text !== internal, 'Invalid plugin declaration name') - definitions.push(factory.replaceModifiers(node, [factory.createModifier(ts.SyntaxKind.ExportKeyword)])) - 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)) { - aliases.push(factory.createTypeAliasDeclaration( - [factory.createModifier(ts.SyntaxKind.ExportKeyword)], - node.name, - node.typeParameters, - factory.createTypeReferenceNode(factory.createQualifiedName(factory.createIdentifier(internal), node.name), node.typeParameters?.map(parameter => factory.createTypeReferenceNode(parameter.name))), - )) - } - } - 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)), - ] - const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }) - return `// Generated from the package public declaration by yarn build:ts. Do not edit.\n/* eslint-disable ts/no-redeclare -- Callable and public type namespace intentionally merge. */\n${statements.map(node => printer.printNode(ts.EmitHint.Unspecified, node, source)).join('\n')}\n` -} - -function generateCommonJSPluginEditor(source, name) { - const statements = [] - let namespace = false - let callable = false - let exported = false - let global = false - let classExport = false - for (const node of source.statements) { - if (ts.isImportDeclaration(node)) { - assert(node.importClause?.isTypeOnly && !node.importClause.namedBindings && node.importClause.name?.text === 'Artplayer' && node.moduleSpecifier.text === 'artplayer', 'Unsupported CommonJS plugin editor import') - continue - } - if (ts.isModuleDeclaration(node)) { - assert.equal(node.name.text, name, 'Unexpected plugin namespace') - assert(node.body && ts.isModuleBlock(node.body) && node.body.statements.every(item => ts.isInterfaceDeclaration(item) || ts.isTypeAliasDeclaration(item)), 'Editor namespace must contain only public type declarations') - namespace = true - } - else if (ts.isClassDeclaration(node)) { - assert.equal(node.name?.text, name, 'Unexpected CommonJS editor class') - callable = true - classExport = true - } - else if (ts.isVariableStatement(node)) { - const definitions = node.declarationList.declarations - assert.equal(definitions.length, 1) - const definition = definitions[0] - assert.equal(definition.name.text, name) - assert(ts.isTypeReferenceNode(definition.type) && ts.isQualifiedName(definition.type.typeName) - && definition.type.typeName.left.text === name && definition.type.typeName.right.text === 'Factory', 'Editor export must use its public Factory interface') - callable = true - } - else if (ts.isExportAssignment(node)) { - assert(node.isExportEquals && node.expression.text === name, 'Unexpected CommonJS plugin export') - exported = true - } - else if (ts.isNamespaceExportDeclaration(node)) { - assert.equal(node.name.text, name) - global = true - } - else { - assert.fail('Unsupported CommonJS plugin editor declaration') - } - statements.push(node) - } - assert(namespace && callable && exported && global, 'Incomplete CommonJS plugin editor declaration') - const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }) - const bridge = classExport ? '' : '/* eslint-disable ts/no-redeclare -- Callable and public type namespace intentionally merge. */\n' - return `// Generated from the package public declaration by yarn build:ts. Do not edit.\n${bridge}${statements.map(node => printer.printNode(ts.EmitHint.Unspecified, node, source)).join('\n')}\n` -} - -export function checkPluginEditorDeclaration(code, core, consumer = '', compiler = ts) { - const folder = fileURLToPath(new URL('../refactor/.cache/plugin-editor-semantic/', import.meta.url)) - const sources = new Map([ - [path.join(folder, 'artplayer.d.ts'), core], - [path.join(folder, 'plugin.d.ts'), code], - [path.join(folder, 'consumer.ts'), consumer], - ]) - const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], module: compiler.ModuleKind.CommonJS, moduleResolution: compiler.ModuleResolutionKind.NodeJs } - const host = compiler.createCompilerHost(options) - const getSourceFile = host.getSourceFile.bind(host) - host.getSourceFile = (file, version, onError, create) => sources.has(path.resolve(file)) - ? compiler.createSourceFile(file, sources.get(path.resolve(file)), version, true) - : getSourceFile(file, version, onError, create) - const program = compiler.createProgram([...sources.keys()], options, host) - for (const file of program.getSourceFiles()) - assert(sources.has(path.resolve(file.fileName)) || program.isSourceFileDefaultLibrary(file), `Plugin editor types escaped the standalone bundle: ${file.fileName}`) - return compiler.getPreEmitDiagnostics(program).map(diagnostic => ({ code: diagnostic.code, message: compiler.flattenDiagnosticMessageText(diagnostic.messageText, '\n') })) -} +// Preserve existing tooling imports while implementation lives in checked TypeScript. +export { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from './editor-declarations/plugin.ts' diff --git a/scripts/tsconfig.docs.json b/scripts/tsconfig.docs.json index ce96af84d..3ea8139cf 100644 --- a/scripts/tsconfig.docs.json +++ b/scripts/tsconfig.docs.json @@ -1,10 +1,23 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { - "lib": ["ES2021", "DOM", "DOM.Iterable"], - "types": ["node"], + "lib": [ + "ES2021", + "DOM", + "DOM.Iterable" + ], + "types": [ + "node" + ], "allowImportingTsExtensions": true, "checkJs": true }, - "include": ["docs-smoke/**/*.ts", "build-test.js"] + "include": [ + "docs-smoke/**/*.ts", + "build-test.js", + "build-ts.js", + "editor-declarations/**/*.ts", + "editor-types.mjs", + "plugin-editor-types.mjs" + ] } diff --git a/test/README.md b/test/README.md index c205d2b52..6840ced17 100644 --- a/test/README.md +++ b/test/README.md @@ -1,5 +1,10 @@ # Tests and fixture ownership +Editor tooling lives in `scripts/editor-declarations/`; see its README. +`test/editor-types.test.js` checks standalone globals and module consumers with +current/old compilers, Chapter/VAST regressions, generated files and library-list +failures. Package baseline tests continue to exercise the old MJS import paths. + Use the pinned Node/Yarn toolchain from `../refactor/toolchain-setup.md`. `yarn build:test` generates deterministic documentation readiness smoke and its diff --git a/test/browser/README.md b/test/browser/README.md index 17ea28654..aebac8c1e 100644 --- a/test/browser/README.md +++ b/test/browser/README.md @@ -1,5 +1,11 @@ # Browser regression entry +`editor-declarations.spec.js` loads the repository Monaco with the 22 declarations +actually listed in `common.js`, checks positive/negative consumers and runs emitted +Chapter code through ready/destroy with controlled media. VAST coverage is types +only, not advertising SDK execution. The unreferenced legacy WebSR declaration +asset is preserved; this test follows the actual editor's library list. + `docs-smoke.spec.js` verifies the documentation runner in three engines with actual core/media, controlled failures and three selected generated snippets. It checks pending readiness beyond the former 100ms timer, returned/unhandled diff --git a/test/browser/editor-declarations.spec.js b/test/browser/editor-declarations.spec.js new file mode 100644 index 000000000..67ef99ff1 --- /dev/null +++ b/test/browser/editor-declarations.spec.js @@ -0,0 +1,82 @@ +import fs from 'node:fs' +import { hash } from '../../refactor/scripts/releases.mjs' +import { expect, test } from './fixtures.js' + +test('Monaco checks all editor declarations together and runs the Chapter consumer', async ({ page }, testInfo) => { + const common = fs.readFileSync('docs/assets/js/common.js', 'utf8') + const list = common.match(/let libUris = \[([\s\S]*?)\]/)?.[1] || '' + const names = [...list.matchAll(/'\.\/assets\/ts\/([^']+\.d\.ts)'/g)].map(match => match[1]) + expect(names).toHaveLength(22) + await testInfo.attach('editor-declaration-inputs', { contentType: 'application/json', body: JSON.stringify(Object.fromEntries(names.map(name => [name, hash(fs.readFileSync(`docs/assets/ts/${name}`))]))) }) + await page.goto('/test/player.html?core=candidate&chapter=candidate') + await page.addScriptTag({ url: '/assets/js/vs/loader.js' }) + const result = await page.evaluate(async (names) => { + window.require.config({ paths: { vs: '/assets/js/vs' } }) + await new Promise((resolve, reject) => window.require(['vs/editor/editor.main'], resolve, reject)) + const api = window.monaco + const types = api.languages.typescript + types.typescriptDefaults.setCompilerOptions({ target: types.ScriptTarget.ES2020, strict: true, skipLibCheck: false, noEmit: false, types: [], allowNonTsExtensions: true }) + const libraries = await Promise.all(names.map(async (name) => { + const source = await (await fetch(`/assets/ts/${name}`)).text() + const uri = `file:///${name}` + return { uri, handle: types.typescriptDefaults.addExtraLib(source, uri) } + })) + const model = api.editor.createModel(` +const chapters: artplayerPluginChapter.Chapters = [{ start: 0, end: 2, title: 'Intro' }]; +const player = new Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, plugins: [artplayerPluginChapter({ chapters })] }); +const chapter = player.plugins.artplayerPluginChapter as artplayerPluginChapter.Result; +chapter.update({ chapters: [{ start: 0, end: 1, title: 'Updated' }] }); +const adOption: artplayerPluginVast.ArtplayerPluginVastOption = context => { + const volume: number = context.init().volume; + context.playerOptions.autoResize = true; + return Promise.resolve(); +}; +`, 'typescript', api.Uri.parse('file:///editor-consumer.ts')) + const invalid = api.editor.createModel(` +artplayerPluginVast(context => { context.init().volume = 'loud'; context.playerOptions.autoResize = 'yes'; }); +artplayerPluginChapter({ chapters: [{ start: '0', end: 2, title: '' }] }); +`, 'typescript', api.Uri.parse('file:///editor-invalid.ts')) + try { + const worker = await (await types.getTypeScriptWorker())(model.uri, invalid.uri) + const syntax = await worker.getSyntacticDiagnostics(model.uri.toString()) + const semantic = await worker.getSemanticDiagnostics(model.uri.toString()) + const declarations = (await Promise.all(libraries.map(lib => worker.getSemanticDiagnostics(lib.uri)))).flat() + const bad = (await worker.getSemanticDiagnostics(invalid.uri.toString())).map(item => item.code) + if (syntax.length || semantic.length || declarations.length) + return { syntax, semantic, declarations, bad } + const output = await worker.getEmitOutput(model.uri.toString()) + const script = output.outputFiles.find(file => file.name.endsWith('.js')) + if (!script) + throw new Error('Monaco did not emit the consumer') + // eslint-disable-next-line no-new-func -- Execute the actual editor output with local controlled media. + const player = new Function(`${script.text}\nreturn player`)() + try { + await new Promise((resolve, reject) => { + player.on('ready', resolve) + player.on('error', reject) + }) + const name = player.plugins.artplayerPluginChapter.name + const ready = player.isReady + player.destroy(true) + return { syntax, semantic, declarations, bad, name, ready, instances: window.Artplayer.instances.length } + } + finally { + if (!player.isDestroy) + player.destroy(true) + } + } + finally { + model.dispose() + invalid.dispose() + for (const library of libraries) library.handle.dispose() + } + }, names) + await testInfo.attach('editor-declaration-result', { contentType: 'application/json', body: JSON.stringify(result) }) + expect(result.syntax).toEqual([]) + expect(result.semantic).toEqual([]) + expect(result.declarations).toEqual([]) + expect(result.bad).toEqual([2322, 2322, 2322]) + expect(result.name).toBe('artplayerPluginChapter') + expect(result.ready).toBe(true) + expect(result.instances).toBe(0) +}) diff --git a/test/editor-types.test.js b/test/editor-types.test.js index d2a22cf3d..3ff607b79 100644 --- a/test/editor-types.test.js +++ b/test/editor-types.test.js @@ -4,6 +4,9 @@ import fs from 'node:fs' import { test } from 'node:test' import { ESLint } from 'eslint' import compat from 'typescript-compat' +import { vastSdkDeclarations } from '../scripts/editor-declarations/dependencies.ts' +import { editorLibUris, generateEditorDeclarations } from '../scripts/editor-declarations/generate.ts' +import { checkStandaloneDeclarations } from '../scripts/editor-declarations/validation.ts' import { asGlobalDeclaration, checkCoreEditorDeclaration, generateCoreEditorDeclaration } from '../scripts/editor-types.mjs' import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from '../scripts/plugin-editor-types.mjs' @@ -21,6 +24,74 @@ test('editor declarations are reproducible, standalone and preserve constructor/ assert.throws(() => asGlobalDeclaration('export { default } from "./missing"', 'Artplayer'), /unresolved exports/) }) +test('Chapter and VAST editor consumers preserve SDK types, optional Window hook and named results', () => { + const core = fs.readFileSync('docs/assets/ts/artplayer.d.ts', 'utf8') + const generated = new Map([['artplayer.d.ts', core]]) + for (const [suffix, global] of [['chapter', 'artplayerPluginChapter'], ['vast', 'artplayerPluginVast']]) { + const source = fs.readFileSync(`packages/artplayer-plugin-${suffix}/types/artplayer-plugin-${suffix}.d.ts`, 'utf8') + const old = `${source.replace(/^import.*$/gim, '')}\nexport = ${global};\nexport as namespace ${global};\n` + const diagnostics = checkPluginEditorDeclaration(old, core) + assert(diagnostics.some(item => item.code === 2309)) + if (suffix === 'vast') + assert(diagnostics.some(item => item.code === 2304 && item.message.includes('Player'))) + generated.set(`${suffix}.d.ts`, generatePluginEditorDeclaration(source, global, {}, suffix === 'vast' ? vastSdkDeclarations() : [])) + } + const consumer = ` +import chapter = require('./chapter'); +import vast = require('./vast'); +declare const art: Artplayer; +const chapters: chapter.Chapters = [{ start: 0, end: 10, title: 'Intro' }]; +const result: chapter.Result = chapter({ chapters })(art); +result.update({ chapters: [] }); +const option: vast.ArtplayerPluginVastOption = async context => { + const player = context.init(); + const volume: number = player.volume; + player.volume = 0.5; + player.pause(); + player.play(); + context.playUrl('ad.xml', { oldUntypedConfig: true }); + // @ts-expect-error SDK numeric property remains checked. + player.volume = 'loud'; + // @ts-expect-error SDK methods must not degrade to any. + player.missingMethod(); + // @ts-expect-error Private SDK implementation stays private. + player._setupIma(); + void volume; +}; +const instance: vast.ArtplayerPluginVastInstance = vast(option)(art); +const windowFactory: typeof vast | undefined = window.artplayerPluginVast; +// @ts-expect-error Historical required callback is still required. +vast(); +// @ts-expect-error Chapter timestamps stay numeric. +chapter({ chapters: [{ start: '0', end: 1, title: '' }] }); +// @ts-expect-error SDK internals are private to the declaration module. +declare const leaked: artplayerPluginVastDefinitions.Player; +void [result, instance, windowFactory, leaked]; +` + generated.set('global.ts', 'declare const globalArt: Artplayer; const globalResult: artplayerPluginChapter.Result = artplayerPluginChapter()(globalArt);') + for (const compiler of [undefined, compat]) { + generated.set('consumer.ts', consumer) + assert.deepEqual(checkStandaloneDeclarations(generated, compiler), []) + generated.set('consumer.ts', consumer.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')) + const errors = checkStandaloneDeclarations(generated, compiler) + assert.equal(errors.length, 6, 'Every intentional misuse must remain rejected') + } +}) + +test('all editor outputs are generated and checked together without modifying source declarations', async () => { + const outputs = await generateEditorDeclarations() + assert.equal([...outputs.keys()].filter(file => file.endsWith('.d.ts')).length, 22) + for (const [file, code] of outputs) + assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), code.replaceAll('\r\n', '\n'), `Stale editor output: ${file}`) + await assert.rejects(generateEditorDeclarations(['missing-plugin']), /Unknown declaration package/) + await assert.rejects(generateEditorDeclarations(['artplayer-plugin-chapter', 'artplayer-plugin-chapter']), /Duplicate/) + const source = 'const note = \'libUris\'; let libUris = [\'old\']; const untouched = 1' + assert.equal(editorLibUris(source, ['chapter.d.ts']), 'const note = \'libUris\'; let libUris = [\n \'./assets/ts/chapter.d.ts\',\n ]; const untouched = 1') + assert.throws(() => editorLibUris('const unrelated = []', []), /exactly one/) + assert.throws(() => editorLibUris('let libUris = []; function x() { let libUris = [] }', []), /exactly one/) + assert.throws(() => editorLibUris('let libUris = fetch()', []), /array initializer/) +}) + test('ASR editor generation supports named types without mixing default and export assignment', async () => { const source = fs.readFileSync('packages/artplayer-plugin-asr/types/artplayer-plugin-asr.d.ts', 'utf8') const core = fs.readFileSync('docs/assets/ts/artplayer.d.ts', 'utf8')