playwright/utils/generate_channels.js

318 lines
10 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* 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 fs = require('fs');
const path = require('path');
const channels = new Set();
function tokenize(source) {
const lines = source.split('\n').filter(line => {
const trimmed = line.trim();
return !!trimmed && trimmed[0] != '#';
});
const stack = [{ indent: -1, list: [], words: '' }];
for (const line of lines) {
const indent = line.length - line.trimLeft().length;
const o = { indent, list: [], words: line.split(' ').filter(word => !!word) };
let current = stack[stack.length - 1];
while (indent <= current.indent) {
stack.pop();
current = stack[stack.length - 1];
}
current.list.push(o);
stack.push(o);
}
return stack[0].list;
}
function raise(item) {
throw new Error(item.words.join(' '));
}
function titleCase(name) {
return name[0].toUpperCase() + name.substring(1);
}
function inlineType(item, indent) {
let type = item.words[1];
const array = type.endsWith('[]');
if (array)
type = type.substring(0, type.length - 2);
let inner = '';
if (type === 'enum') {
const literals = item.list.map(literal => {
if (literal.words.length > 1 || literal.list.length)
raise(literal);
return literal.words[0];
});
inner = literals.map(literal => `'${literal}'`).join(' | ');
if (array)
inner = `(${inner})`;
} else if (['string', 'boolean', 'number', 'undefined'].includes(type)) {
inner = type;
} else if (type === 'object') {
inner = `{\n${properties(item, indent + ' ')}\n${indent}}`;
} else if (type === 'binary') {
inner = 'Binary';
} else if (channels.has(type)) {
inner = type + 'Channel';
} else {
inner = type;
}
return inner + (array ? '[]' : '');
}
function inlineTypeScheme(item, indent) {
let type = item.words[1];
const array = type.endsWith('[]');
if (array)
type = type.substring(0, type.length - 2);
let inner = '';
if (type === 'enum') {
const literals = item.list.map(literal => {
if (literal.words.length > 1 || literal.list.length)
raise(literal);
return literal.words[0];
});
inner = `tEnum([${literals.map(literal => `'${literal}'`).join(', ')}])`;
} else if (['string', 'boolean', 'number', 'undefined'].includes(type)) {
inner = `t${titleCase(type)}`;
} else if (type === 'object') {
inner = `tObject({\n${propertiesScheme(item, indent + ' ')}\n${indent}})`;
} else if (type === 'binary') {
inner = 'tBinary';
} else if (channels.has(type)) {
inner = `tChannel('${type}')`;
} else if (type === 'Channel') {
inner = `tChannel('*')`;
} else {
inner = `tType('${type}')`;
}
return array ? `tArray(${inner})` : inner;
}
function properties(item, indent) {
const result = [];
for (const prop of item.list) {
if (prop.words.length !== 2)
raise(prop);
let name = prop.words[0];
if (!name.endsWith(':'))
raise(item);
name = name.substring(0, name.length - 1);
const optional = name.endsWith('?');
if (optional)
name = name.substring(0, name.length - 1);
result.push(`${indent}${name}${optional ? '?' : ''}: ${inlineType(prop, indent)},`);
}
return result.join('\n');
}
function propertiesScheme(item, indent) {
const result = [];
for (const prop of item.list) {
if (prop.words.length !== 2)
raise(prop);
let name = prop.words[0];
if (!name.endsWith(':'))
raise(item);
name = name.substring(0, name.length - 1);
const optional = name.endsWith('?');
if (optional)
name = name.substring(0, name.length - 1);
let type = inlineTypeScheme(prop, indent);
if (optional)
type = `tOptional(${type})`;
result.push(`${indent}${name}: ${type},`);
}
return result.join('\n');
}
function objectType(name, item, indent) {
if (!item.list.length)
return `export type ${name} = {};`;
return `export type ${name} = {\n${properties(item, indent)}\n};`
}
function objectTypeScheme(item, indent) {
if (!item.list.length)
return `tObject({})`;
return `tObject({\n${propertiesScheme(item, indent)}\n})`
}
const channels_ts = [
`/**
* 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.
*/
// This file is generated by ${path.basename(__filename)}, do not edit manually.
import { EventEmitter } from 'events';
export type Binary = string;
export interface Channel extends EventEmitter {
}
`];
const pdl = fs.readFileSync(path.join(__dirname, '..', 'src', 'rpc', 'protocol.pdl'), 'utf-8');
const list = tokenize(pdl);
const scheme = new Map();
const inherits = new Map();
function addScheme(name, s) {
if (scheme.has(name))
throw new Error('Duplicate scheme name ' + name);
scheme.set(name, s);
}
for (const item of list) {
if (item.words[0] === 'interface') {
channels.add(item.words[1]);
if (item.words[2] === 'extends')
inherits.set(item.words[1], item.words[3]);
}
}
for (const item of list) {
if (item.words[0] === 'type') {
if (item.words.length !== 2)
raise(item);
channels_ts.push(`export type ${item.words[1]} = {`);
channels_ts.push(properties(item, ' '));
channels_ts.push(`};`);
addScheme(item.words[1], objectTypeScheme(item, ' '));
} else if (item.words[0] === 'interface') {
const channelName = item.words[1];
channels_ts.push(`// ----------- ${channelName} -----------`);
const init = item.list.find(i => i.words[0] === 'initializer');
if (init && init.words.length > 1)
raise(init);
channels_ts.push(objectType(channelName + 'Initializer', init || { list: [] }, ' '));
addScheme(channelName + 'Initializer', objectTypeScheme(init || { list: [] }, ' '));
let extendsName = 'Channel';
if (item.words.length === 4 && item.words[2] === 'extends')
extendsName = item.words[3] + 'Channel';
else if (item.words.length !== 2)
raise(item);
channels_ts.push(`export interface ${channelName}Channel extends ${extendsName} {`);
const types = new Map();
for (const method of item.list) {
if (method === init)
continue;
if (method.words[0] === 'command') {
if (method.words.length !== 2)
raise(method);
const methodName = method.words[1];
const parameters = method.list.find(i => i.words[0] === 'parameters');
const paramsName = `${channelName}${titleCase(methodName)}Params`;
types.set(paramsName, parameters || { list: [] });
addScheme(paramsName, parameters ? objectTypeScheme(parameters, ' ') : `tOptional(tObject({}))`);
const returns = method.list.find(i => i.words[0] === 'returns');
const resultName = `${channelName}${titleCase(methodName)}Result`;
types.set(resultName, returns);
addScheme(resultName, returns ? objectTypeScheme(returns, ' ') : `tUndefined`);
channels_ts.push(` ${methodName}(params${parameters ? '' : '?'}: ${paramsName}): Promise<${resultName}>;`);
for (const key of inherits.keys()) {
if (inherits.get(key) === channelName) {
addScheme(`${key}${titleCase(methodName)}Params`, `tType('${paramsName}')`);
addScheme(`${key}${titleCase(methodName)}Result`, `tType('${resultName}')`);
}
}
} else if (method.words[0] === 'event') {
if (method.words.length !== 2)
raise(method);
const eventName = method.words[1];
const parameters = method.list.find(i => i.words[0] === 'parameters');
const paramsName = `${channelName}${titleCase(eventName)}Event`;
types.set(paramsName, parameters || { list: [] });
addScheme(paramsName, objectTypeScheme(parameters || { list: [] }, ' '));
channels_ts.push(` on(event: '${eventName}', callback: (params: ${paramsName}) => void): this;`);
for (const key of inherits.keys()) {
if (inherits.get(key) === channelName)
addScheme(`${key}${titleCase(eventName)}Event`, `tType('${paramsName}')`);
}
} else {
raise(method);
}
}
channels_ts.push(`}`);
for (const [name, item] of types) {
if (!item)
channels_ts.push(`export type ${name} = void;`);
else
channels_ts.push(objectType(name, item, ' '));
}
} else {
raise(item);
}
channels_ts.push(``);
}
const client_validator_ts = [
`/**
* 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.
*/
// This file is generated by ${path.basename(__filename)}, do not edit manually.
import { scheme, tOptional, tObject, tBoolean, tNumber, tString, tType, tEnum, tArray, tChannel, tUndefined, tBinary } from './validatorPrimitives';
export { validateParams } from './validatorPrimitives';
`];
for (const [name, value] of scheme)
client_validator_ts.push(`scheme.${name} = ${value};`);
client_validator_ts.push(``);
fs.writeFileSync(path.join(__dirname, '..', 'src', 'rpc', 'channels.ts'), channels_ts.join('\n'), 'utf-8');
fs.writeFileSync(path.join(__dirname, '..', 'src', 'rpc', 'client', 'validator.ts'), client_validator_ts.join('\n'), 'utf-8');