chore(server): validate/convert protocol both ways (#14811)

Previously, we only validated/converted on the way to the server,
but not from the server.

Validating both ways catches issues earlier, and allows us to
perform automatic conversions, for example only converting
buffers to base64 when sending over wire.
This commit is contained in:
Dmitry Gozman 2022-07-01 09:58:07 -07:00 committed by GitHub
parent c4d2342339
commit 82032be368
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 2477 additions and 1693 deletions

View File

@ -16,8 +16,7 @@
import { EventEmitter } from 'events';
import type * as channels from '../protocol/channels';
import type { Validator } from '../protocol/validator';
import { createScheme, ValidationError } from '../protocol/validator';
import { maybeFindValidator, ValidationError, type ValidatorContext } from '../protocol/validator';
import { debugLogger } from '../common/debugLogger';
import type { ParsedStackTrace } from '../utils/stackTrace';
import { captureRawStack, captureStackTrace } from '../utils/stackTrace';
@ -81,10 +80,8 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
private _createChannel(base: Object): T {
const channel = new Proxy(base, {
get: (obj: any, prop) => {
if (prop === 'debugScopeState')
return (params: any) => this._connection.sendMessageToServer(this, prop, params, null);
if (typeof prop === 'string') {
const validator = scheme[paramsName(this._type, prop)];
const validator = maybeFindValidator(this._type, prop, 'Params');
if (validator) {
return (params: any) => {
return this._wrapApiCall(apiZone => {
@ -92,7 +89,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
apiZone.reported = true;
if (csi && stackTrace && stackTrace.apiName)
csi.onApiCallBegin(renderCallWithParams(stackTrace.apiName, params), stackTrace, callCookie);
return this._connection.sendMessageToServer(this, prop, validator(params, ''), stackTrace);
return this._connection.sendMessageToServer(this, this._type, prop, validator(params, '', { tChannelImpl: tChannelImplToWire }), stackTrace);
});
};
}
@ -162,12 +159,8 @@ function logApiCall(logger: Logger | undefined, message: string, isNested: boole
debugLogger.log('api', message);
}
function paramsName(type: string, method: string) {
return type + method[0].toUpperCase() + method.substring(1) + 'Params';
}
const paramsToRender = ['url', 'selector', 'text', 'key'];
export function renderCallWithParams(apiName: string, params: any) {
function renderCallWithParams(apiName: string, params: any) {
const paramsArray = [];
if (params) {
for (const name of paramsToRender) {
@ -179,15 +172,11 @@ export function renderCallWithParams(apiName: string, params: any) {
return apiName + paramsText;
}
const tChannel = (name: string): Validator => {
return (arg: any, path: string) => {
if (arg._object instanceof ChannelOwner && (name === '*' || arg._object._type === name))
return { guid: arg._object._guid };
throw new ValidationError(`${path}: expected ${name}`);
};
};
const scheme = createScheme(tChannel);
function tChannelImplToWire(names: '*' | string[], arg: any, path: string, context: ValidatorContext) {
if (arg._object instanceof ChannelOwner && (names === '*' || names.includes(arg._object._type)))
return { guid: arg._object._guid };
throw new ValidationError(`${path}: expected channel ${names.toString()}`);
}
type ApiZone = {
stackTrace: ParsedStackTrace;

View File

@ -43,6 +43,7 @@ import { JsonPipe } from './jsonPipe';
import { APIRequestContext } from './fetch';
import { LocalUtils } from './localUtils';
import { Tracing } from './tracing';
import { findValidator, ValidationError, type ValidatorContext } from '../protocol/validator';
class Root extends ChannelOwner<channels.RootChannel> {
constructor(connection: Connection) {
@ -63,7 +64,7 @@ export class Connection extends EventEmitter {
readonly _objects = new Map<string, ChannelOwner>();
onmessage = (message: object): void => {};
private _lastId = 0;
private _callbacks = new Map<number, { resolve: (a: any) => void, reject: (a: Error) => void, stackTrace: ParsedStackTrace | null }>();
private _callbacks = new Map<number, { resolve: (a: any) => void, reject: (a: Error) => void, stackTrace: ParsedStackTrace | null, type: string, method: string }>();
private _rootObject: Root;
private _closedErrorMessage: string | undefined;
private _isRemote = false;
@ -101,7 +102,7 @@ export class Connection extends EventEmitter {
return this._objects.get(guid)!;
}
async sendMessageToServer(object: ChannelOwner, method: string, params: any, stackTrace: ParsedStackTrace | null): Promise<any> {
async sendMessageToServer(object: ChannelOwner, type: string, method: string, params: any, stackTrace: ParsedStackTrace | null): Promise<any> {
if (this._closedErrorMessage)
throw new Error(this._closedErrorMessage);
@ -114,11 +115,7 @@ export class Connection extends EventEmitter {
const metadata: channels.Metadata = { stack: frames, apiName, internal: !apiName };
this.onmessage({ ...converted, metadata });
return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, stackTrace }));
}
_debugScopeState(): any {
return this._rootObject._debugScopeState();
return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, stackTrace, type, method }));
}
dispatch(message: object) {
@ -132,10 +129,12 @@ export class Connection extends EventEmitter {
if (!callback)
throw new Error(`Cannot find command to respond: ${id}`);
this._callbacks.delete(id);
if (error && !result)
if (error && !result) {
callback.reject(parseError(error));
else
callback.resolve(this._replaceGuidsWithChannels(result));
} else {
const validator = findValidator(callback.type, callback.method, 'Result');
callback.resolve(validator(result, '', { tChannelImpl: this._tChannelImplFromWire.bind(this) }));
}
return;
}
@ -154,7 +153,8 @@ export class Connection extends EventEmitter {
const object = this._objects.get(guid);
if (!object)
throw new Error(`Cannot find object to emit "${method}": ${guid}`);
(object._channel as any).emit(method, object._type === 'JsonPipe' ? params : this._replaceGuidsWithChannels(params));
const validator = findValidator(object._type, method, 'Event');
(object._channel as any).emit(method, validator(params, '', { tChannelImpl: this._tChannelImplFromWire.bind(this) }));
}
close(errorMessage: string = 'Connection closed') {
@ -165,20 +165,14 @@ export class Connection extends EventEmitter {
this.emit('close');
}
private _replaceGuidsWithChannels(payload: any): any {
if (!payload)
return payload;
if (Array.isArray(payload))
return payload.map(p => this._replaceGuidsWithChannels(p));
if (payload.guid && this._objects.has(payload.guid))
return this._objects.get(payload.guid)!._channel;
if (typeof payload === 'object') {
const result: any = {};
for (const key of Object.keys(payload))
result[key] = this._replaceGuidsWithChannels(payload[key]);
return result;
private _tChannelImplFromWire(names: '*' | string[], arg: any, path: string, context: ValidatorContext) {
if (arg && typeof arg === 'object' && typeof arg.guid === 'string' && this._objects.has(arg.guid)) {
const object = this._objects.get(arg.guid)!;
if (names !== '*' && !names.includes(object._type))
throw new ValidationError(`${path}: expected channel ${names.toString()}`);
return object._channel;
}
return payload;
throw new ValidationError(`${path}: expected channel ${names.toString()}`);
}
private _createRemoteObject(parentGuid: string, type: string, guid: string, initializer: any): any {
@ -186,7 +180,8 @@ export class Connection extends EventEmitter {
if (!parent)
throw new Error(`Cannot find parent object ${parentGuid} to create ${guid}`);
let result: ChannelOwner<any>;
initializer = this._replaceGuidsWithChannels(initializer);
const validator = findValidator(type, '', 'Initializer');
initializer = validator(initializer, '', { tChannelImpl: this._tChannelImplFromWire.bind(this) });
switch (type) {
case 'Android':
result = new Android(parent, type, guid, initializer);

View File

@ -42,7 +42,7 @@ export function createInProcessPlaywright(): PlaywrightAPI {
dispatcherConnection.onmessage = message => setImmediate(() => clientConnection.dispatch(message));
clientConnection.onmessage = message => setImmediate(() => dispatcherConnection.dispatch(message));
clientConnection.toImpl = (x: any) => dispatcherConnection._dispatchers.get(x._guid)!._object;
clientConnection.toImpl = (x: any) => x ? dispatcherConnection._dispatchers.get(x._guid)!._object : dispatcherConnection._dispatchers.get('');
(playwrightAPI as any)._toImpl = clientConnection.toImpl;
return playwrightAPI;
}

View File

@ -289,7 +289,7 @@ APIRequestContext:
parameters:
fetchUid: string
returns:
binary?: binary
binary: binary?
fetchLog:
parameters:

File diff suppressed because it is too large Load Diff

View File

@ -17,67 +17,85 @@
import { isUnderTest } from '../utils';
export class ValidationError extends Error {}
export type Validator = (arg: any, path: string) => any;
export type Validator = (arg: any, path: string, context: ValidatorContext) => any;
export type ValidatorContext = {
tChannelImpl: (names: '*' | string[], arg: any, path: string, context: ValidatorContext) => any,
};
export const scheme: { [key: string]: Validator } = {};
export const tNumber: Validator = (arg: any, path: string) => {
export function findValidator(type: string, method: string, kind: 'Initializer' | 'Event' | 'Params' | 'Result'): Validator {
const validator = maybeFindValidator(type, method, kind);
if (!validator)
throw new ValidationError(`Unknown scheme for ${kind}: ${type}.${method}`);
return validator;
}
export function maybeFindValidator(type: string, method: string, kind: 'Initializer' | 'Event' | 'Params' | 'Result'): Validator | undefined {
const schemeName = type + (kind === 'Initializer' ? '' : method[0].toUpperCase() + method.substring(1)) + kind;
return scheme[schemeName];
}
export function createMetadataValidator(): Validator {
return tOptional(scheme['Metadata']);
}
export const tNumber: Validator = (arg: any, path: string, context: ValidatorContext) => {
if (arg instanceof Number)
return arg.valueOf();
if (typeof arg === 'number')
return arg;
throw new ValidationError(`${path}: expected number, got ${typeof arg}`);
};
export const tBoolean: Validator = (arg: any, path: string) => {
export const tBoolean: Validator = (arg: any, path: string, context: ValidatorContext) => {
if (arg instanceof Boolean)
return arg.valueOf();
if (typeof arg === 'boolean')
return arg;
throw new ValidationError(`${path}: expected boolean, got ${typeof arg}`);
};
export const tString: Validator = (arg: any, path: string) => {
export const tString: Validator = (arg: any, path: string, context: ValidatorContext) => {
if (arg instanceof String)
return arg.valueOf();
if (typeof arg === 'string')
return arg;
throw new ValidationError(`${path}: expected string, got ${typeof arg}`);
};
export const tBinary: Validator = (arg: any, path: string) => {
export const tBinary: Validator = (arg: any, path: string, context: ValidatorContext) => {
if (arg instanceof String)
return arg.valueOf();
if (typeof arg === 'string')
return arg;
throw new ValidationError(`${path}: expected base64-encoded buffer, got ${typeof arg}`);
};
export const tUndefined: Validator = (arg: any, path: string) => {
export const tUndefined: Validator = (arg: any, path: string, context: ValidatorContext) => {
if (Object.is(arg, undefined))
return arg;
throw new ValidationError(`${path}: expected undefined, got ${typeof arg}`);
};
export const tAny: Validator = (arg: any, path: string) => {
export const tAny: Validator = (arg: any, path: string, context: ValidatorContext) => {
return arg;
};
export const tOptional = (v: Validator): Validator => {
return (arg: any, path: string) => {
return (arg: any, path: string, context: ValidatorContext) => {
if (Object.is(arg, undefined))
return arg;
return v(arg, path);
return v(arg, path, context);
};
};
export const tArray = (v: Validator): Validator => {
return (arg: any, path: string) => {
return (arg: any, path: string, context: ValidatorContext) => {
if (!Array.isArray(arg))
throw new ValidationError(`${path}: expected array, got ${typeof arg}`);
return arg.map((x, index) => v(x, path + '[' + index + ']'));
return arg.map((x, index) => v(x, path + '[' + index + ']', context));
};
};
export const tObject = (s: { [key: string]: Validator }): Validator => {
return (arg: any, path: string) => {
return (arg: any, path: string, context: ValidatorContext) => {
if (Object.is(arg, null))
throw new ValidationError(`${path}: expected object, got null`);
if (typeof arg !== 'object')
throw new ValidationError(`${path}: expected object, got ${typeof arg}`);
const result: any = {};
for (const [key, v] of Object.entries(s)) {
const value = v(arg[key], path ? path + '.' + key : key);
const value = v(arg[key], path ? path + '.' + key : key, context);
if (!Object.is(value, undefined))
result[key] = value;
}
@ -91,9 +109,22 @@ export const tObject = (s: { [key: string]: Validator }): Validator => {
};
};
export const tEnum = (e: string[]): Validator => {
return (arg: any, path: string) => {
return (arg: any, path: string, context: ValidatorContext) => {
if (!e.includes(arg))
throw new ValidationError(`${path}: expected one of (${e.join('|')})`);
return arg;
};
};
export const tChannel = (names: '*' | string[]): Validator => {
return (arg: any, path: string, context: ValidatorContext) => {
return context.tChannelImpl(names, arg, path, context);
};
};
export const tType = (name: string): Validator => {
return (arg: any, path: string, context: ValidatorContext) => {
const v = scheme[name];
if (!v)
throw new ValidationError(path + ': unknown type "' + name + '"');
return v(arg, path, context);
};
};

View File

@ -17,10 +17,8 @@
import { EventEmitter } from 'events';
import type * as channels from '../../protocol/channels';
import { serializeError } from '../../protocol/serializers';
import type { Validator } from '../../protocol/validator';
import { createScheme, ValidationError } from '../../protocol/validator';
import { findValidator, ValidationError, createMetadataValidator, type ValidatorContext } from '../../protocol/validator';
import { assert, debugAssert, isUnderTest, monotonicTime } from '../../utils';
import { tOptional } from '../../protocol/validatorPrimitives';
import { kBrowserOrContextClosedError } from '../../common/errors';
import type { CallMetadata } from '../instrumentation';
import { SdkObject } from '../instrumentation';
@ -28,6 +26,7 @@ import { rewriteErrorMessage } from '../../utils/stackTrace';
import type { PlaywrightDispatcher } from './playwrightDispatcher';
export const dispatcherSymbol = Symbol('dispatcher');
const metadataValidator = createMetadataValidator();
export function lookupDispatcher<DispatcherType>(object: any): DispatcherType {
const result = object[dispatcherSymbol];
@ -79,7 +78,7 @@ export class Dispatcher<Type extends { guid: string }, ChannelType> extends Even
(object as any)[dispatcherSymbol] = this;
if (this._parent)
this._connection.sendMessageToClient(this._parent._guid, type, '__create__', { type, initializer, guid }, this._parent._object);
this._connection.sendCreate(this._parent, type, guid, initializer, this._parent._object);
}
_dispatchEvent<T extends keyof channels.EventsTraits<ChannelType>>(method: T, params?: channels.EventsTraits<ChannelType>[T]) {
@ -90,7 +89,7 @@ export class Dispatcher<Type extends { guid: string }, ChannelType> extends Even
return;
}
const sdkObject = this._object instanceof SdkObject ? this._object : undefined;
this._connection.sendMessageToClient(this._guid, this._type, method as string, params, sdkObject);
this._connection.sendEvent(this, method as string, params, sdkObject);
}
protected _dispose() {
@ -108,7 +107,7 @@ export class Dispatcher<Type extends { guid: string }, ChannelType> extends Even
this._dispatchers.clear();
if (this._isScope)
this._connection.sendMessageToClient(this._guid, this._type, '__dispose__', {});
this._connection.sendDispose(this);
}
_debugScopeState(): any {
@ -144,12 +143,25 @@ export class Root extends Dispatcher<{ guid: '' }, any> {
export class DispatcherConnection {
readonly _dispatchers = new Map<string, Dispatcher<any, any>>();
onmessage = (message: object) => {};
private _validateParams: (type: string, method: string, params: any) => any;
private _validateMetadata: (metadata: any) => { stack?: channels.StackFrame[] };
private _waitOperations = new Map<string, CallMetadata>();
sendMessageToClient(guid: string, type: string, method: string, params: any, sdkObject?: SdkObject) {
params = this._replaceDispatchersWithGuids(params);
sendEvent(dispatcher: Dispatcher<any, any>, event: string, params: any, sdkObject?: SdkObject) {
const validator = findValidator(dispatcher._type, event, 'Event');
params = validator(params, '', { tChannelImpl: this._tChannelImplToWire.bind(this) });
this._sendMessageToClient(dispatcher._guid, dispatcher._type, event, params, sdkObject);
}
sendCreate(parent: Dispatcher<any, any>, type: string, guid: string, initializer: any, sdkObject?: SdkObject) {
const validator = findValidator(type, '', 'Initializer');
initializer = validator(initializer, '', { tChannelImpl: this._tChannelImplToWire.bind(this) });
this._sendMessageToClient(parent._guid, type, '__create__', { type, initializer, guid }, sdkObject);
}
sendDispose(dispatcher: Dispatcher<any, any>) {
this._sendMessageToClient(dispatcher._guid, dispatcher._type, '__dispose__', {});
}
private _sendMessageToClient(guid: string, type: string, method: string, params: any, sdkObject?: SdkObject) {
if (sdkObject) {
const eventMetadata: CallMetadata = {
id: `event@${++lastEventId}`,
@ -170,31 +182,26 @@ export class DispatcherConnection {
this.onmessage({ guid, method, params });
}
constructor() {
const tChannel = (name: string): Validator => {
return (arg: any, path: string) => {
if (arg && typeof arg === 'object' && typeof arg.guid === 'string') {
const guid = arg.guid;
const dispatcher = this._dispatchers.get(guid);
if (!dispatcher)
throw new ValidationError(`${path}: no object with guid ${guid}`);
if (name !== '*' && dispatcher._type !== name)
throw new ValidationError(`${path}: object with guid ${guid} has type ${dispatcher._type}, expected ${name}`);
return dispatcher;
}
throw new ValidationError(`${path}: expected ${name}`);
};
};
const scheme = createScheme(tChannel);
this._validateParams = (type: string, method: string, params: any): any => {
const name = type + method[0].toUpperCase() + method.substring(1) + 'Params';
if (!scheme[name])
throw new ValidationError(`Unknown scheme for ${type}.${method}`);
return scheme[name](params, '');
};
this._validateMetadata = (metadata: any): any => {
return tOptional(scheme['Metadata'])(metadata, '');
};
private _tChannelImplFromWire(names: '*' | string[], arg: any, path: string, context: ValidatorContext): any {
if (arg && typeof arg === 'object' && typeof arg.guid === 'string') {
const guid = arg.guid;
const dispatcher = this._dispatchers.get(guid);
if (!dispatcher)
throw new ValidationError(`${path}: no object with guid ${guid}`);
if (names !== '*' && !names.includes(dispatcher._type))
throw new ValidationError(`${path}: object with guid ${guid} has type ${dispatcher._type}, expected ${names.toString()}`);
return dispatcher;
}
throw new ValidationError(`${path}: expected guid for ${names.toString()}`);
}
private _tChannelImplToWire(names: '*' | string[], arg: any, path: string, context: ValidatorContext): any {
if (arg instanceof Dispatcher) {
if (names !== '*' && !names.includes(arg._type))
throw new ValidationError(`${path}: dispatcher with guid ${arg._guid} has type ${arg._type}, expected ${names.toString()}`);
return { guid: arg._guid };
}
throw new ValidationError(`${path}: expected dispatcher ${names.toString()}`);
}
async dispatch(message: object) {
@ -204,17 +211,13 @@ export class DispatcherConnection {
this.onmessage({ id, error: serializeError(new Error(kBrowserOrContextClosedError)) });
return;
}
if (method === 'debugScopeState') {
const rootDispatcher = this._dispatchers.get('')!;
this.onmessage({ id, result: rootDispatcher._debugScopeState() });
return;
}
let validParams: any;
let validMetadata: channels.Metadata;
try {
validParams = this._validateParams(dispatcher._type, method, params);
validMetadata = this._validateMetadata(metadata);
const validator = findValidator(dispatcher._type, method, 'Params');
validParams = validator(params, '', { tChannelImpl: this._tChannelImplFromWire.bind(this) });
validMetadata = metadataValidator(metadata, '', { tChannelImpl: this._tChannelImplFromWire.bind(this) });
if (typeof (dispatcher as any)[method] !== 'function')
throw new Error(`Mismatching dispatcher: "${dispatcher._type}" does not implement "${method}"`);
} catch (e) {
@ -273,7 +276,8 @@ export class DispatcherConnection {
await sdkObject?.instrumentation.onBeforeCall(sdkObject, callMetadata);
try {
const result = await (dispatcher as any)[method](validParams, callMetadata);
callMetadata.result = this._replaceDispatchersWithGuids(result);
const validator = findValidator(dispatcher._type, method, 'Result');
callMetadata.result = validator(result, '', { tChannelImpl: this._tChannelImplToWire.bind(this) });
} catch (e) {
// Dispatching error
// We want original, unmodified error in metadata.
@ -293,22 +297,6 @@ export class DispatcherConnection {
response.error = error;
this.onmessage(response);
}
private _replaceDispatchersWithGuids(payload: any): any {
if (!payload)
return payload;
if (payload instanceof Dispatcher)
return { guid: payload._guid };
if (Array.isArray(payload))
return payload.map(p => this._replaceDispatchersWithGuids(p));
if (typeof payload === 'object') {
const result: any = {};
for (const key of Object.keys(payload))
result[key] = this._replaceDispatchersWithGuids(payload[key]);
return result;
}
return payload;
}
}
function formatLogRecording(log: string[]): string {

View File

@ -24,7 +24,7 @@ export type TestModeWorkerOptions = {
export type TestModeWorkerFixtures = {
playwright: typeof import('@playwright/test');
toImpl: (rpcObject: any) => any;
toImpl: (rpcObject?: any) => any;
};
export const testModeTest = test.extend<{}, TestModeWorkerOptions & TestModeWorkerFixtures>({

View File

@ -16,19 +16,27 @@
*/
import domain from 'domain';
import { playwrightTest as it, expect } from '../config/browserTest';
import { playwrightTest, expect } from '../config/browserTest';
// Use something worker-scoped (e.g. launch args) to force a new worker for this file.
// Use something worker-scoped (e.g. expectScopeState) forces a new worker for this file.
// Otherwise, a browser launched for other tests in this worker will affect the expectations.
it.use({
launchOptions: async ({ launchOptions }, use) => {
await use({ ...launchOptions, args: [] });
}
const it = playwrightTest.extend<{}, { expectScopeState: (object: any, golden: any) => void }>({
expectScopeState: [ async ({ toImpl }, use) => {
await use((object, golden) => {
golden = trimGuids(golden);
const remoteRoot = toImpl();
const remoteState = trimGuids(remoteRoot._debugScopeState());
const localRoot = object._connection._rootObject;
const localState = trimGuids(localRoot._debugScopeState());
expect(localState).toEqual(golden);
expect(remoteState).toEqual(golden);
});
}, { scope: 'worker' }],
});
it.skip(({ mode }) => mode === 'service');
it('should scope context handles', async ({ browserType, server }) => {
it('should scope context handles', async ({ browserType, server, expectScopeState }) => {
const browser = await browserType.launch();
const GOLDEN_PRECONDITION = {
_guid: '',
@ -82,7 +90,7 @@ it('should scope context handles', async ({ browserType, server }) => {
await browser.close();
});
it('should scope CDPSession handles', async ({ browserType, browserName }) => {
it('should scope CDPSession handles', async ({ browserType, browserName, expectScopeState }) => {
it.skip(browserName !== 'chromium');
const browser = await browserType.launch();
@ -128,7 +136,7 @@ it('should scope CDPSession handles', async ({ browserType, browserName }) => {
await browser.close();
});
it('should scope browser handles', async ({ browserType }) => {
it('should scope browser handles', async ({ browserType, expectScopeState }) => {
const GOLDEN_PRECONDITION = {
_guid: '',
objects: [
@ -204,14 +212,6 @@ it('should work with the domain module', async ({ browserType, server, browserNa
throw err;
});
async function expectScopeState(object, golden) {
golden = trimGuids(golden);
const remoteState = trimGuids(await object._channel.debugScopeState());
const localState = trimGuids(object._connection._debugScopeState());
expect(localState).toEqual(golden);
expect(remoteState).toEqual(golden);
}
function compareObjects(a, b) {
if (a._guid !== b._guid)
return a._guid.localeCompare(b._guid);

View File

@ -22,8 +22,7 @@ const os = require('os');
const path = require('path');
const yaml = require('yaml');
const channels = new Set();
const inherits = new Map();
const channels = new Map();
const mixins = new Map();
function raise(item) {
@ -45,8 +44,11 @@ function inlineType(type, indent, wrapEnums = false) {
return { ts: 'any', scheme: 'tAny', optional };
if (['string', 'boolean', 'number', 'undefined'].includes(type))
return { ts: type, scheme: `t${titleCase(type)}`, optional };
if (channels.has(type))
return { ts: `${type}Channel`, scheme: `tChannel('${type}')` , optional };
if (channels.has(type)) {
let derived = derivedClasses.get(type) || [];
derived = [...derived, type];
return { ts: `${type}Channel`, scheme: `tChannel([${derived.map(c => `'${c}'`).join(', ')}])` , optional };
}
if (type === 'Channel')
return { ts: `Channel`, scheme: `tChannel('*')`, optional };
return { ts: type, scheme: `tType('${type}')`, optional };
@ -149,24 +151,9 @@ const validator_ts = [
// This file is generated by ${path.basename(__filename)}, do not edit manually.
import type { Validator } from './validatorPrimitives';
import { ValidationError, tOptional, tObject, tBoolean, tNumber, tString, tAny, tEnum, tArray, tBinary } from './validatorPrimitives';
export type { Validator } from './validatorPrimitives';
export { ValidationError } from './validatorPrimitives';
type Scheme = { [key: string]: Validator };
export function createScheme(tChannel: (name: string) => Validator): Scheme {
const scheme: Scheme = {};
const tType = (name: string): Validator => {
return (arg: any, path: string) => {
const v = scheme[name];
if (!v)
throw new ValidationError(path + ': unknown type "' + name + '"');
return v(arg, path);
};
};
import { scheme, tOptional, tObject, tBoolean, tNumber, tString, tAny, tEnum, tArray, tBinary, tChannel, tType } from './validatorPrimitives';
export type { Validator, ValidatorContext } from './validatorPrimitives';
export { ValidationError, findValidator, maybeFindValidator, createMetadataValidator } from './validatorPrimitives';
`];
const tracingSnapshots = [];
@ -176,16 +163,12 @@ const yml = fs.readFileSync(path.join(__dirname, '..', 'packages', 'playwright-c
const protocol = yaml.parse(yml);
function addScheme(name, s) {
const lines = `scheme.${name} = ${s};`.split('\n');
validator_ts.push(...lines.map(line => ' ' + line));
validator_ts.push(`scheme.${name} = ${s};`);
}
for (const [name, value] of Object.entries(protocol)) {
if (value.type === 'interface') {
channels.add(name);
if (value.extends)
inherits.set(name, value.extends);
}
if (value.type === 'interface')
channels.set(name, value);
if (value.type === 'mixin')
mixins.set(name, value);
}
@ -239,6 +222,16 @@ for (const [name, item] of Object.entries(protocol)) {
const initializerName = channelName + 'Initializer';
channels_ts.push(`export type ${initializerName} = ${init.ts};`);
let ancestorInit = init;
let ancestor = item;
while (!ancestor.initializer) {
if (!ancestor.extends)
break;
ancestor = channels.get(ancestor.extends);
ancestorInit = objectType(ancestor.initializer || {}, '');
}
addScheme(`${channelName}Initializer`, ancestor.initializer ? ancestorInit.scheme : `tOptional(tObject({}))`);
channels_ts.push(`export interface ${channelName}EventTarget {`);
const ts_types = new Map();
@ -252,6 +245,9 @@ for (const [name, item] of Object.entries(protocol)) {
ts_types.set(paramsName, parameters.ts);
channels_ts.push(` on(event: '${eventName}', callback: (params: ${paramsName}) => void): this;`);
eventTypes.push({eventName, eventType: paramsName});
addScheme(paramsName, event.parameters ? parameters.scheme : `tOptional(tObject({}))`);
for (const derived of derivedClasses.get(channelName) || [])
addScheme(`${derived}${titleCase(eventName)}Event`, `tType('${paramsName}')`);
}
channels_ts.push(`}`);
@ -276,14 +272,15 @@ for (const [name, item] of Object.entries(protocol)) {
ts_types.set(paramsName, parameters.ts);
ts_types.set(optionsName, objectType(method.parameters || {}, '', true).ts);
addScheme(paramsName, method.parameters ? parameters.scheme : `tOptional(tObject({}))`);
for (const key of inherits.keys()) {
if (inherits.get(key) === channelName)
addScheme(`${key}${titleCase(methodName)}Params`, `tType('${paramsName}')`);
}
for (const derived of derivedClasses.get(channelName) || [])
addScheme(`${derived}${titleCase(methodName)}Params`, `tType('${paramsName}')`);
const resultName = `${channelName}${titleCase(methodName)}Result`;
const returns = objectType(method.returns || {}, '');
ts_types.set(resultName, method.returns ? returns.ts : 'void');
addScheme(resultName, method.returns ? returns.scheme : `tOptional(tObject({}))`);
for (const derived of derivedClasses.get(channelName) || [])
addScheme(`${derived}${titleCase(methodName)}Result`, `tType('${resultName}')`);
channels_ts.push(` ${methodName}(params${method.parameters ? '' : '?'}: ${paramsName}, metadata?: Metadata): Promise<${resultName}>;`);
}
@ -318,11 +315,6 @@ channels_ts.push(`export const pausesBeforeInputActions = new Set([
'${pausesBeforeInputActions.join(`',\n '`)}'
]);`);
validator_ts.push(`
return scheme;
}
`);
let hasChanges = false;
function writeFile(filePath, content) {