2019-11-18 18:18:28 -08:00
|
|
|
/**
|
|
|
|
* Copyright 2017 Google Inc. All rights reserved.
|
|
|
|
* Modifications 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.
|
|
|
|
*/
|
2019-12-05 14:48:39 -08:00
|
|
|
|
2020-04-01 14:42:47 -07:00
|
|
|
import * as crypto from 'crypto';
|
|
|
|
import { EventEmitter } from 'events';
|
|
|
|
import * as fs from 'fs';
|
2020-06-08 21:45:35 -07:00
|
|
|
import * as removeFolder from 'rimraf';
|
2020-04-01 14:42:47 -07:00
|
|
|
import * as util from 'util';
|
2020-03-03 16:09:32 -08:00
|
|
|
import * as types from './types';
|
2020-06-10 15:12:50 -07:00
|
|
|
import { Progress } from './progress';
|
|
|
|
|
2020-06-08 21:45:35 -07:00
|
|
|
const removeFolderAsync = util.promisify(removeFolder);
|
2019-11-18 18:18:28 -08:00
|
|
|
|
|
|
|
export type RegisteredListener = {
|
2020-04-01 14:42:47 -07:00
|
|
|
emitter: EventEmitter;
|
2019-11-18 18:18:28 -08:00
|
|
|
eventName: (string | symbol);
|
2019-12-09 15:41:20 -07:00
|
|
|
handler: (...args: any[]) => void;
|
2019-11-18 18:18:28 -08:00
|
|
|
};
|
|
|
|
|
2020-04-01 14:42:47 -07:00
|
|
|
export type Listener = (...args: any[]) => void;
|
|
|
|
|
2020-06-14 17:24:45 -07:00
|
|
|
let isInDebugMode = !!getFromENV('PWDEBUG');
|
2020-06-16 10:15:08 -07:00
|
|
|
let isInRecordMode = false;
|
2020-06-11 18:18:33 -07:00
|
|
|
|
2020-06-29 16:37:01 -07:00
|
|
|
const deprecatedHits = new Set();
|
|
|
|
export function deprecate(methodName: string, message: string) {
|
|
|
|
if (deprecatedHits.has(methodName))
|
|
|
|
return;
|
|
|
|
deprecatedHits.add(methodName);
|
|
|
|
console.warn(message);
|
|
|
|
}
|
|
|
|
|
2019-11-18 18:18:28 -08:00
|
|
|
class Helper {
|
2020-06-29 16:37:01 -07:00
|
|
|
|
2019-11-18 18:18:28 -08:00
|
|
|
static evaluationString(fun: Function | string, ...args: any[]): string {
|
|
|
|
if (Helper.isString(fun)) {
|
2020-03-20 15:08:17 -07:00
|
|
|
assert(args.length === 0 || (args.length === 1 && args[0] === undefined), 'Cannot evaluate a string with arguments');
|
2020-02-07 13:38:50 -08:00
|
|
|
return fun;
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
2020-06-25 08:30:56 -07:00
|
|
|
return Helper.evaluationStringForFunctionBody(String(fun), ...args);
|
|
|
|
}
|
2019-11-18 18:18:28 -08:00
|
|
|
|
2020-06-25 08:30:56 -07:00
|
|
|
static evaluationStringForFunctionBody(functionBody: string, ...args: any[]): string {
|
|
|
|
return `(${functionBody})(${args.map(serializeArgument).join(',')})`;
|
2019-11-18 18:18:28 -08:00
|
|
|
function serializeArgument(arg: any): string {
|
|
|
|
if (Object.is(arg, undefined))
|
|
|
|
return 'undefined';
|
|
|
|
return JSON.stringify(arg);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-20 15:08:17 -07:00
|
|
|
static async evaluationScript(fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise<string> {
|
2020-02-27 17:42:14 -08:00
|
|
|
if (!helper.isString(fun) && typeof fun !== 'function') {
|
|
|
|
if (fun.content !== undefined) {
|
|
|
|
fun = fun.content;
|
|
|
|
} else if (fun.path !== undefined) {
|
2020-04-01 14:42:47 -07:00
|
|
|
let contents = await util.promisify(fs.readFile)(fun.path, 'utf8');
|
2020-02-28 15:34:07 -08:00
|
|
|
if (addSourceUrl)
|
|
|
|
contents += '//# sourceURL=' + fun.path.replace(/\n/g, '');
|
2020-02-27 17:42:14 -08:00
|
|
|
fun = contents;
|
|
|
|
} else {
|
|
|
|
throw new Error('Either path or content property must be present');
|
|
|
|
}
|
|
|
|
}
|
2020-03-20 15:08:17 -07:00
|
|
|
return helper.evaluationString(fun, arg);
|
2020-02-27 17:42:14 -08:00
|
|
|
}
|
|
|
|
|
2020-01-16 17:48:38 -08:00
|
|
|
static installApiHooks(className: string, classType: any) {
|
2019-11-18 18:18:28 -08:00
|
|
|
for (const methodName of Reflect.ownKeys(classType.prototype)) {
|
|
|
|
const method = Reflect.get(classType.prototype, methodName);
|
2020-01-16 17:48:38 -08:00
|
|
|
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function')
|
|
|
|
continue;
|
|
|
|
const isAsync = method.constructor.name === 'AsyncFunction';
|
2020-04-20 07:52:26 -07:00
|
|
|
if (!isAsync)
|
2019-11-18 18:18:28 -08:00
|
|
|
continue;
|
2020-06-01 08:54:18 -07:00
|
|
|
const override = function(this: any, ...args: any[]) {
|
2020-02-25 07:09:27 -08:00
|
|
|
const syncStack: any = {};
|
|
|
|
Error.captureStackTrace(syncStack);
|
2020-01-13 13:33:25 -08:00
|
|
|
return method.call(this, ...args).catch((e: any) => {
|
2019-11-18 18:18:28 -08:00
|
|
|
const stack = syncStack.stack.substring(syncStack.stack.indexOf('\n') + 1);
|
|
|
|
const clientStack = stack.substring(stack.indexOf('\n'));
|
|
|
|
if (e instanceof Error && e.stack && !e.stack.includes(clientStack))
|
|
|
|
e.stack += '\n -- ASYNC --\n' + stack;
|
|
|
|
throw e;
|
|
|
|
});
|
2020-06-01 08:54:18 -07:00
|
|
|
};
|
|
|
|
Object.defineProperty(override, 'name', { writable: false, value: methodName });
|
|
|
|
Reflect.set(classType.prototype, methodName, override);
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static addEventListener(
|
2020-04-01 14:42:47 -07:00
|
|
|
emitter: EventEmitter,
|
2019-11-18 18:18:28 -08:00
|
|
|
eventName: (string | symbol),
|
2019-12-09 15:41:20 -07:00
|
|
|
handler: (...args: any[]) => void): RegisteredListener {
|
2019-11-18 18:18:28 -08:00
|
|
|
emitter.on(eventName, handler);
|
|
|
|
return { emitter, eventName, handler };
|
|
|
|
}
|
|
|
|
|
|
|
|
static removeEventListeners(listeners: Array<{
|
2020-04-01 14:42:47 -07:00
|
|
|
emitter: EventEmitter;
|
2019-11-18 18:18:28 -08:00
|
|
|
eventName: (string | symbol);
|
2019-12-09 15:41:20 -07:00
|
|
|
handler: (...args: any[]) => void;
|
2019-11-18 18:18:28 -08:00
|
|
|
}>) {
|
|
|
|
for (const listener of listeners)
|
|
|
|
listener.emitter.removeListener(listener.eventName, listener.handler);
|
|
|
|
listeners.splice(0, listeners.length);
|
|
|
|
}
|
|
|
|
|
|
|
|
static isString(obj: any): obj is string {
|
|
|
|
return typeof obj === 'string' || obj instanceof String;
|
|
|
|
}
|
|
|
|
|
|
|
|
static isNumber(obj: any): obj is number {
|
|
|
|
return typeof obj === 'number' || obj instanceof Number;
|
|
|
|
}
|
|
|
|
|
2020-02-25 10:32:17 +08:00
|
|
|
static isRegExp(obj: any): obj is RegExp {
|
|
|
|
return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]';
|
|
|
|
}
|
|
|
|
|
2020-02-22 06:16:28 -08:00
|
|
|
static isObject(obj: any): obj is NonNullable<object> {
|
|
|
|
return typeof obj === 'object' && obj !== null;
|
|
|
|
}
|
|
|
|
|
|
|
|
static isBoolean(obj: any): obj is boolean {
|
|
|
|
return typeof obj === 'boolean' || obj instanceof Boolean;
|
|
|
|
}
|
|
|
|
|
2020-02-03 14:23:24 -08:00
|
|
|
static globToRegex(glob: string): RegExp {
|
|
|
|
const tokens = ['^'];
|
|
|
|
let inGroup;
|
|
|
|
for (let i = 0; i < glob.length; ++i) {
|
|
|
|
const c = glob[i];
|
|
|
|
if (escapeGlobChars.has(c)) {
|
|
|
|
tokens.push('\\' + c);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (c === '*') {
|
|
|
|
const beforeDeep = glob[i - 1];
|
|
|
|
let starCount = 1;
|
|
|
|
while (glob[i + 1] === '*') {
|
|
|
|
starCount++;
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
const afterDeep = glob[i + 1];
|
|
|
|
const isDeep = starCount > 1 &&
|
|
|
|
(beforeDeep === '/' || beforeDeep === undefined) &&
|
|
|
|
(afterDeep === '/' || afterDeep === undefined);
|
|
|
|
if (isDeep) {
|
|
|
|
tokens.push('((?:[^/]*(?:\/|$))*)');
|
|
|
|
i++;
|
|
|
|
} else {
|
|
|
|
tokens.push('([^/]*)');
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (c) {
|
|
|
|
case '?':
|
|
|
|
tokens.push('.');
|
|
|
|
break;
|
|
|
|
case '{':
|
|
|
|
inGroup = true;
|
|
|
|
tokens.push('(');
|
|
|
|
break;
|
|
|
|
case '}':
|
|
|
|
inGroup = false;
|
|
|
|
tokens.push(')');
|
|
|
|
break;
|
|
|
|
case ',':
|
|
|
|
if (inGroup) {
|
|
|
|
tokens.push('|');
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
tokens.push('\\' + c);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
tokens.push(c);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
tokens.push('$');
|
|
|
|
return new RegExp(tokens.join(''));
|
|
|
|
}
|
2020-02-04 19:39:52 -08:00
|
|
|
|
|
|
|
static completeUserURL(urlString: string): string {
|
|
|
|
if (urlString.startsWith('localhost') || urlString.startsWith('127.0.0.1'))
|
|
|
|
urlString = 'http://' + urlString;
|
|
|
|
return urlString;
|
|
|
|
}
|
2020-02-25 07:09:27 -08:00
|
|
|
|
|
|
|
static trimMiddle(string: string, maxLength: number) {
|
|
|
|
if (string.length <= maxLength)
|
|
|
|
return string;
|
|
|
|
|
|
|
|
const leftHalf = maxLength >> 1;
|
|
|
|
const rightHalf = maxLength - leftHalf - 1;
|
|
|
|
return string.substr(0, leftHalf) + '\u2026' + string.substr(this.length - rightHalf, rightHalf);
|
|
|
|
}
|
2020-03-03 16:09:32 -08:00
|
|
|
|
|
|
|
static enclosingIntRect(rect: types.Rect): types.Rect {
|
|
|
|
const x = Math.floor(rect.x + 1e-3);
|
|
|
|
const y = Math.floor(rect.y + 1e-3);
|
|
|
|
const x2 = Math.ceil(rect.x + rect.width - 1e-3);
|
|
|
|
const y2 = Math.ceil(rect.y + rect.height - 1e-3);
|
|
|
|
return { x, y, width: x2 - x, height: y2 - y };
|
|
|
|
}
|
|
|
|
|
|
|
|
static enclosingIntSize(size: types.Size): types.Size {
|
|
|
|
return { width: Math.floor(size.width + 1e-3), height: Math.floor(size.height + 1e-3) };
|
|
|
|
}
|
2020-03-16 14:39:44 -07:00
|
|
|
|
|
|
|
static urlMatches(urlString: string, match: types.URLMatch | undefined): boolean {
|
|
|
|
if (match === undefined || match === '')
|
|
|
|
return true;
|
|
|
|
if (helper.isString(match))
|
|
|
|
match = helper.globToRegex(match);
|
|
|
|
if (helper.isRegExp(match))
|
|
|
|
return match.test(urlString);
|
|
|
|
if (typeof match === 'string' && match === urlString)
|
|
|
|
return true;
|
|
|
|
const url = new URL(urlString);
|
|
|
|
if (typeof match === 'string')
|
|
|
|
return url.pathname === match;
|
|
|
|
|
|
|
|
assert(typeof match === 'function', 'url parameter should be string, RegExp or function');
|
|
|
|
return match(url);
|
|
|
|
}
|
2020-04-01 14:42:47 -07:00
|
|
|
|
|
|
|
// See https://joel.tools/microtasks/
|
|
|
|
static makeWaitForNextTask() {
|
|
|
|
if (parseInt(process.versions.node, 10) >= 11)
|
|
|
|
return setImmediate;
|
|
|
|
|
|
|
|
// Unlike Node 11, Node 10 and less have a bug with Task and MicroTask execution order:
|
|
|
|
// - https://github.com/nodejs/node/issues/22257
|
|
|
|
//
|
|
|
|
// So we can't simply run setImmediate to dispatch code in a following task.
|
|
|
|
// However, we can run setImmediate from-inside setImmediate to make sure we're getting
|
|
|
|
// in the following task.
|
|
|
|
|
|
|
|
let spinning = false;
|
|
|
|
const callbacks: (() => void)[] = [];
|
|
|
|
const loop = () => {
|
|
|
|
const callback = callbacks.shift();
|
|
|
|
if (!callback) {
|
|
|
|
spinning = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
setImmediate(loop);
|
|
|
|
// Make sure to call callback() as the last thing since it's
|
|
|
|
// untrusted code that might throw.
|
|
|
|
callback();
|
|
|
|
};
|
|
|
|
|
|
|
|
return (callback: () => void) => {
|
|
|
|
callbacks.push(callback);
|
|
|
|
if (!spinning) {
|
|
|
|
spinning = true;
|
|
|
|
setImmediate(loop);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
static guid(): string {
|
|
|
|
return crypto.randomBytes(16).toString('hex');
|
|
|
|
}
|
2020-04-07 14:35:34 -07:00
|
|
|
|
2020-05-22 15:56:37 -07:00
|
|
|
static getViewportSizeFromWindowFeatures(features: string[]): types.Size | null {
|
|
|
|
const widthString = features.find(f => f.startsWith('width='));
|
|
|
|
const heightString = features.find(f => f.startsWith('height='));
|
|
|
|
const width = widthString ? parseInt(widthString.substring(6), 10) : NaN;
|
|
|
|
const height = heightString ? parseInt(heightString.substring(7), 10) : NaN;
|
|
|
|
if (!Number.isNaN(width) && !Number.isNaN(height))
|
|
|
|
return { width, height };
|
|
|
|
return null;
|
|
|
|
}
|
2020-06-08 21:45:35 -07:00
|
|
|
|
|
|
|
static async removeFolders(dirs: string[]) {
|
|
|
|
await Promise.all(dirs.map(dir => {
|
|
|
|
return removeFolderAsync(dir).catch((err: Error) => console.error(err));
|
|
|
|
}));
|
|
|
|
}
|
2020-06-10 15:12:50 -07:00
|
|
|
|
|
|
|
static async waitForEvent(progress: Progress, emitter: EventEmitter, event: string, predicate?: Function): Promise<any> {
|
|
|
|
const listeners: RegisteredListener[] = [];
|
|
|
|
const promise = new Promise((resolve, reject) => {
|
|
|
|
listeners.push(helper.addEventListener(emitter, event, eventArg => {
|
|
|
|
try {
|
|
|
|
if (predicate && !predicate(eventArg))
|
|
|
|
return;
|
|
|
|
resolve(eventArg);
|
|
|
|
} catch (e) {
|
|
|
|
reject(e);
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
});
|
|
|
|
progress.cleanupWhenAborted(() => helper.removeEventListeners(listeners));
|
|
|
|
const result = await promise;
|
|
|
|
helper.removeEventListeners(listeners);
|
|
|
|
return result;
|
|
|
|
}
|
2020-06-11 18:18:33 -07:00
|
|
|
|
|
|
|
static isDebugMode(): boolean {
|
2020-06-14 17:24:45 -07:00
|
|
|
return isInDebugMode;
|
|
|
|
}
|
|
|
|
|
2020-06-16 10:15:08 -07:00
|
|
|
static setDebugMode(enabled: boolean) {
|
|
|
|
isInDebugMode = enabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
static isRecordMode(): boolean {
|
|
|
|
return isInRecordMode;
|
|
|
|
}
|
|
|
|
|
|
|
|
static setRecordMode(enabled: boolean) {
|
|
|
|
isInRecordMode = enabled;
|
2020-06-11 18:18:33 -07:00
|
|
|
}
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2020-02-05 16:53:36 -08:00
|
|
|
export function assert(value: any, message?: string): asserts value {
|
2019-11-18 18:18:28 -08:00
|
|
|
if (!value)
|
|
|
|
throw new Error(message);
|
|
|
|
}
|
|
|
|
|
2020-05-08 10:37:54 -07:00
|
|
|
let _isUnderTest = false;
|
|
|
|
|
|
|
|
export function setUnderTest() {
|
|
|
|
_isUnderTest = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isUnderTest(): boolean {
|
|
|
|
return _isUnderTest;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function debugAssert(value: any, message?: string): asserts value {
|
|
|
|
if (_isUnderTest && !value)
|
|
|
|
throw new Error(message);
|
|
|
|
}
|
|
|
|
|
2020-04-29 18:35:04 -07:00
|
|
|
export function assertMaxArguments(count: number, max: number): asserts count {
|
|
|
|
assert(count <= max, 'Too many arguments. If you need to pass more than 1 argument to the function wrap them in an object.');
|
|
|
|
}
|
|
|
|
|
2020-04-28 10:37:23 -07:00
|
|
|
export function getFromENV(name: string) {
|
|
|
|
let value = process.env[name];
|
|
|
|
value = value || process.env[`npm_config_${name.toLowerCase()}`];
|
|
|
|
value = value || process.env[`npm_package_config_${name.toLowerCase()}`];
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function logPolitely(toBeLogged: string) {
|
|
|
|
const logLevel = process.env.npm_config_loglevel;
|
|
|
|
const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel || '') > -1;
|
|
|
|
|
|
|
|
if (!logLevelDisplay)
|
|
|
|
console.log(toBeLogged); // eslint-disable-line no-console
|
|
|
|
}
|
|
|
|
|
2020-02-03 14:23:24 -08:00
|
|
|
const escapeGlobChars = new Set(['/', '$', '^', '+', '.', '(', ')', '=', '!', '|']);
|
|
|
|
|
2019-11-18 18:18:28 -08:00
|
|
|
export const helper = Helper;
|