From bbe26dbdbb172fcb05750ff5d52c0b6f2a8b311d Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sun, 14 Jan 2024 08:41:40 -0800 Subject: [PATCH] chore: build import registry source (#28978) --- .eslintignore | 1 + .gitignore | 3 +- packages/playwright-ct-core/src/DEPS.list | 8 +- .../playwright-ct-core/src/importRegistry.ts | 59 --------------- .../src/injected/importRegistry.ts | 49 +++++++++++++ .../playwright-ct-core/src/injected/index.ts | 21 ++++++ .../src/injected/serializers.ts | 73 +++++++++++++++++++ packages/playwright-ct-core/src/mount.ts | 57 ++------------- packages/playwright-ct-core/src/vitePlugin.ts | 19 +---- .../playwright-ct-core/types/component.d.ts | 11 +-- utils/generate_injected.js | 46 +++++++++--- 11 files changed, 203 insertions(+), 144 deletions(-) delete mode 100644 packages/playwright-ct-core/src/importRegistry.ts create mode 100644 packages/playwright-ct-core/src/injected/importRegistry.ts create mode 100644 packages/playwright-ct-core/src/injected/index.ts create mode 100644 packages/playwright-ct-core/src/injected/serializers.ts diff --git a/.eslintignore b/.eslintignore index 38c19d93ec..9400c2637d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,6 +4,7 @@ test/assets/modernizr.js /packages/playwright-core/src/generated/* /packages/playwright-core/src/third_party/ /packages/playwright-core/types/* +/packages/playwright-ct-core/src/generated/* /index.d.ts utils/generate_types/overrides.d.ts utils/generate_types/test/test.ts diff --git a/.gitignore b/.gitignore index 0978f13e88..8902876690 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,8 @@ node_modules/ .vscode .idea yarn.lock -/packages/playwright-core/src/generated/* +/packages/playwright-core/src/generated +/packages/playwright-ct-core/src/generated packages/*/lib/ drivers/ .android-sdk/ diff --git a/packages/playwright-ct-core/src/DEPS.list b/packages/playwright-ct-core/src/DEPS.list index dec82b04be..c45c7c3a7b 100644 --- a/packages/playwright-ct-core/src/DEPS.list +++ b/packages/playwright-ct-core/src/DEPS.list @@ -1,2 +1,6 @@ -[importRegistry.ts] -../types/** +[vitePlugin.ts] +generated/indexSource.ts + +[mount.ts] +generated/serializers.ts +injected/** diff --git a/packages/playwright-ct-core/src/importRegistry.ts b/packages/playwright-ct-core/src/importRegistry.ts deleted file mode 100644 index 641e73991c..0000000000 --- a/packages/playwright-ct-core/src/importRegistry.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * 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. - */ - -import type { ImportRef } from '../types/component'; - -export class ImportRegistry { - private _registry = new Map Promise>(); - - initialize(components: Record Promise>) { - for (const [name, value] of Object.entries(components)) - this._registry.set(name, value); - } - - async resolveImports(value: any): Promise { - if (value === null || typeof value !== 'object') - return value; - if (this._isImportRef(value)) { - const importFunction = this._registry.get(value.id); - if (!importFunction) - throw new Error(`Unregistered component: ${value.id}. Following components are registered: ${[...this._registry.keys()]}`); - let importedObject = await importFunction(); - if (!importedObject) - throw new Error(`Could not resolve component: ${value.id}.`); - if (value.property) { - importedObject = importedObject[value.property]; - if (!importedObject) - throw new Error(`Could not instantiate component: ${value.id}.${value.property}.`); - } - return importedObject; - } - if (Array.isArray(value)) { - const result = []; - for (const item of value) - result.push(await this.resolveImports(item)); - return result; - } - const result: any = {}; - for (const [key, prop] of Object.entries(value)) - result[key] = await this.resolveImports(prop); - return result; - } - - private _isImportRef(value: any): value is ImportRef { - return typeof value === 'object' && value && value.__pw_type === 'importRef'; - } -} diff --git a/packages/playwright-ct-core/src/injected/importRegistry.ts b/packages/playwright-ct-core/src/injected/importRegistry.ts new file mode 100644 index 0000000000..138d31ce05 --- /dev/null +++ b/packages/playwright-ct-core/src/injected/importRegistry.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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. + */ + +export type ImportRef = { + __pw_type: 'importRef', + id: string, + property?: string, +}; + +export function isImportRef(value: any): value is ImportRef { + return typeof value === 'object' && value && value.__pw_type === 'importRef'; +} + +export class ImportRegistry { + private _registry = new Map Promise>(); + + initialize(components: Record Promise>) { + for (const [name, value] of Object.entries(components)) + this._registry.set(name, value); + } + + async resolveImportRef(importRef: ImportRef): Promise { + const importFunction = this._registry.get(importRef.id); + if (!importFunction) + throw new Error(`Unregistered component: ${importRef.id}. Following components are registered: ${[...this._registry.keys()]}`); + let importedObject = await importFunction(); + if (!importedObject) + throw new Error(`Could not resolve component: ${importRef.id}.`); + if (importRef.property) { + importedObject = importedObject[importRef.property]; + if (!importedObject) + throw new Error(`Could not instantiate component: ${importRef.id}.${importRef.property}.`); + } + return importedObject; + } +} diff --git a/packages/playwright-ct-core/src/injected/index.ts b/packages/playwright-ct-core/src/injected/index.ts new file mode 100644 index 0000000000..e81d4ff83c --- /dev/null +++ b/packages/playwright-ct-core/src/injected/index.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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. + */ + +import { ImportRegistry } from './importRegistry'; +import { unwrapObject } from './serializers'; + +window.__pwRegistry = new ImportRegistry(); +window.__pwUnwrapObject = unwrapObject; diff --git a/packages/playwright-ct-core/src/injected/serializers.ts b/packages/playwright-ct-core/src/injected/serializers.ts new file mode 100644 index 0000000000..41ae72c489 --- /dev/null +++ b/packages/playwright-ct-core/src/injected/serializers.ts @@ -0,0 +1,73 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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. + */ + +import { isImportRef } from './importRegistry'; + +type FunctionRef = { + __pw_type: 'function'; + ordinal: number; +}; + +function isFunctionRef(value: any): value is FunctionRef { + return value && typeof value === 'object' && value.__pw_type === 'function'; +} + +export function wrapObject(value: any, callbacks: Function[]): any { + if (typeof value === 'function') { + const ordinal = callbacks.length; + callbacks.push(value as Function); + const result: FunctionRef = { + __pw_type: 'function', + ordinal, + }; + return result; + } + if (value === null || typeof value !== 'object') + return value; + if (Array.isArray(value)) { + const result = []; + for (const item of value) + result.push(wrapObject(item, callbacks)); + return result; + } + const result: any = {}; + for (const [key, prop] of Object.entries(value)) + result[key] = wrapObject(prop, callbacks); + return result; +} + +export async function unwrapObject(value: any): Promise { + if (value === null || typeof value !== 'object') + return value; + if (isFunctionRef(value)) { + return (...args: any[]) => { + window.__ctDispatchFunction(value.ordinal, args); + }; + } + if (isImportRef(value)) + return window.__pwRegistry.resolveImportRef(value); + + if (Array.isArray(value)) { + const result = []; + for (const item of value) + result.push(await unwrapObject(item)); + return result; + } + const result: any = {}; + for (const [key, prop] of Object.entries(value)) + result[key] = await unwrapObject(prop); + return result; +} diff --git a/packages/playwright-ct-core/src/mount.ts b/packages/playwright-ct-core/src/mount.ts index 0dcf0bcde8..e8ee9272ac 100644 --- a/packages/playwright-ct-core/src/mount.ts +++ b/packages/playwright-ct-core/src/mount.ts @@ -15,8 +15,10 @@ */ import type { Fixtures, Locator, Page, BrowserContextOptions, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, BrowserContext } from 'playwright/test'; -import type { Component, ImportRef, JsxComponent, MountOptions, ObjectComponentOptions } from '../types/component'; +import type { Component, JsxComponent, MountOptions, ObjectComponentOptions } from '../types/component'; import type { ContextReuseMode, FullConfigInternal } from '../../playwright/src/common/config'; +import type { ImportRef } from './injected/importRegistry'; +import { wrapObject } from './injected/serializers'; let boundCallbacksForMount: Function[] = []; @@ -46,7 +48,7 @@ export const fixtures: Fixtures if (!((info as any)._configInternal as FullConfigInternal).defineConfigWasUsed) throw new Error('Component testing requires the use of the defineConfig() in your playwright-ct.config.{ts,js}: https://aka.ms/playwright/ct-define-config'); await (page as any)._wrapApiCall(async () => { - await page.exposeFunction('__ct_dispatch', (ordinal: number, args: any[]) => { + await page.exposeFunction('__ctDispatchFunction', (ordinal: number, args: any[]) => { boundCallbacksForMount[ordinal](...args); }); await page.goto(process.env.PLAYWRIGHT_TEST_BASE_URL!); @@ -83,59 +85,29 @@ function isJsxComponent(component: any): component is JsxComponent { } async function innerUpdate(page: Page, componentRef: JsxComponent | ImportRef, options: ObjectComponentOptions = {}): Promise { - const component = createComponent(componentRef, options); - wrapFunctions(component, page, boundCallbacksForMount); + const component = wrapObject(createComponent(componentRef, options), boundCallbacksForMount); await page.evaluate(async ({ component }) => { - const unwrapFunctions = (object: any) => { - for (const [key, value] of Object.entries(object)) { - if (typeof value === 'string' && (value as string).startsWith('__pw_func_')) { - const ordinal = +value.substring('__pw_func_'.length); - object[key] = (...args: any[]) => { - (window as any)['__ct_dispatch'](ordinal, args); - }; - } else if (typeof value === 'object' && value) { - unwrapFunctions(value); - } - } - }; - - unwrapFunctions(component); - component = await window.__pwRegistry.resolveImports(component); + component = await window.__pwUnwrapObject(component); const rootElement = document.getElementById('root')!; return await window.playwrightUpdate(rootElement, component); }, { component }); } async function innerMount(page: Page, componentRef: JsxComponent | ImportRef, options: ObjectComponentOptions & MountOptions = {}): Promise { - const component = createComponent(componentRef, options); - wrapFunctions(component, page, boundCallbacksForMount); + const component = wrapObject(createComponent(componentRef, options), boundCallbacksForMount); // WebKit does not wait for deferred scripts. await page.waitForFunction(() => !!window.playwrightMount); const selector = await page.evaluate(async ({ component, hooksConfig }) => { - const unwrapFunctions = (object: any) => { - for (const [key, value] of Object.entries(object)) { - if (typeof value === 'string' && (value as string).startsWith('__pw_func_')) { - const ordinal = +value.substring('__pw_func_'.length); - object[key] = (...args: any[]) => { - (window as any)['__ct_dispatch'](ordinal, args); - }; - } else if (typeof value === 'object' && value) { - unwrapFunctions(value); - } - } - }; - - unwrapFunctions(component); + component = await window.__pwUnwrapObject(component); let rootElement = document.getElementById('root'); if (!rootElement) { rootElement = document.createElement('div'); rootElement.id = 'root'; document.body.appendChild(rootElement); } - component = await window.__pwRegistry.resolveImports(component); await window.playwrightMount(component, rootElement, hooksConfig); return '#root >> internal:control=component'; @@ -152,16 +124,3 @@ function createComponent(component: JsxComponent | ImportRef, options: ObjectCom ...options, }; } - -function wrapFunctions(object: any, page: Page, callbacks: Function[]) { - for (const [key, value] of Object.entries(object)) { - const type = typeof value; - if (type === 'function') { - const functionName = '__pw_func_' + callbacks.length; - callbacks.push(value as Function); - object[key] = functionName; - } else if (type === 'object' && value) { - wrapFunctions(value, page, callbacks); - } - } -} diff --git a/packages/playwright-ct-core/src/vitePlugin.ts b/packages/playwright-ct-core/src/vitePlugin.ts index 82022d1860..1451794160 100644 --- a/packages/playwright-ct-core/src/vitePlugin.ts +++ b/packages/playwright-ct-core/src/vitePlugin.ts @@ -32,6 +32,7 @@ import { getPlaywrightVersion } from 'playwright-core/lib/utils'; import { setExternalDependencies } from 'playwright/lib/transform/compilationCache'; import { collectComponentUsages, importInfo } from './tsxTransform'; import { version as viteVersion, build, preview, mergeConfig } from 'vite'; +import { source as injectedSource } from './generated/indexSource'; import type { ImportInfo } from './tsxTransform'; const log = debug('pw:vite'); @@ -115,13 +116,7 @@ export function createPlugin( let buildExists = false; let buildInfo: BuildInfo; - const importRegistryFile = await fs.promises.readFile(path.resolve(__dirname, 'importRegistry.js'), 'utf-8'); - assert(importRegistryFile.includes(importRegistryPrefix)); - assert(importRegistryFile.includes(importRegistrySuffix)); - const importRegistrySource = importRegistryFile.replace(importRegistryPrefix, '').replace(importRegistrySuffix, '') + ` - window.__pwRegistry = new ImportRegistry(); - `; - const registerSource = importRegistrySource + await fs.promises.readFile(registerSourceFile, 'utf-8'); + const registerSource = injectedSource + '\n' + await fs.promises.readFile(registerSourceFile, 'utf-8'); const registerSourceHash = calculateSha1(registerSource); try { @@ -436,13 +431,3 @@ function hasJSComponents(components: ComponentInfo[]): boolean { } return false; } - - -const importRegistryPrefix = `"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.ImportRegistry = void 0;`; - -const importRegistrySuffix = `exports.ImportRegistry = ImportRegistry;`; diff --git a/packages/playwright-ct-core/types/component.d.ts b/packages/playwright-ct-core/types/component.d.ts index 9de92f9fde..38ce1bfe2a 100644 --- a/packages/playwright-ct-core/types/component.d.ts +++ b/packages/playwright-ct-core/types/component.d.ts @@ -14,19 +14,13 @@ * limitations under the License. */ -import type { ImportRegistry } from '../src/importRegistry'; +import type { ImportRegistry } from '../src/injected/importRegistry'; type JsonPrimitive = string | number | boolean | null; type JsonValue = JsonPrimitive | JsonObject | JsonArray; type JsonArray = JsonValue[]; export type JsonObject = { [Key in string]?: JsonValue }; -export type ImportRef = { - __pw_type: 'importRef', - id: string, - property?: string, -}; - export type JsxComponent = { __pw_type: 'jsx', type: any, @@ -62,5 +56,8 @@ declare global { params: { hooksConfig?: HooksConfig; [key: string]: any } ) => Promise)[]; __pwRegistry: ImportRegistry; + // Can't start with __pw due to core reuse bindings logic for __pw*. + __ctDispatchFunction: (ordinal: number, args: any[]) => void; + __pwUnwrapObject: (value: any) => Promise; } } diff --git a/utils/generate_injected.js b/utils/generate_injected.js index 9fefed0d4d..c07d9a4877 100644 --- a/utils/generate_injected.js +++ b/utils/generate_injected.js @@ -22,11 +22,40 @@ const path = require('path'); const ROOT = path.join(__dirname, '..'); const esbuild = require('esbuild'); +/** + * @type {[string, string, string, boolean][]} + */ const injectedScripts = [ - path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'utilityScript.ts'), - path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'injectedScript.ts'), - path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'consoleApi.ts'), - path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'recorder.ts'), + [ + path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'utilityScript.ts'), + path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed'), + path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), + true, + ], + [ + path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'injectedScript.ts'), + path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed'), + path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), + true, + ], + [ + path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'consoleApi.ts'), + path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed'), + path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), + true, + ], + [ + path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'recorder.ts'), + path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed'), + path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), + true, + ], + [ + path.join(ROOT, 'packages', 'playwright-ct-core', 'src', 'injected', 'index.ts'), + path.join(ROOT, 'packages', 'playwright-ct-core', 'lib', 'injected', 'packed'), + path.join(ROOT, 'packages', 'playwright-ct-core', 'src', 'generated'), + false, + ] ]; const modulePrefix = ` @@ -79,10 +108,8 @@ const inlineCSSPlugin = { }; (async () => { - const generatedFolder = path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'); - await fs.promises.mkdir(generatedFolder, { recursive: true }); - for (const injected of injectedScripts) { - const outdir = path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed'); + for (const [injected, outdir, generatedFolder, hasExports] of injectedScripts) { + await fs.promises.mkdir(generatedFolder, { recursive: true }); const buildOutput = await esbuild.build({ entryPoints: [injected], bundle: true, @@ -97,7 +124,8 @@ const inlineCSSPlugin = { const baseName = path.basename(injected); const outFileJs = path.join(outdir, baseName.replace('.ts', '.js')); let content = await fs.promises.readFile(outFileJs, 'utf-8'); - content = await replaceEsbuildHeader(content, outFileJs); + if (hasExports) + content = await replaceEsbuildHeader(content, outFileJs); const newContent = `export const source = ${JSON.stringify(content)};`; await fs.promises.writeFile(path.join(generatedFolder, baseName.replace('.ts', 'Source.ts')), newContent); }