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

View File

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

View File

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

View File

@ -99,8 +99,9 @@ export class FrameExecutionContext extends js.ExecutionContext {
custom.push(`{ name: '${name}', engine: (${source}) }`);
const source = `
(() => {
const module = {};
${injectedScriptSource.source}
return new pwExport(
return new module.exports(
${isUnderTest()},
${this.frame._page._delegate.rafCountForStablePosition()},
"${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 { debugLogger } from '../utils/debugLogger';
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 { isSessionClosedError } from './protocolError';
import { isInvalidSelectorError, splitSelectorByFrame, stringifySelector, ParsedSelector } from './common/selectorParser';

View File

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

View File

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

View File

@ -16,12 +16,12 @@
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[]) {
const args = argsAndHandles.slice(0, argCount);
const handles = argsAndHandles.slice(argCount);
const parameters = args.map(a => parseEvaluationResultValue(a, handles));
let result = global.eval(expression);
let result = globalThis.eval(expression);
if (isFunction === true) {
result = result(...parameters);
} else if (isFunction === false) {
@ -63,3 +63,5 @@ export default class UtilityScript {
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 utilityScriptSource from '../generated/utilityScriptSource';
import { serializeAsCallArgument } from './common/utilityScriptSerializers';
import type UtilityScript from './injected/utilityScript';
import { type UtilityScript } from './injected/utilityScript';
import { SdkObject } from './instrumentation';
import { ManualPromise } from '../utils/async';
@ -114,8 +114,9 @@ export class ExecutionContext extends SdkObject {
if (!this._utilityScriptPromise) {
const source = `
(() => {
const module = {};
${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)));
}

View File

@ -15,7 +15,7 @@
*/
import { escapeWithQuotes } from '../../../utils/stringUtils';
import type InjectedScript from '../../injected/injectedScript';
import { type InjectedScript } from '../../injected/injectedScript';
import { generateSelector } from '../../injected/selectorGenerator';
function createLocator(injectedScript: InjectedScript, initial: string, options?: { hasText?: string | RegExp }) {
@ -64,7 +64,7 @@ declare global {
}
}
export class ConsoleAPI {
class ConsoleAPI {
private _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 InjectedScript from '../../injected/injectedScript';
import { type InjectedScript } from '../../injected/injectedScript';
import { generateSelector, querySelector } from '../../injected/selectorGenerator';
import type { Point } from '../../../common/types';
import type { UIState } from '../recorder/recorderTypes';
@ -30,7 +30,7 @@ declare module globalThis {
let _playwrightRefreshOverlay: () => void;
}
export class Recorder {
class Recorder {
private _injectedScript: InjectedScript;
private _performingAction = false;
private _listeners: (() => void)[] = [];
@ -473,4 +473,4 @@ function removeEventListeners(listeners: (() => void)[]) {
listeners.splice(0, listeners.length);
}
export default Recorder;
module.exports = Recorder;

View File

@ -15,7 +15,6 @@
*/
import { BrowserContext } from '../../browserContext';
import { eventsHelper } from '../../../utils/eventsHelper';
import { Page } from '../../page';
import { FrameSnapshot } from '../common/snapshotTypes';
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(() => {});
return new Promise<SnapshotRenderer>(fulfill => {
const listener = eventsHelper.addEventListener(this, 'snapshot', (renderer: SnapshotRenderer) => {
const disposable = this.onSnapshotEvent((renderer: SnapshotRenderer) => {
if (renderer.snapshotName === snapshotName) {
eventsHelper.removeEventListeners([listener]);
disposable.dispose();
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 { EventEmitter } from 'events';
import { EventEmitter } from './events';
import { SnapshotRenderer } from './snapshotRenderer';
export interface SnapshotStorage {
@ -25,12 +25,14 @@ export interface SnapshotStorage {
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 _frameSnapshots = new Map<string, {
raw: FrameSnapshot[],
renderer: SnapshotRenderer[]
}>();
private _didSnapshot = new EventEmitter<SnapshotRenderer>();
readonly onSnapshotEvent = this._didSnapshot.event;
clear() {
this._resources = [];
@ -55,7 +57,7 @@ export abstract class BaseSnapshotStorage extends EventEmitter implements Snapsh
frameSnapshots.raw.push(snapshot);
const renderer = new SnapshotRenderer(this._resources, frameSnapshots.raw, frameSnapshots.raw.length - 1);
frameSnapshots.renderer.push(renderer);
this.emit('snapshot', renderer);
this._didSnapshot.fire(renderer);
}
abstract resourceContent(sha1: string): Promise<Blob | undefined>;

View File

@ -59,7 +59,7 @@ it.describe('snapshots', () => {
it('should collect multiple', async ({ page, toImpl, snapshotter }) => {
await page.setContent('<button>Hello</button>');
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), 'snapshot2');
expect(snapshots.length).toBe(2);

View File

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