chore: migrate injected scripts to esbuild (#13143)

This commit is contained in:
Pavel Feldman 2022-03-28 22:10:17 -08:00 committed by GitHub
parent de0af27837
commit 1961959dcb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 175 additions and 3226 deletions

3098
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -65,7 +65,6 @@
"@types/resize-observer-browser": "^0.1.6", "@types/resize-observer-browser": "^0.1.6",
"@types/rimraf": "^3.0.2", "@types/rimraf": "^3.0.2",
"@types/source-map-support": "^0.5.4", "@types/source-map-support": "^0.5.4",
"@types/webpack": "^5.28.0",
"@types/ws": "8.2.2", "@types/ws": "8.2.2",
"@types/xml2js": "^0.4.9", "@types/xml2js": "^0.4.9",
"@types/yazl": "^2.4.2", "@types/yazl": "^2.4.2",
@ -74,22 +73,17 @@
"@vitejs/plugin-react": "^1.0.7", "@vitejs/plugin-react": "^1.0.7",
"@zip.js/zip.js": "^2.4.2", "@zip.js/zip.js": "^2.4.2",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",
"babel-loader": "^8.2.3",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"commonmark": "^0.30.0", "commonmark": "^0.30.0",
"concurrently": "^6.2.1", "concurrently": "^6.2.1",
"copy-webpack-plugin": "^9.1.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "^6.5.1",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"electron": "^12.2.1", "electron": "^12.2.1",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
"eslint": "^8.8.0", "eslint": "^8.8.0",
"eslint-plugin-notice": "^0.9.10", "eslint-plugin-notice": "^0.9.10",
"eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-react-hooks": "^4.3.0",
"file-loader": "^6.2.0",
"formidable": "^2.0.1", "formidable": "^2.0.1",
"html-webpack-plugin": "^5.5.0",
"mime": "^3.0.0", "mime": "^3.0.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"node-stream-zip": "^1.15.0", "node-stream-zip": "^1.15.0",
@ -97,11 +91,8 @@
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"socksv5": "0.0.6", "socksv5": "0.0.6",
"style-loader": "^3.3.1",
"typescript": "^4.5.5", "typescript": "^4.5.5",
"vite": "^2.8.0", "vite": "^2.8.0",
"webpack": "^5.68.0",
"webpack-cli": "^4.9.2",
"xml2js": "^0.4.23", "xml2js": "^0.4.23",
"yaml": "^1.10.2" "yaml": "^1.10.2"
} }

View file

@ -113,7 +113,7 @@ export function serializeValue(value: any, handleSerializer: (value: any) => Han
return { s: value }; return { s: value };
if (isError(value)) { if (isError(value)) {
const error = value; const error = value;
if ('captureStackTrace' in global.Error) { if ('captureStackTrace' in globalThis.Error) {
// v8 // v8
return { s: error.stack || '' }; return { s: error.stack || '' };
} }

View file

@ -110,7 +110,7 @@ function serialize(value: any, handleSerializer: (value: any) => HandleOrValue,
if (isError(value)) { if (isError(value)) {
const error = value; const error = value;
if ('captureStackTrace' in global.Error) { if ('captureStackTrace' in globalThis.Error) {
// v8 // v8
return error.stack || ''; return error.stack || '';
} }

View file

@ -99,8 +99,9 @@ export class FrameExecutionContext extends js.ExecutionContext {
custom.push(`{ name: '${name}', engine: (${source}) }`); custom.push(`{ name: '${name}', engine: (${source}) }`);
const source = ` const source = `
(() => { (() => {
const module = {};
${injectedScriptSource.source} ${injectedScriptSource.source}
return new pwExport( return new module.exports(
${isUnderTest()}, ${isUnderTest()},
${this.frame._page._delegate.rafCountForStablePosition()}, ${this.frame._page._delegate.rafCountForStablePosition()},
"${this.frame._page._browserContext._browser.options.name}", "${this.frame._page._browserContext._browser.options.name}",

View file

@ -31,7 +31,7 @@ import { assert, constructURLBasedOnBaseURL, makeWaitForNextTask } from '../util
import { ManualPromise } from '../utils/async'; import { ManualPromise } from '../utils/async';
import { debugLogger } from '../utils/debugLogger'; import { debugLogger } from '../utils/debugLogger';
import { CallMetadata, serverSideCallMetadata, SdkObject } from './instrumentation'; import { CallMetadata, serverSideCallMetadata, SdkObject } from './instrumentation';
import type InjectedScript from './injected/injectedScript'; import { type InjectedScript } from './injected/injectedScript';
import type { ElementStateWithoutStable, FrameExpectParams, InjectedScriptPoll, InjectedScriptProgress } from './injected/injectedScript'; import type { ElementStateWithoutStable, FrameExpectParams, InjectedScriptPoll, InjectedScriptProgress } from './injected/injectedScript';
import { isSessionClosedError } from './protocolError'; import { isSessionClosedError } from './protocolError';
import { isInvalidSelectorError, splitSelectorByFrame, stringifySelector, ParsedSelector } from './common/selectorParser'; import { isInvalidSelectorError, splitSelectorByFrame, stringifySelector, ParsedSelector } from './common/selectorParser';

View file

@ -121,7 +121,7 @@ export class InjectedScript {
} }
eval(expression: string): any { eval(expression: string): any {
return global.eval(expression); return globalThis.eval(expression);
} }
parseSelector(selector: string): ParsedSelector { parseSelector(selector: string): ParsedSelector {
@ -303,10 +303,11 @@ export class InjectedScript {
} }
extend(source: string, params: any): any { extend(source: string, params: any): any {
const constrFunction = global.eval(` const constrFunction = globalThis.eval(`
(() => { (() => {
const module = {};
${source} ${source}
return pwExport; return module.exports;
})()`); })()`);
return new constrFunction(this, params); return new constrFunction(this, params);
} }
@ -1257,4 +1258,4 @@ function deepEquals(a: any, b: any): boolean {
return false; return false;
} }
export default InjectedScript; module.exports = InjectedScript;

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import type InjectedScript from './injectedScript'; import { type InjectedScript } from './injectedScript';
import { elementText } from './selectorEvaluator'; import { elementText } from './selectorEvaluator';
type SelectorToken = { type SelectorToken = {

View file

@ -16,12 +16,12 @@
import { serializeAsCallArgument, parseEvaluationResultValue } from '../common/utilityScriptSerializers'; import { serializeAsCallArgument, parseEvaluationResultValue } from '../common/utilityScriptSerializers';
export default class UtilityScript { export class UtilityScript {
evaluate(isFunction: boolean | undefined, returnByValue: boolean, expression: string, argCount: number, ...argsAndHandles: any[]) { evaluate(isFunction: boolean | undefined, returnByValue: boolean, expression: string, argCount: number, ...argsAndHandles: any[]) {
const args = argsAndHandles.slice(0, argCount); const args = argsAndHandles.slice(0, argCount);
const handles = argsAndHandles.slice(argCount); const handles = argsAndHandles.slice(argCount);
const parameters = args.map(a => parseEvaluationResultValue(a, handles)); const parameters = args.map(a => parseEvaluationResultValue(a, handles));
let result = global.eval(expression); let result = globalThis.eval(expression);
if (isFunction === true) { if (isFunction === true) {
result = result(...parameters); result = result(...parameters);
} else if (isFunction === false) { } else if (isFunction === false) {
@ -63,3 +63,5 @@ export default class UtilityScript {
return safeJson(value); return safeJson(value);
} }
} }
module.exports = UtilityScript;

View file

@ -1,80 +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.
*/
const path = require('path');
const fs = require('fs');
class InlineSource {
/**
* @param {string[]} outFiles
*/
constructor(outFiles) {
this.outFiles = outFiles;
}
/**
* @param {import('webpack').Compiler} compiler
*/
apply(compiler) {
compiler.hooks.emit.tapAsync('InlineSource', (compilation, callback) => {
for (const outFile of this.outFiles) {
const source = compilation.assets[path.basename(outFile).replace('.ts', '.js')].source();
fs.mkdirSync(path.dirname(outFile), { recursive: true });
const newSource = 'export const source = ' + JSON.stringify(source) + ';';
fs.writeFileSync(outFile, newSource);
}
callback();
});
}
}
const entry = {
utilityScriptSource: path.join(__dirname, 'utilityScript.ts'),
injectedScriptSource: path.join(__dirname, 'injectedScript.ts'),
consoleApiSource: path.join(__dirname, '..', 'supplements', 'injected', 'consoleApi.ts'),
recorderSource: path.join(__dirname, '..', 'supplements', 'injected', 'recorder.ts'),
}
/** @type {import('webpack').Configuration} */
module.exports = {
entry,
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
devtool: false,
module: {
rules: [
{
test: /\.(j|t)sx?$/,
loader: 'babel-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
output: {
libraryTarget: 'var',
library: 'pwExport',
libraryExport: 'default',
filename: '[name].js',
path: path.resolve(__dirname, '../../../lib/server/injected/packed')
},
plugins: [
new InlineSource(
Object.keys(entry).map(x => path.join(__dirname, '..', '..', 'generated', x + '.ts'))
),
]
};

View file

@ -17,7 +17,7 @@
import * as dom from './dom'; import * as dom from './dom';
import * as utilityScriptSource from '../generated/utilityScriptSource'; import * as utilityScriptSource from '../generated/utilityScriptSource';
import { serializeAsCallArgument } from './common/utilityScriptSerializers'; import { serializeAsCallArgument } from './common/utilityScriptSerializers';
import type UtilityScript from './injected/utilityScript'; import { type UtilityScript } from './injected/utilityScript';
import { SdkObject } from './instrumentation'; import { SdkObject } from './instrumentation';
import { ManualPromise } from '../utils/async'; import { ManualPromise } from '../utils/async';
@ -114,8 +114,9 @@ export class ExecutionContext extends SdkObject {
if (!this._utilityScriptPromise) { if (!this._utilityScriptPromise) {
const source = ` const source = `
(() => { (() => {
const module = {};
${utilityScriptSource.source} ${utilityScriptSource.source}
return new pwExport(); return new module.exports();
})();`; })();`;
this._utilityScriptPromise = this._raceAgainstContextDestroyed(this._delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', undefined, objectId))); this._utilityScriptPromise = this._raceAgainstContextDestroyed(this._delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', undefined, objectId)));
} }

View file

@ -15,7 +15,7 @@
*/ */
import { escapeWithQuotes } from '../../../utils/stringUtils'; import { escapeWithQuotes } from '../../../utils/stringUtils';
import type InjectedScript from '../../injected/injectedScript'; import { type InjectedScript } from '../../injected/injectedScript';
import { generateSelector } from '../../injected/selectorGenerator'; import { generateSelector } from '../../injected/selectorGenerator';
function createLocator(injectedScript: InjectedScript, initial: string, options?: { hasText?: string | RegExp }) { function createLocator(injectedScript: InjectedScript, initial: string, options?: { hasText?: string | RegExp }) {
@ -64,7 +64,7 @@ declare global {
} }
} }
export class ConsoleAPI { class ConsoleAPI {
private _injectedScript: InjectedScript; private _injectedScript: InjectedScript;
constructor(injectedScript: InjectedScript) { constructor(injectedScript: InjectedScript) {
@ -112,4 +112,4 @@ export class ConsoleAPI {
} }
} }
export default ConsoleAPI; module.exports = ConsoleAPI;

View file

@ -15,7 +15,7 @@
*/ */
import type * as actions from '../recorder/recorderActions'; import type * as actions from '../recorder/recorderActions';
import type InjectedScript from '../../injected/injectedScript'; import { type InjectedScript } from '../../injected/injectedScript';
import { generateSelector, querySelector } from '../../injected/selectorGenerator'; import { generateSelector, querySelector } from '../../injected/selectorGenerator';
import type { Point } from '../../../common/types'; import type { Point } from '../../../common/types';
import type { UIState } from '../recorder/recorderTypes'; import type { UIState } from '../recorder/recorderTypes';
@ -30,7 +30,7 @@ declare module globalThis {
let _playwrightRefreshOverlay: () => void; let _playwrightRefreshOverlay: () => void;
} }
export class Recorder { class Recorder {
private _injectedScript: InjectedScript; private _injectedScript: InjectedScript;
private _performingAction = false; private _performingAction = false;
private _listeners: (() => void)[] = []; private _listeners: (() => void)[] = [];
@ -473,4 +473,4 @@ function removeEventListeners(listeners: (() => void)[]) {
listeners.splice(0, listeners.length); listeners.splice(0, listeners.length);
} }
export default Recorder; module.exports = Recorder;

View file

@ -15,7 +15,6 @@
*/ */
import { BrowserContext } from '../../browserContext'; import { BrowserContext } from '../../browserContext';
import { eventsHelper } from '../../../utils/eventsHelper';
import { Page } from '../../page'; import { Page } from '../../page';
import { FrameSnapshot } from '../common/snapshotTypes'; import { FrameSnapshot } from '../common/snapshotTypes';
import { SnapshotRenderer } from '../../../../../trace-viewer/src/snapshotRenderer'; import { SnapshotRenderer } from '../../../../../trace-viewer/src/snapshotRenderer';
@ -61,9 +60,9 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
this._snapshotter.captureSnapshot(page, snapshotName, element).catch(() => {}); this._snapshotter.captureSnapshot(page, snapshotName, element).catch(() => {});
return new Promise<SnapshotRenderer>(fulfill => { return new Promise<SnapshotRenderer>(fulfill => {
const listener = eventsHelper.addEventListener(this, 'snapshot', (renderer: SnapshotRenderer) => { const disposable = this.onSnapshotEvent((renderer: SnapshotRenderer) => {
if (renderer.snapshotName === snapshotName) { if (renderer.snapshotName === snapshotName) {
eventsHelper.removeEventListeners([listener]); disposable.dispose();
fulfill(renderer); fulfill(renderer);
} }
}); });

View file

@ -0,0 +1,77 @@
/**
* 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 namespace Disposable {
export function disposeAll(disposables: Disposable[]): void {
for (const disposable of disposables.splice(0))
disposable.dispose();
}
}
export type Disposable = {
dispose(): void;
};
export interface Event<T> {
(listener: (e: T) => any, disposables?: Disposable[]): Disposable;
}
export class EventEmitter<T> {
public event: Event<T>;
private _deliveryQueue?: {listener: (e: T) => void, event: T}[];
private _listeners = new Set<(e: T) => void>();
constructor() {
this.event = (listener: (e: T) => any, disposables?: Disposable[]) => {
this._listeners.add(listener);
let disposed = false;
const self = this;
const result: Disposable = {
dispose() {
if (!disposed) {
disposed = true;
self._listeners.delete(listener);
}
}
};
if (disposables)
disposables.push(result);
return result;
};
}
fire(event: T): void {
const dispatch = !this._deliveryQueue;
if (!this._deliveryQueue)
this._deliveryQueue = [];
for (const listener of this._listeners)
this._deliveryQueue.push({ listener, event });
if (!dispatch)
return;
for (let index = 0; index < this._deliveryQueue.length; index++) {
const { listener, event } = this._deliveryQueue[index];
listener.call(null, event);
}
this._deliveryQueue = undefined;
}
dispose() {
this._listeners.clear();
if (this._deliveryQueue)
this._deliveryQueue = [];
}
}

View file

@ -15,7 +15,7 @@
*/ */
import type { FrameSnapshot, ResourceSnapshot } from '@playwright-core/server/trace/common/snapshotTypes'; import type { FrameSnapshot, ResourceSnapshot } from '@playwright-core/server/trace/common/snapshotTypes';
import { EventEmitter } from 'events'; import { EventEmitter } from './events';
import { SnapshotRenderer } from './snapshotRenderer'; import { SnapshotRenderer } from './snapshotRenderer';
export interface SnapshotStorage { export interface SnapshotStorage {
@ -25,12 +25,14 @@ export interface SnapshotStorage {
snapshotByIndex(frameId: string, index: number): SnapshotRenderer | undefined; snapshotByIndex(frameId: string, index: number): SnapshotRenderer | undefined;
} }
export abstract class BaseSnapshotStorage extends EventEmitter implements SnapshotStorage { export abstract class BaseSnapshotStorage implements SnapshotStorage {
protected _resources: ResourceSnapshot[] = []; protected _resources: ResourceSnapshot[] = [];
protected _frameSnapshots = new Map<string, { protected _frameSnapshots = new Map<string, {
raw: FrameSnapshot[], raw: FrameSnapshot[],
renderer: SnapshotRenderer[] renderer: SnapshotRenderer[]
}>(); }>();
private _didSnapshot = new EventEmitter<SnapshotRenderer>();
readonly onSnapshotEvent = this._didSnapshot.event;
clear() { clear() {
this._resources = []; this._resources = [];
@ -55,7 +57,7 @@ export abstract class BaseSnapshotStorage extends EventEmitter implements Snapsh
frameSnapshots.raw.push(snapshot); frameSnapshots.raw.push(snapshot);
const renderer = new SnapshotRenderer(this._resources, frameSnapshots.raw, frameSnapshots.raw.length - 1); const renderer = new SnapshotRenderer(this._resources, frameSnapshots.raw, frameSnapshots.raw.length - 1);
frameSnapshots.renderer.push(renderer); frameSnapshots.renderer.push(renderer);
this.emit('snapshot', renderer); this._didSnapshot.fire(renderer);
} }
abstract resourceContent(sha1: string): Promise<Blob | undefined>; abstract resourceContent(sha1: string): Promise<Blob | undefined>;

View file

@ -59,7 +59,7 @@ it.describe('snapshots', () => {
it('should collect multiple', async ({ page, toImpl, snapshotter }) => { it('should collect multiple', async ({ page, toImpl, snapshotter }) => {
await page.setContent('<button>Hello</button>'); await page.setContent('<button>Hello</button>');
const snapshots = []; const snapshots = [];
snapshotter.on('snapshot', snapshot => snapshots.push(snapshot)); snapshotter.onSnapshotEvent(snapshot => snapshots.push(snapshot));
await snapshotter.captureSnapshot(toImpl(page), 'snapshot1'); await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
await snapshotter.captureSnapshot(toImpl(page), 'snapshot2'); await snapshotter.captureSnapshot(toImpl(page), 'snapshot2');
expect(snapshots.length).toBe(2); expect(snapshots.length).toBe(2);

View file

@ -190,19 +190,11 @@ steps.push({
}); });
// Build injected scripts. // Build injected scripts.
const webPackFiles = [ steps.push({
'packages/playwright-core/src/server/injected/webpack.config.js', command: 'node',
]; args: ['utils/generate_injected.js'],
for (const file of webPackFiles) {
steps.push({
command: 'npx',
args: ['webpack', '--config', quotePath(filePath(file)), ...(watchMode ? ['--watch', '--stats', 'none'] : [])],
shell: true, shell: true,
env: { });
NODE_ENV: watchMode ? 'development' : 'production'
}
});
}
// Run Babel. // Run Babel.
for (const pkg of workspace.packages()) { for (const pkg of workspace.packages()) {
@ -216,11 +208,22 @@ for (const pkg of workspace.packages()) {
'--extensions', '.ts', '--extensions', '.ts',
'--out-dir', quotePath(path.join(pkg.path, 'lib')), '--out-dir', quotePath(path.join(pkg.path, 'lib')),
'--ignore', '"packages/playwright-core/src/server/injected/**/*"', '--ignore', '"packages/playwright-core/src/server/injected/**/*"',
'--ignore', '"packages/playwright-core/src/server/supplements/injected/**/*"',
quotePath(path.join(pkg.path, 'src'))], quotePath(path.join(pkg.path, 'src'))],
shell: true, shell: true,
}); });
} }
// Generate injected.
onChanges.push({
committed: false,
inputs: [
'packages/playwright-core/src/server/injected/**',
'packages/playwright-core/src/supplements/injected/**',
'utils/generate_injected.js',
],
script: 'utils/generate_injected.js',
});
// Generate channels. // Generate channels.
onChanges.push({ onChanges.push({

View file

@ -0,0 +1,50 @@
#!/usr/bin/env node
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* 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.
*/
// @ts-check
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
const esbuild = require('esbuild');
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', 'supplements', 'injected', 'consoleApi.ts'),
path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'supplements', 'injected', 'recorder.ts'),
];
(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');
await esbuild.build({
entryPoints: [injected],
bundle: true,
outdir,
format: 'cjs',
platform: 'browser',
target: 'ES2019'
});
const baseName = path.basename(injected);
const content = await fs.promises.readFile(path.join(outdir, baseName.replace('.ts', '.js')), 'utf-8');
const newContent = `export const source = ${JSON.stringify(content)};`;
await fs.promises.writeFile(path.join(generatedFolder, baseName.replace('.ts', 'Source.ts')), newContent);
}
})();