1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 | import * as fs from 'fs';
|
10 | import * as path from 'path';
|
11 |
|
12 | import type {Protocol} from './protocol_schema.js';
|
13 |
|
14 | const PROTOCOL_JSON_PATH = path.resolve(
|
15 | __dirname, path.join('..', '..', 'third_party', 'blink', 'public', 'devtools_protocol', 'browser_protocol.json'));
|
16 |
|
17 | const protocolJson = require(PROTOCOL_JSON_PATH);
|
18 | const removedDomains = new Set(['Console']);
|
19 | const protocolDomains: Protocol.Domain[] = protocolJson.domains.filter(({domain}) => !removedDomains.has(domain));
|
20 |
|
21 | let numIndents = 0;
|
22 | let emitStr = '';
|
23 |
|
24 | const emit = (str: string) => {
|
25 | emitStr += str;
|
26 | };
|
27 |
|
28 | const getIndent = () => ' '.repeat(numIndents);
|
29 |
|
30 | const emitIndent = () => {
|
31 | emitStr += getIndent();
|
32 | };
|
33 |
|
34 | const emitLine = (str?: string) => {
|
35 | if (str) {
|
36 | emitIndent();
|
37 | emit(`${str}\n`);
|
38 | } else {
|
39 | emit('\n');
|
40 | }
|
41 | };
|
42 |
|
43 | const emitOpenBlock = (str: string, openChar = ' {') => {
|
44 | emitLine(`${str}${openChar}`);
|
45 | numIndents++;
|
46 | };
|
47 |
|
48 | const emitCloseBlock = (closeChar = '}') => {
|
49 | numIndents--;
|
50 | emitLine(closeChar);
|
51 | };
|
52 |
|
53 | const emitHeaderComments = () => {
|
54 | emitLine('// Copyright (c) 2020 The Chromium Authors. All rights reserved.');
|
55 | emitLine('// Use of this source code is governed by a BSD-style license that can be');
|
56 | emitLine('// found in the LICENSE file.');
|
57 | emitLine();
|
58 | emitLine('/**');
|
59 | emitLine(' * This file is auto-generated, do not edit manually. *');
|
60 | emitLine(' * Re-generate with: npm run generate-protocol-resources.');
|
61 | emitLine(' */');
|
62 | emitLine();
|
63 | };
|
64 |
|
65 | const emitModule = (moduleName: string, domains: Protocol.Domain[]) => {
|
66 | moduleName = toTitleCase(moduleName);
|
67 | emitHeaderComments();
|
68 | emitOpenBlock(`declare namespace ${moduleName}`);
|
69 | emitGlobalTypeDefs();
|
70 | domains.forEach(emitDomain);
|
71 | emitCloseBlock();
|
72 | emitLine();
|
73 | emitLine('export = Protocol;');
|
74 | };
|
75 |
|
76 | const emitGlobalTypeDefs = () => {
|
77 | emitLine();
|
78 | emitLine('export type integer = number;');
|
79 | emitLine('export type binary = string;');
|
80 | emitLine('export type EnumerableEnum<T> = {[K in keyof T]: T[K]};');
|
81 | emitLine('export interface ProtocolResponseWithError {');
|
82 | numIndents++;
|
83 | emitLine('/** Returns an error message if the request failed. */');
|
84 | emitLine('getError(): string|undefined;');
|
85 | numIndents--;
|
86 | emitLine('}');
|
87 | emitLine('type OpaqueType<Tag extends string> = {protocolOpaqueTypeTag: Tag};');
|
88 | emitLine('type OpaqueIdentifier<RepresentationType, Tag extends string> = RepresentationType&OpaqueType<Tag>;');
|
89 | };
|
90 |
|
91 | const emitDomain = (domain: Protocol.Domain) => {
|
92 | const domainName = toTitleCase(domain.domain);
|
93 | emitLine();
|
94 | emitDescription(domain.description);
|
95 | emitOpenBlock(`export namespace ${domainName}`);
|
96 | if (domain.types) {
|
97 | domain.types.forEach(emitDomainType.bind(null, domain));
|
98 | }
|
99 | if (domain.commands) {
|
100 | domain.commands.forEach(emitCommand);
|
101 | }
|
102 | if (domain.events) {
|
103 | domain.events.forEach(emitEvent);
|
104 | }
|
105 | emitCloseBlock();
|
106 | };
|
107 |
|
108 | const getCommentLines = (description: string) => {
|
109 | const lines = description.split(/\r?\n/g).map(line => line && ` * ${line}` || ' *');
|
110 | return ['/**', ...lines, ' */'];
|
111 | };
|
112 |
|
113 | const emitDescription = (description?: string) => {
|
114 | if (description) {
|
115 | getCommentLines(description).map(l => emitLine(l));
|
116 | }
|
117 | };
|
118 |
|
119 | const isPropertyInlineEnum = (prop: Protocol.ProtocolType): boolean => {
|
120 | if ('$ref' in prop) {
|
121 | return false;
|
122 | }
|
123 | return prop.type === 'string' && prop.enum !== null && prop.enum !== undefined;
|
124 | };
|
125 |
|
126 | const getPropertyDef = (interfaceName: string, prop: Protocol.PropertyType): string => {
|
127 |
|
128 | const propName = prop.name.includes('.') ? `'${prop.name}'` : prop.name;
|
129 | let type: string;
|
130 | if (isPropertyInlineEnum(prop)) {
|
131 | type = interfaceName + toTitleCase(prop.name);
|
132 | } else {
|
133 | type = getPropertyType(interfaceName, prop);
|
134 | }
|
135 | return `${propName}${prop.optional ? '?' : ''}: ${type}`;
|
136 | };
|
137 |
|
138 | const getPropertyType = (interfaceName: string, prop: Protocol.ProtocolType): string => {
|
139 | if ('$ref' in prop) {
|
140 | return prop.$ref;
|
141 | }
|
142 | if (prop.type === 'array') {
|
143 | return `${getPropertyType(interfaceName, prop.items)}[]`;
|
144 | }
|
145 | if (prop.type === 'object') {
|
146 | if (!prop.properties) {
|
147 |
|
148 | return 'any';
|
149 | }
|
150 |
|
151 | let objStr = '{\n';
|
152 | numIndents++;
|
153 | objStr += prop.properties.map(p => `${getIndent()}${getPropertyDef(interfaceName, p)};\n`).join('');
|
154 | numIndents--;
|
155 | objStr += `${getIndent()}}`;
|
156 | return objStr;
|
157 | }
|
158 | return prop.type;
|
159 | };
|
160 |
|
161 | const emitProperty = (interfaceName: string, prop: Protocol.PropertyType) => {
|
162 | emitDescription(prop.description);
|
163 | emitLine(`${getPropertyDef(interfaceName, prop)};`);
|
164 | };
|
165 |
|
166 | const emitInterface = (interfaceName: string, props?: Protocol.PropertyType[], optionalExtendsClause: string = '') => {
|
167 | emitOpenBlock(`export interface ${interfaceName}${optionalExtendsClause}`);
|
168 | props ? props.forEach(prop => emitProperty(interfaceName, prop)) : emitLine('[key: string]: string;');
|
169 | emitCloseBlock();
|
170 | };
|
171 |
|
172 | const emitEnum = (enumName: string, enumValues: string[]) => {
|
173 | emitOpenBlock(`export const enum ${enumName}`);
|
174 | enumValues.forEach(value => {
|
175 | emitLine(`${fixCamelCase(value)} = '${value}',`);
|
176 | });
|
177 | emitCloseBlock();
|
178 | };
|
179 |
|
180 |
|
181 | const fixCamelCase = (name: string): string => {
|
182 | let prefix = '';
|
183 | let result = name;
|
184 | if (name[0] === '-') {
|
185 | prefix = 'Negative';
|
186 | result = name.substring(1);
|
187 | }
|
188 | const refined = result.split('-').map(toTitleCase).join('');
|
189 | return prefix + refined.replace(/HTML|XML|WML|API/i, match => match.toUpperCase());
|
190 | };
|
191 |
|
192 | const emitInlineEnumForDomainType = (type: Protocol.DomainType) => {
|
193 | if (type.type === 'object') {
|
194 | emitInlineEnums(type.id, type.properties);
|
195 | }
|
196 | };
|
197 |
|
198 | const emitInlineEnumsForCommands = (command: Protocol.Command) => {
|
199 | emitInlineEnums(toCmdRequestName(command.name), command.parameters);
|
200 | emitInlineEnums(toCmdResponseName(command.name), command.returns);
|
201 | };
|
202 |
|
203 | const emitInlineEnumsForEvents = (event: Protocol.Event) => {
|
204 | emitInlineEnums(toEventPayloadName(event.name), event.parameters);
|
205 | };
|
206 |
|
207 | const emitInlineEnums = (prefix: string, propertyTypes?: Protocol.PropertyType[]) => {
|
208 | if (!propertyTypes) {
|
209 | return;
|
210 | }
|
211 | for (const type of propertyTypes) {
|
212 | if (isPropertyInlineEnum(type)) {
|
213 | emitLine();
|
214 | const enumName = prefix + toTitleCase(type.name);
|
215 | emitEnum(enumName, (type as Protocol.StringType).enum);
|
216 | }
|
217 | }
|
218 | };
|
219 |
|
220 |
|
221 | const identifierTypesOverride = new Map([
|
222 | ['IO.StreamHandle', true],
|
223 | ['Page.ScriptIdentifier', true],
|
224 | ]);
|
225 |
|
226 | function isIdentifierTypeName(identifierName: string): boolean {
|
227 | const looksLikeIdentifierName = identifierName.endsWith('Id') || identifierName.endsWith('ID');
|
228 | const override = identifierTypesOverride.get(identifierName);
|
229 | return looksLikeIdentifierName && override !== false || override;
|
230 | }
|
231 |
|
232 | const emitDomainType = (domain: Protocol.Domain, type: Protocol.DomainType) => {
|
233 |
|
234 |
|
235 | emitInlineEnumForDomainType(type);
|
236 |
|
237 | emitLine();
|
238 | emitDescription(type.description);
|
239 |
|
240 | if (type.type === 'object') {
|
241 | emitInterface(type.id, type.properties);
|
242 | } else if (type.type === 'string' && type.enum) {
|
243 |
|
244 | emitEnum(type.id, type.enum);
|
245 | } else if (isIdentifierTypeName(`${domain.domain}.${type.id}`)) {
|
246 | const representationType = getPropertyType(type.id, type);
|
247 | const tag = `Protocol.${domain.domain}.${type.id}`;
|
248 | const opaqueType = `OpaqueIdentifier<${representationType}, '${tag}'>`;
|
249 | emitLine(`export type ${type.id} = ${opaqueType};`);
|
250 | } else {
|
251 | emitLine(`export type ${type.id} = ${getPropertyType(type.id, type)};`);
|
252 | }
|
253 | };
|
254 |
|
255 | const toTitleCase = (str: string) => str[0].toUpperCase() + str.substr(1);
|
256 |
|
257 | const toCmdRequestName = (commandName: string) => `${toTitleCase(commandName)}Request`;
|
258 |
|
259 | const toCmdResponseName = (commandName: string) => `${toTitleCase(commandName)}Response`;
|
260 |
|
261 | const emitCommand = (command: Protocol.Command) => {
|
262 | emitInlineEnumsForCommands(command);
|
263 |
|
264 |
|
265 | if (command.parameters) {
|
266 | emitLine();
|
267 | emitInterface(toCmdRequestName(command.name), command.parameters);
|
268 | }
|
269 |
|
270 | if (command.returns) {
|
271 | emitLine();
|
272 | emitInterface(toCmdResponseName(command.name), command.returns, ' extends ProtocolResponseWithError');
|
273 | }
|
274 | };
|
275 |
|
276 | const toEventPayloadName = (eventName: string) => `${toTitleCase(eventName)}Event`;
|
277 |
|
278 | const emitEvent = (event: Protocol.Event) => {
|
279 | if (!event.parameters) {
|
280 | return;
|
281 | }
|
282 |
|
283 | emitInlineEnumsForEvents(event);
|
284 |
|
285 | emitLine();
|
286 | emitDescription(event.description);
|
287 | emitInterface(toEventPayloadName(event.name), event.parameters);
|
288 | };
|
289 |
|
290 | const getEventMapping =
|
291 | (event: Protocol.Event, domainName: string, modulePrefix: string): Protocol.RefType&Protocol.PropertyBaseType => {
|
292 |
|
293 | const payloadType = event.parameters ? `[${modulePrefix}.${domainName}.${toEventPayloadName(event.name)}]` : '[]';
|
294 |
|
295 | return {
|
296 |
|
297 | name: `${domainName}.${event.name}`,
|
298 | description: event.description,
|
299 | $ref: payloadType,
|
300 | };
|
301 | };
|
302 |
|
303 | const isWeakInterface = (params: Protocol.PropertyType[]): boolean => {
|
304 | return params.every(p => Boolean(p.optional));
|
305 | };
|
306 |
|
307 | const getCommandMapping = (command: Protocol.Command, domainName: string,
|
308 | modulePrefix: string): Protocol.ObjectType&Protocol.PropertyBaseType => {
|
309 | const prefix = `${modulePrefix}.${domainName}.`;
|
310 |
|
311 | let requestType = '[]';
|
312 | if (command.parameters) {
|
313 | const optional = isWeakInterface(command.parameters) ? '?' : '';
|
314 | requestType = '[' + prefix + toCmdRequestName(command.name) + optional + ']';
|
315 | }
|
316 | const responseType = command.returns ? prefix + toCmdResponseName(command.name) : 'void';
|
317 |
|
318 | return {
|
319 | type: 'object',
|
320 | name: `${domainName}.${command.name}`,
|
321 | description: command.description,
|
322 | properties: [
|
323 | {
|
324 | name: 'paramsType',
|
325 | $ref: requestType,
|
326 | },
|
327 | {
|
328 | name: 'returnType',
|
329 | $ref: responseType,
|
330 | },
|
331 | ],
|
332 | };
|
333 | };
|
334 |
|
335 | const flatten = <T>(arr: T[][]) => ([] as T[]).concat(...arr);
|
336 |
|
337 | const emitMapping = (moduleName: string, protocolModuleName: string, domains: Protocol.Domain[]) => {
|
338 | moduleName = toTitleCase(moduleName);
|
339 | emitHeaderComments();
|
340 | emitDescription('Mappings from protocol event and command names to the types required for them.');
|
341 | emitOpenBlock(`export namespace ${moduleName}`);
|
342 |
|
343 | const protocolModulePrefix = toTitleCase(protocolModuleName);
|
344 | const eventDefs = flatten(domains.map(d => {
|
345 | const domainName = toTitleCase(d.domain);
|
346 | return (d.events || []).map(e => getEventMapping(e, domainName, protocolModulePrefix));
|
347 | }));
|
348 | emitInterface('Events', eventDefs);
|
349 |
|
350 | emitLine();
|
351 |
|
352 | const commandDefs = flatten(domains.map(d => {
|
353 | const domainName = toTitleCase(d.domain);
|
354 | return (d.commands || []).map(c => getCommandMapping(c, domainName, protocolModulePrefix));
|
355 | }));
|
356 | emitInterface('Commands', commandDefs);
|
357 |
|
358 | emitCloseBlock();
|
359 | emitLine();
|
360 | emitLine(`export default ${moduleName};`);
|
361 | };
|
362 |
|
363 | const emitApiCommand = (command: Protocol.Command, domainName: string, modulePrefix: string) => {
|
364 | const prefix = `${modulePrefix}.${domainName}.`;
|
365 | emitDescription(command.description);
|
366 | const params = command.parameters ? `params: ${prefix}${toCmdRequestName(command.name)}` : '';
|
367 | const response =
|
368 | command.returns ? `${prefix}${toCmdResponseName(command.name)}` : 'Protocol.ProtocolResponseWithError';
|
369 | emitLine(`invoke_${command.name}(${params}): Promise<${response}>;`);
|
370 | emitLine();
|
371 | };
|
372 |
|
373 | const emitApiEvent = (event: Protocol.Event, domainName: string, modulePrefix: string) => {
|
374 | const prefix = `${modulePrefix}.${domainName}.`;
|
375 | emitDescription(event.description);
|
376 | const params = event.parameters ? `params: ${prefix}${toEventPayloadName(event.name)}` : '';
|
377 | emitLine(`${event.name}(${params}): void;`);
|
378 | emitLine();
|
379 | };
|
380 |
|
381 | const emitDomainApi = (domain: Protocol.Domain, modulePrefix: string) => {
|
382 | emitLine();
|
383 | const domainName = toTitleCase(domain.domain);
|
384 | if (domainName.startsWith('I')) {
|
385 | emitLine('// eslint thinks this is us prefixing our interfaces but it\'s not!');
|
386 | emitLine('// eslint-disable-next-line @typescript-eslint/interface-name-prefix');
|
387 | }
|
388 | emitOpenBlock(`export interface ${domainName}Api`);
|
389 | if (domain.commands) {
|
390 | domain.commands.forEach(c => emitApiCommand(c, domainName, modulePrefix));
|
391 | }
|
392 | emitCloseBlock();
|
393 | emitOpenBlock(`export interface ${domainName}Dispatcher`);
|
394 | if (domain.events) {
|
395 | domain.events.forEach(e => emitApiEvent(e, domainName, modulePrefix));
|
396 | }
|
397 | emitCloseBlock();
|
398 | };
|
399 |
|
400 | const emitApi = (moduleName: string, protocolModuleName: string, domains: Protocol.Domain[]) => {
|
401 | moduleName = toTitleCase(moduleName);
|
402 | emitHeaderComments();
|
403 | emitLine();
|
404 | emitLine('import type * as Protocol from \'./protocol.js\'');
|
405 | emitLine();
|
406 | emitDescription('API generated from Protocol commands and events.');
|
407 | emitOpenBlock(`declare namespace ${moduleName}`);
|
408 |
|
409 | emitLine();
|
410 | emitLine('export type ProtocolDomainName = keyof ProtocolApi;');
|
411 |
|
412 | emitLine();
|
413 | emitOpenBlock('export interface ProtocolApi');
|
414 | domains.forEach(d => {
|
415 | emitLine(`${d.domain}: ${d.domain}Api;`);
|
416 | emitLine();
|
417 | });
|
418 | emitCloseBlock();
|
419 |
|
420 | emitLine();
|
421 | emitOpenBlock('export interface ProtocolDispatchers');
|
422 | domains.forEach(d => {
|
423 | emitLine(`${d.domain}: ${d.domain}Dispatcher;`);
|
424 | emitLine();
|
425 | });
|
426 | emitCloseBlock();
|
427 |
|
428 | emitLine();
|
429 | const protocolModulePrefix = toTitleCase(protocolModuleName);
|
430 | domains.forEach(d => emitDomainApi(d, protocolModulePrefix));
|
431 | emitCloseBlock();
|
432 | emitLine();
|
433 | emitLine('export = ProtocolProxyApi;');
|
434 | };
|
435 |
|
436 | const flushEmitToFile = (path: string) => {
|
437 | console.log(`Wrote file: ${path}`);
|
438 | fs.writeFileSync(path, emitStr, {encoding: 'utf-8'});
|
439 | numIndents = 0;
|
440 | emitStr = '';
|
441 | };
|
442 |
|
443 | const main = () => {
|
444 | const FRONTEND_GENERATED_DIR = path.resolve(__dirname, path.join('../../front_end/generated'));
|
445 |
|
446 | const destProtocolFilePath = path.join(FRONTEND_GENERATED_DIR, 'protocol.d.ts');
|
447 | const protocolModuleName = path.basename(destProtocolFilePath, '.d.ts');
|
448 | emitModule(protocolModuleName, protocolDomains);
|
449 | flushEmitToFile(destProtocolFilePath);
|
450 |
|
451 | const destMappingFilePath = path.join(FRONTEND_GENERATED_DIR, 'protocol-mapping.d.ts');
|
452 | const mappingModuleName = 'ProtocolMapping';
|
453 | emitMapping(mappingModuleName, protocolModuleName, protocolDomains);
|
454 | flushEmitToFile(destMappingFilePath);
|
455 |
|
456 | const destApiFilePath = path.join(FRONTEND_GENERATED_DIR, 'protocol-proxy-api.d.ts');
|
457 | const apiModuleName = 'ProtocolProxyApi';
|
458 | emitApi(apiModuleName, protocolModuleName, protocolDomains);
|
459 | flushEmitToFile(destApiFilePath);
|
460 | };
|
461 |
|
462 | main();
|