UNPKG

15.5 kBPlain TextView Raw
1// Copyright (c) 2020 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/*
6 * this script was taken from https://github.com/ChromeDevTools/devtools-protocol/tree/master/scripts
7 * and adjusted slightly to fit within devtools-frontend
8 */
9import * as fs from 'fs';
10import * as path from 'path';
11
12import type {Protocol} from './protocol_schema.js';
13
14const PROTOCOL_JSON_PATH = path.resolve(
15 __dirname, path.join('..', '..', 'third_party', 'blink', 'public', 'devtools_protocol', 'browser_protocol.json'));
16
17const protocolJson = require(PROTOCOL_JSON_PATH);
18const removedDomains = new Set(['Console']);
19const protocolDomains: Protocol.Domain[] = protocolJson.domains.filter(({domain}) => !removedDomains.has(domain));
20
21let numIndents = 0;
22let emitStr = '';
23
24const emit = (str: string) => {
25 emitStr += str;
26};
27
28const getIndent = () => ' '.repeat(numIndents); // 2 spaced indents
29
30const emitIndent = () => {
31 emitStr += getIndent();
32};
33
34const emitLine = (str?: string) => {
35 if (str) {
36 emitIndent();
37 emit(`${str}\n`);
38 } else {
39 emit('\n');
40 }
41};
42
43const emitOpenBlock = (str: string, openChar = ' {') => {
44 emitLine(`${str}${openChar}`);
45 numIndents++;
46};
47
48const emitCloseBlock = (closeChar = '}') => {
49 numIndents--;
50 emitLine(closeChar);
51};
52
53const 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
65const 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
76const 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
91const 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
108const getCommentLines = (description: string) => {
109 const lines = description.split(/\r?\n/g).map(line => line && ` * ${line}` || ' *');
110 return ['/**', ...lines, ' */'];
111};
112
113const emitDescription = (description?: string) => {
114 if (description) {
115 getCommentLines(description).map(l => emitLine(l));
116 }
117};
118
119const 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
126const getPropertyDef = (interfaceName: string, prop: Protocol.PropertyType): string => {
127 // Quote key if it has a . in it.
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
138const 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 // TODO: actually 'any'? or can use generic '[key: string]: string'?
148 return 'any';
149 }
150 // hack: access indent, \n directly
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
161const emitProperty = (interfaceName: string, prop: Protocol.PropertyType) => {
162 emitDescription(prop.description);
163 emitLine(`${getPropertyDef(interfaceName, prop)};`);
164};
165
166const 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
172const 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// This is straight-up adopted from fix_camel_case in code_generator_frontend.py.
181const 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
192const emitInlineEnumForDomainType = (type: Protocol.DomainType) => {
193 if (type.type === 'object') {
194 emitInlineEnums(type.id, type.properties);
195 }
196};
197
198const emitInlineEnumsForCommands = (command: Protocol.Command) => {
199 emitInlineEnums(toCmdRequestName(command.name), command.parameters);
200 emitInlineEnums(toCmdResponseName(command.name), command.returns);
201};
202
203const emitInlineEnumsForEvents = (event: Protocol.Event) => {
204 emitInlineEnums(toEventPayloadName(event.name), event.parameters);
205};
206
207const 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// Please keep the keys sorted.
221const identifierTypesOverride = new Map([
222 ['IO.StreamHandle', true],
223 ['Page.ScriptIdentifier', true],
224]);
225
226function 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
232const emitDomainType = (domain: Protocol.Domain, type: Protocol.DomainType) => {
233 // Check if this type is an object that declares inline enum types for some of its properties.
234 // These inline enums must be emitted first.
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 // Explicit enums declared as separate types that inherit from 'string'.
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
255const toTitleCase = (str: string) => str[0].toUpperCase() + str.substr(1);
256
257const toCmdRequestName = (commandName: string) => `${toTitleCase(commandName)}Request`;
258
259const toCmdResponseName = (commandName: string) => `${toTitleCase(commandName)}Response`;
260
261const emitCommand = (command: Protocol.Command) => {
262 emitInlineEnumsForCommands(command);
263
264 // TODO(bckenny): should description be emitted for params and return types?
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
276const toEventPayloadName = (eventName: string) => `${toTitleCase(eventName)}Event`;
277
278const 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
290const getEventMapping =
291 (event: Protocol.Event, domainName: string, modulePrefix: string): Protocol.RefType&Protocol.PropertyBaseType => {
292 // Use TS3.0+ tuples
293 const payloadType = event.parameters ? `[${modulePrefix}.${domainName}.${toEventPayloadName(event.name)}]` : '[]';
294
295 return {
296 // domain-prefixed name since it will be used outside of the module.
297 name: `${domainName}.${event.name}`,
298 description: event.description,
299 $ref: payloadType,
300 };
301 };
302
303const isWeakInterface = (params: Protocol.PropertyType[]): boolean => {
304 return params.every(p => Boolean(p.optional));
305};
306
307const getCommandMapping = (command: Protocol.Command, domainName: string,
308 modulePrefix: string): Protocol.ObjectType&Protocol.PropertyBaseType => {
309 const prefix = `${modulePrefix}.${domainName}.`;
310 // Use TS3.0+ tuples for paramsType
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
335const flatten = <T>(arr: T[][]) => ([] as T[]).concat(...arr);
336
337const 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
363const 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
373const 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
381const 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
400const 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
436const flushEmitToFile = (path: string) => {
437 console.log(`Wrote file: ${path}`);
438 fs.writeFileSync(path, emitStr, {encoding: 'utf-8'});
439 numIndents = 0;
440 emitStr = '';
441};
442
443const 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
462main();