chore: move server-side code to src/server (#411)

This commit is contained in:
Dmitry Gozman 2020-01-07 15:27:45 -08:00 committed by Pavel Feldman
parent e0693654b7
commit 6318b1de75
16 changed files with 232 additions and 194 deletions

1
index.d.ts vendored
View file

@ -18,3 +18,4 @@ export * from './lib/api';
export function playwright(browser: 'chromium'): import('./lib/api').ChromiumPlaywright;
export function playwright(browser: 'firefox'): import('./lib/api').FirefoxPlaywright;
export function playwright(browser: 'webkit'): import('./lib/api').WebKitPlaywright;
export function connect(browser: 'chromium'): import('./lib/api').ChromiumBrowser.connect;

View file

@ -33,3 +33,9 @@ module.exports.playwright = browser => {
return new api.WebKitPlaywright(__dirname, packageJson.playwright.webkit_revision);
throw new Error(`Unsupported browser "${browser}"`);
};
module.exports.connect = browser => {
if (browser === 'chromium')
return api.ChromiumBrowser.connect;
throw new Error(`Unsupported browser "${browser}"`);
};

View file

@ -17,7 +17,6 @@
export { Accessibility } from './accessibility';
export { Browser, BrowserServer } from './browser';
export { BrowserContext } from './browserContext';
export { BrowserFetcher } from './browserFetcher';
export { ConsoleMessage } from './console';
export { Dialog } from './dialog';
export { ElementHandle } from './dom';
@ -28,6 +27,9 @@ export { JSHandle } from './javascript';
export { Request, Response } from './network';
export { Coverage, FileChooser, Page, Worker } from './page';
export { BrowserFetcher } from './server/browserFetcher';
export { CRPlaywright as ChromiumPlaywright, CRBrowserServer as ChromiumBrowserServer } from './server/crPlaywright';
export * from './chromium/crApi';
export * from './firefox/ffApi';
export * from './webkit/wkApi';

View file

@ -16,5 +16,4 @@
export { CRBrowser as ChromiumBrowser } from './crBrowser';
export { CRSession as ChromiumSession } from './crConnection';
export { CRPlaywright as ChromiumPlaywright } from './crPlaywright';
export { CRTarget as ChromiumTarget } from './crTarget';

View file

@ -28,9 +28,16 @@ import * as browser from '../browser';
import * as network from '../network';
import * as types from '../types';
import * as platform from '../platform';
import { ConnectionTransport } from '../transport';
import { ConnectionTransport, SlowMoTransport } from '../transport';
import { readProtocolStream } from './crProtocolHelper';
export type CRConnectOptions = {
slowMo?: number,
browserWSEndpoint?: string;
browserURL?: string;
transport?: ConnectionTransport;
};
export class CRBrowser extends browser.Browser {
_connection: CRConnection;
_client: CRSession;
@ -42,10 +49,9 @@ export class CRBrowser extends browser.Browser {
private _tracingPath = '';
private _tracingClient: CRSession | undefined;
static async create(
transport: ConnectionTransport) {
static async connect(options: CRConnectOptions): Promise<CRBrowser> {
const transport = await createTransport(options);
const connection = new CRConnection(transport);
const { browserContextIds } = await connection.rootSession.send('Target.getBrowserContexts');
const browser = new CRBrowser(connection, browserContextIds);
await connection.rootSession.send('Target.setDiscoverTargets', { discover: true });
@ -297,3 +303,25 @@ export class CRBrowser extends browser.Browser {
return !this._connection._closed;
}
}
export async function createTransport(options: CRConnectOptions): Promise<ConnectionTransport> {
assert(Number(!!options.browserWSEndpoint) + Number(!!options.browserURL) + Number(!!options.transport) === 1, 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to playwright.connect');
let transport: ConnectionTransport | undefined;
let connectionURL: string = '';
if (options.transport) {
transport = options.transport;
} else if (options.browserWSEndpoint) {
connectionURL = options.browserWSEndpoint;
transport = await platform.createWebSocketTransport(options.browserWSEndpoint);
} else if (options.browserURL) {
try {
const data = await platform.fetchUrl(new URL('/json/version', options.browserURL).href);
connectionURL = JSON.parse(data).webSocketDebuggerUrl;
} catch (e) {
e.message = `Failed to fetch browser webSocket url from ${options.browserURL}: ` + e.message;
throw e;
}
transport = await platform.createWebSocketTransport(connectionURL);
}
return SlowMoTransport.wrap(transport, options.slowMo);
}

View file

@ -18,14 +18,15 @@
import * as os from 'os';
import * as path from 'path';
import { FFBrowser } from './ffBrowser';
import { BrowserFetcher, BrowserFetcherOptions } from '../browserFetcher';
import { BrowserFetcher, BrowserFetcherOptions } from '../server/browserFetcher';
import * as fs from 'fs';
import * as util from 'util';
import { assert } from '../helper';
import { TimeoutError } from '../errors';
import { WebSocketTransport, SlowMoTransport } from '../transport';
import { launchProcess, waitForLine } from '../processLauncher';
import { SlowMoTransport } from '../transport';
import { launchProcess, waitForLine } from '../server/processLauncher';
import { BrowserServer } from '../browser';
import * as platform from '../platform';
const mkdtempAsync = util.promisify(fs.mkdtemp);
const writeFileAsync = util.promisify(fs.writeFile);
@ -122,7 +123,7 @@ export class FFLauncher {
const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Firefox!`);
const match = await waitForLine(launchedProcess, launchedProcess.stdout, /^Juggler listening on (ws:\/\/.*)$/, timeout, timeoutError);
const url = match[1];
const transport = await WebSocketTransport.create(url);
const transport = await platform.createWebSocketTransport(url);
browser = await FFBrowser.create(SlowMoTransport.wrap(transport, slowMo));
await browser._waitForTarget(t => t.type() === 'page');
return new BrowserServer(browser, launchedProcess, url);

View file

@ -17,12 +17,13 @@
import * as browsers from '../browser';
import { FFBrowser } from './ffBrowser';
import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetcherRevisionInfo } from '../browserFetcher';
import { WebSocketTransport, SlowMoTransport } from '../transport';
import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetcherRevisionInfo } from '../server/browserFetcher';
import { SlowMoTransport } from '../transport';
import { DeviceDescriptors } from '../deviceDescriptors';
import * as Errors from '../errors';
import * as types from '../types';
import { FFLauncher, createBrowserFetcher } from './ffLauncher';
import * as platform from '../platform';
export class FFPlaywright {
private _projectRoot: string;
@ -52,7 +53,7 @@ export class FFPlaywright {
}
async connect(options: { slowMo?: number, browserWSEndpoint: string }): Promise<FFBrowser> {
const transport = await WebSocketTransport.create(options.browserWSEndpoint);
const transport = await platform.createWebSocketTransport(options.browserWSEndpoint);
return FFBrowser.create(SlowMoTransport.wrap(transport, options.slowMo || 0));
}

View file

@ -9,9 +9,13 @@ import * as nodeBuffer from 'buffer';
import * as mime from 'mime';
import * as jpeg from 'jpeg-js';
import * as png from 'pngjs';
import * as http from 'http';
import * as https from 'https';
import * as NodeWebSocket from 'ws';
import { assert, helper } from './helper';
import * as types from './types';
import { ConnectionTransport } from './transport';
export const isNode = typeof process === 'object' && !!process && typeof process.versions === 'object' && !!process.versions && !!process.versions.node;
@ -219,3 +223,79 @@ export function pngToJpeg(buffer: Buffer): Buffer {
assert(isNode, 'Converting from png to jpeg is only supported in Node.js');
return jpeg.encode(png.PNG.sync.read(buffer)).data;
}
function nodeFetch(url: string): Promise<string> {
let resolve: (url: string) => void;
let reject: (e: Error) => void;
const promise = new Promise<string>((res, rej) => { resolve = res; reject = rej; });
const endpointURL = new URL(url);
const protocol = endpointURL.protocol === 'https:' ? https : http;
const request = protocol.request(endpointURL, res => {
let data = '';
if (res.statusCode !== 200) {
// Consume response data to free up memory.
res.resume();
reject(new Error('HTTP ' + res.statusCode));
return;
}
res.setEncoding('utf8');
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(data));
});
request.on('error', reject);
request.end();
return promise;
}
export function fetchUrl(url: string): Promise<string> {
if (isNode)
return nodeFetch(url);
return fetch(url).then(response => {
if (!response.ok)
throw new Error('HTTP ' + response.status + ' ' + response.statusText);
return response.text();
});
}
class WebSocketTransport implements ConnectionTransport {
private _ws: WebSocket;
onmessage?: (message: string) => void;
onclose?: () => void;
constructor(ws: WebSocket) {
this._ws = ws;
this._ws.addEventListener('message', event => {
if (this.onmessage)
this.onmessage.call(null, event.data);
});
this._ws.addEventListener('close', event => {
if (this.onclose)
this.onclose.call(null);
});
// Silently ignore all errors - we don't know what to do with them.
this._ws.addEventListener('error', () => {});
}
send(message: string) {
this._ws.send(message);
}
close() {
this._ws.close();
}
}
export function createWebSocketTransport(url: string): Promise<ConnectionTransport> {
return new Promise((resolve, reject) => {
const ws = (isNode ? new NodeWebSocket(url, [], {
perMessageDeflate: false,
maxPayload: 256 * 1024 * 1024, // 256Mb
}) : new WebSocket(url)) as WebSocket;
ws.addEventListener('open', () => resolve(new WebSocketTransport(ws)));
ws.addEventListener('error', reject);
});
}

View file

@ -19,21 +19,20 @@ import * as extract from 'extract-zip';
import * as fs from 'fs';
import * as ProxyAgent from 'https-proxy-agent';
import * as path from 'path';
import * as platform from './platform';
// @ts-ignore
import * as platform from '../platform';
import { getProxyForUrl } from 'proxy-from-env';
import * as removeRecursive from 'rimraf';
import * as URL from 'url';
import { assert } from './helper';
import { assert } from '../helper';
const readdirAsync = platform.promisify(fs.readdir.bind(fs));
const mkdirAsync = platform.promisify(fs.mkdir.bind(fs));
const unlinkAsync = platform.promisify(fs.unlink.bind(fs));
const chmodAsync = platform.promisify(fs.chmod.bind(fs));
function existsAsync(filePath) {
let fulfill = null;
const promise = new Promise(x => fulfill = x);
function existsAsync(filePath: string): Promise<boolean> {
let fulfill: (exists: boolean) => void;
const promise = new Promise<boolean>(x => fulfill = x);
fs.access(filePath, err => fulfill(!err));
return promise;
}

View file

@ -15,25 +15,22 @@
* limitations under the License.
*/
import * as http from 'http';
import * as https from 'https';
import * as URL from 'url';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as util from 'util';
import { BrowserFetcher, BrowserFetcherOptions, BrowserFetcherRevisionInfo, OnProgressCallback } from '../browserFetcher';
import { BrowserFetcher, BrowserFetcherOptions, BrowserFetcherRevisionInfo, OnProgressCallback } from '../server/browserFetcher';
import { DeviceDescriptors } from '../deviceDescriptors';
import * as Errors from '../errors';
import * as types from '../types';
import { assert } from '../helper';
import { ConnectionTransport, WebSocketTransport, SlowMoTransport, PipeTransport } from '../transport';
import { CRBrowser } from './crBrowser';
import { CRBrowser, CRConnectOptions, createTransport } from '../chromium/crBrowser';
import * as platform from '../platform';
import { TimeoutError } from '../errors';
import { launchProcess, waitForLine } from '../processLauncher';
import { launchProcess, waitForLine } from '../server/processLauncher';
import { ChildProcess } from 'child_process';
import { CRConnection } from './crConnection';
import { CRConnection } from '../chromium/crConnection';
import { PipeTransport } from './pipeTransport';
export type SlowMoOptions = {
slowMo?: number,
@ -58,24 +55,17 @@ export type LaunchOptions = ChromeArgOptions & SlowMoOptions & {
pipe?: boolean,
};
export type ConnectOptions = SlowMoOptions & {
browserWSEndpoint?: string;
browserURL?: string;
transport?: ConnectionTransport;
};
export class CRBrowserServer {
private _process: ChildProcess;
private _connectOptions: ConnectOptions;
private _connectOptions: CRConnectOptions;
constructor(process: ChildProcess, connectOptions: ConnectOptions) {
constructor(process: ChildProcess, connectOptions: CRConnectOptions) {
this._process = process;
this._connectOptions = connectOptions;
}
async connect(): Promise<CRBrowser> {
const transport = await createTransport(this._connectOptions);
return CRBrowser.create(transport);
return CRBrowser.connect(this._connectOptions);
}
process(): ChildProcess {
@ -86,7 +76,7 @@ export class CRBrowserServer {
return this._connectOptions.browserWSEndpoint || null;
}
connectOptions(): ConnectOptions {
connectOptions(): CRConnectOptions {
return this._connectOptions;
}
@ -179,7 +169,7 @@ export class CRPlaywright {
let server: CRBrowserServer | undefined;
try {
let connectOptions: ConnectOptions | undefined;
let connectOptions: CRConnectOptions | undefined;
let browserWSEndpoint: string = '';
if (!usePipe) {
const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Chrome! The only Chrome revision guaranteed to work is r${this._revision}`);
@ -199,9 +189,8 @@ export class CRPlaywright {
}
}
async connect(options: ConnectOptions): Promise<CRBrowser> {
const transport = await createTransport(options);
return CRBrowser.create(transport);
async connect(options: CRConnectOptions): Promise<CRBrowser> {
return CRBrowser.connect(options);
}
executablePath(): string {
@ -328,49 +317,3 @@ const DEFAULT_ARGS = [
'--password-store=basic',
'--use-mock-keychain',
];
function getWSEndpoint(browserURL: string): Promise<string> {
let resolve: (url: string) => void;
let reject: (e: Error) => void;
const promise = new Promise<string>((res, rej) => { resolve = res; reject = rej; });
const endpointURL = URL.resolve(browserURL, '/json/version');
const protocol = endpointURL.startsWith('https') ? https : http;
const requestOptions = Object.assign(URL.parse(endpointURL), { method: 'GET' });
const request = protocol.request(requestOptions, res => {
let data = '';
if (res.statusCode !== 200) {
// Consume response data to free up memory.
res.resume();
reject(new Error('HTTP ' + res.statusCode));
return;
}
res.setEncoding('utf8');
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(JSON.parse(data).webSocketDebuggerUrl));
});
request.on('error', reject);
request.end();
return promise.catch(e => {
e.message = `Failed to fetch browser webSocket url from ${endpointURL}: ` + e.message;
throw e;
});
}
async function createTransport(options: ConnectOptions): Promise<ConnectionTransport> {
assert(Number(!!options.browserWSEndpoint) + Number(!!options.browserURL) + Number(!!options.transport) === 1, 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to playwright.connect');
let transport: ConnectionTransport | undefined;
let connectionURL: string = '';
if (options.transport) {
transport = options.transport;
} else if (options.browserWSEndpoint) {
connectionURL = options.browserWSEndpoint;
transport = await WebSocketTransport.create(options.browserWSEndpoint);
} else if (options.browserURL) {
connectionURL = await getWSEndpoint(options.browserURL);
transport = await WebSocketTransport.create(connectionURL);
}
return SlowMoTransport.wrap(transport, options.slowMo);
}

View file

@ -0,0 +1,73 @@
/**
* Copyright 2018 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.
*/
import { debugError, helper, RegisteredListener } from '../helper';
import { ConnectionTransport } from '../transport';
export class PipeTransport implements ConnectionTransport {
private _pipeWrite: NodeJS.WritableStream;
private _pendingMessage = '';
private _eventListeners: RegisteredListener[];
onmessage?: (message: string) => void;
onclose?: () => void;
constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream) {
this._pipeWrite = pipeWrite;
this._eventListeners = [
helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)),
helper.addEventListener(pipeRead, 'close', () => {
if (this.onclose)
this.onclose.call(null);
}),
helper.addEventListener(pipeRead, 'error', debugError),
helper.addEventListener(pipeWrite, 'error', debugError),
];
this.onmessage = null;
this.onclose = null;
}
send(message: string) {
this._pipeWrite.write(message);
this._pipeWrite.write('\0');
}
_dispatch(buffer: Buffer) {
let end = buffer.indexOf('\0');
if (end === -1) {
this._pendingMessage += buffer.toString();
return;
}
const message = this._pendingMessage + buffer.toString(undefined, 0, end);
if (this.onmessage)
this.onmessage.call(null, message);
let start = end + 1;
end = buffer.indexOf('\0', start);
while (end !== -1) {
if (this.onmessage)
this.onmessage.call(null, buffer.toString(undefined, start, end));
start = end + 1;
end = buffer.indexOf('\0', start);
}
this._pendingMessage = buffer.toString(undefined, start);
}
close() {
this._pipeWrite = null;
helper.removeEventListeners(this._eventListeners);
}
}

View file

@ -18,10 +18,10 @@
import * as childProcess from 'child_process';
import * as stream from 'stream';
import * as removeFolder from 'rimraf';
import { helper } from './helper';
import { helper } from '../helper';
import * as readline from 'readline';
import { TimeoutError } from './errors';
import * as platform from './platform';
import { TimeoutError } from '../errors';
import * as platform from '../platform';
const removeFolderAsync = platform.promisify(removeFolder);

View file

@ -15,9 +15,6 @@
* limitations under the License.
*/
import * as WebSocket from 'ws';
import { debugError, helper, RegisteredListener } from './helper';
export interface ConnectionTransport {
send(s: string): void;
close(): void;
@ -25,100 +22,6 @@ export interface ConnectionTransport {
onclose?: () => void,
}
export class WebSocketTransport implements ConnectionTransport {
private _ws: WebSocket;
onmessage?: (message: string) => void;
onclose?: () => void;
static create(url: string): Promise<WebSocketTransport> {
return new Promise((resolve, reject) => {
const ws = new WebSocket(url, [], {
perMessageDeflate: false,
maxPayload: 256 * 1024 * 1024, // 256Mb
});
ws.addEventListener('open', () => resolve(new WebSocketTransport(ws, url)));
ws.addEventListener('error', reject);
});
}
constructor(ws: WebSocket, url: string) {
this._ws = ws;
this._ws.addEventListener('message', event => {
if (this.onmessage)
this.onmessage.call(null, event.data);
});
this._ws.addEventListener('close', event => {
if (this.onclose)
this.onclose.call(null);
});
// Silently ignore all errors - we don't know what to do with them.
this._ws.addEventListener('error', () => {});
}
send(message: string) {
this._ws.send(message);
}
close() {
this._ws.close();
}
}
export class PipeTransport implements ConnectionTransport {
private _pipeWrite: NodeJS.WritableStream;
private _pendingMessage = '';
private _eventListeners: RegisteredListener[];
onmessage?: (message: string) => void;
onclose?: () => void;
constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream) {
this._pipeWrite = pipeWrite;
this._eventListeners = [
helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)),
helper.addEventListener(pipeRead, 'close', () => {
if (this.onclose)
this.onclose.call(null);
}),
helper.addEventListener(pipeRead, 'error', debugError),
helper.addEventListener(pipeWrite, 'error', debugError),
];
this.onmessage = null;
this.onclose = null;
}
send(message: string) {
this._pipeWrite.write(message);
this._pipeWrite.write('\0');
}
_dispatch(buffer: Buffer) {
let end = buffer.indexOf('\0');
if (end === -1) {
this._pendingMessage += buffer.toString();
return;
}
const message = this._pendingMessage + buffer.toString(undefined, 0, end);
if (this.onmessage)
this.onmessage.call(null, message);
let start = end + 1;
end = buffer.indexOf('\0', start);
while (end !== -1) {
if (this.onmessage)
this.onmessage.call(null, buffer.toString(undefined, start, end));
start = end + 1;
end = buffer.indexOf('\0', start);
}
this._pendingMessage = buffer.toString(undefined, start);
}
close() {
this._pipeWrite = null;
helper.removeEventListeners(this._eventListeners);
}
}
export class SlowMoTransport {
private readonly _delay: number;
private readonly _delegate: ConnectionTransport;

View file

@ -17,14 +17,15 @@
import { assert } from '../helper';
import { WKBrowser } from './wkBrowser';
import { BrowserFetcher, BrowserFetcherOptions } from '../browserFetcher';
import { PipeTransport, SlowMoTransport } from '../transport';
import { BrowserFetcher, BrowserFetcherOptions } from '../server/browserFetcher';
import { SlowMoTransport } from '../transport';
import { execSync } from 'child_process';
import * as path from 'path';
import * as util from 'util';
import * as os from 'os';
import { launchProcess } from '../processLauncher';
import { launchProcess } from '../server/processLauncher';
import { BrowserServer } from '../browser';
import { PipeTransport } from '../server/pipeTransport';
const DEFAULT_ARGS = [
];

View file

@ -15,7 +15,7 @@
* limitations under the License.
*/
import * as browsers from '../browser';
import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetcherRevisionInfo } from '../browserFetcher';
import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetcherRevisionInfo } from '../server/browserFetcher';
import { DeviceDescriptors } from '../deviceDescriptors';
import * as Errors from '../errors';
import * as types from '../types';

View file

@ -98,11 +98,12 @@ function checkSources(sources) {
excludeClasses.add(className);
}
}
if (!node.getSourceFile().fileName.endsWith('platform.ts')) {
const fileName = node.getSourceFile().fileName;
if (!fileName.endsWith('platform.ts') && !fileName.includes('src/server/')) {
// Only relative imports.
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
const module = node.moduleSpecifier.text;
if (!module.startsWith('.')) {
if (!module.startsWith('.') || path.resolve(path.dirname(fileName), module).includes('src/server')) {
const lac = ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.moduleSpecifier.pos);
errors.push(`Disallowed import "${module}" at ${node.getSourceFile().fileName}:${lac.line + 1}`);
}