import * as fs from 'fs' ; import * as path from 'path' ; import { IProtocol , Protocol as P } from './protocol-schema' ; const jsProtocol = require ( '../json/js_protocol.json' ) ; const browserProtocol = require ( '../json/browser_protocol.json' ) ; const protocolDomains = jsProtocol . domains . concat ( browserProtocol . domains ) ; let numIndents = 0 ; let emitStr = '' ; const emit = ( str ) => { emitStr += str ; } ; const getIndent = ( ) => '    ' . repeat ( numIndents ) ; const emitIndent = ( ) => { emitStr += getIndent ( ) ; } ; const emitLine = ( str ? ) => { if ( str ) { emitIndent ( ) ; emit ( `${ str }\n` ) ; } else { emit ( '\n' ) ; } } ; const emitOpenBlock = ( str , openChar = ' {' ) => { emitLine ( `${ str }${ openChar }` ) ; numIndents ++ ; } ; const emitCloseBlock = ( closeChar = '}' ) => { numIndents -- ; emitLine ( closeChar ) ; } ; const emitHeaderComments = ( ) => { emitLine ( '/**********************************************************************' ) ; emitLine ( ' * Auto-generated by protocol-dts-generator.ts, do not edit manually. *' ) ; emitLine ( ' **********************************************************************/' ) ; emitLine ( ) ; } ; const fixCamelCase = ( name ) : string => { let prefix = '' ; let result = name ; if ( name [ 0 ] === '-' ) { prefix = 'Negative' ; result = name . substring ( 1 ) ; } const refined = result . split ( '-' ) . map ( toTitleCase ) . join ( '' ) ; return prefix + refined . replace ( /HTML|XML|WML|API/i , match => match . toUpperCase ( ) ) ; } ; const emitEnum = ( enumName , enumValues ) => { emitOpenBlock ( `export const enum ${ enumName }` ) ; enumValues . forEach ( value => { emitLine ( `${ fixCamelCase ( value ) } = '${ value }',` ) ; } ) ; emitCloseBlock ( ) ; } ; const emitPublicDocDeclaration = ( ) => { emitLine ( '/**' ) ; emitLine ( ' * The Chrome DevTools Protocol.' ) ; emitLine ( ' * @public' ) ; emitLine ( ' */' ) ; } ; const emitModule = ( moduleName , domains ) => { moduleName = toTitleCase ( moduleName ) ; emitHeaderComments ( ) ; emitPublicDocDeclaration ( ) ; emitOpenBlock ( `export namespace ${ moduleName }` ) ; emitGlobalTypeDefs ( ) ; domains . forEach ( emitDomain ) ; emitCloseBlock ( ) ; emitLine ( ) ; emitLine ( `export default ${ moduleName };` ) ; } ; const emitGlobalTypeDefs = ( ) => { emitLine ( ) ; emitLine ( `export type integer = number` ) ; } ; const emitDomain = ( domain ) => { const domainName = toTitleCase ( domain . domain ) ; emitLine ( ) ; emitDescription ( domain . description ) ; emitOpenBlock ( `export namespace ${ domainName }` ) ; if ( domain . types ) domain . types . forEach ( emitDomainType ) ; if ( domain . commands ) domain . commands . forEach ( emitCommand ) ; if ( domain . events ) domain . events . forEach ( emitEvent ) ; emitCloseBlock ( ) ; } ; const getCommentLines = ( description ) => { const lines = description . split ( /\r?\n/g ) . map ( line => ` * ${ line }` ) ; return [ `/**` , ... lines , ` */` ] ; } ; const emitDescription = ( description ? ) => { if ( description ) getCommentLines ( description ) . map ( l => emitLine ( l ) ) ; } ; const isPropertyInlineEnum = ( prop ) : boolean => { if ( '$ref' in prop ) { return false ; } return prop . type === 'string' && prop . enum !== null && prop . enum !== undefined ; } ; const getPropertyDef = ( interfaceName , prop ) : string => { const propName = prop . name . includes ( '.' ) ? `'${ prop . name }'` : prop . name ; const type = getPropertyType ( interfaceName , prop ) ; return `${ propName }${ prop . optional ? '?' : '' }: ${ type }` ; } ; const getPropertyType = ( interfaceName , prop ) : string => { if ( '$ref' in prop ) return prop . $ref ; else if ( prop . type === 'array' ) return `${ getPropertyType ( interfaceName , prop . items ) }[]` ; else if ( prop . type === 'object' ) if ( ! prop . properties ) { return `any` ; } else { let objStr = '{\n' ; numIndents ++ ; objStr += prop . properties . map ( p => `${ getIndent ( ) }${ getPropertyDef ( interfaceName , p ) };\n` ) . join ( '' ) ; numIndents -- ; objStr += `${ getIndent ( ) }}` ; return objStr ; } else if ( prop . type === 'string' && prop . enum ) return '(' + prop . enum . map ( ( v ) => `'${ v }'` ) . join ( ' | ' ) + ')' ; return prop . type ; } ; const emitProperty = ( interfaceName , prop ) => { let description = prop . description ; if ( isPropertyInlineEnum ( prop ) ) { const enumName = interfaceName + toTitleCase ( prop . name ) ; description = `${ description || '' } (${ enumName } enum)` ; } emitDescription ( description ) ; emitLine ( `${ getPropertyDef ( interfaceName , prop ) };` ) ; } ; const emitInlineEnumForDomainType = ( type ) => { if ( type . type === 'object' ) { emitInlineEnums ( type . id , type . properties ) ; } } ; const emitInlineEnumsForCommands = ( command ) => { emitInlineEnums ( toCmdRequestName ( command . name ) , command . parameters ) ; emitInlineEnums ( toCmdResponseName ( command . name ) , command . returns ) ; } ; const emitInlineEnumsForEvents = ( event ) => { emitInlineEnums ( toEventPayloadName ( event . name ) , event . parameters ) ; } ; const emitInlineEnums = ( prefix , propertyTypes ? ) => { if ( ! propertyTypes ) { return ; } for ( const type of propertyTypes ) { if ( isPropertyInlineEnum ( type ) ) { emitLine ( ) ; const enumName = prefix + toTitleCase ( type . name ) ; emitEnum ( enumName , ( type as P . StringType ) . enum || [ ] ) ; } } } ; const emitInterface = ( interfaceName , props ? ) => { emitOpenBlock ( `export interface ${ interfaceName }` ) ; props ? props . forEach ( prop => emitProperty ( interfaceName , prop ) ) : emitLine ( '[key: string]: string;' ) ; emitCloseBlock ( ) ; } ; const emitDomainType = ( type ) => { emitInlineEnumForDomainType ( type ) ; emitLine ( ) ; emitDescription ( type . description ) ; if ( type . type === 'object' ) { emitInterface ( type . id , type . properties ) ; } else { emitLine ( `export type ${ type . id } = ${ getPropertyType ( type . id , type ) };` ) ; } } ; const toTitleCase = ( str ) => str [ 0 ] . toUpperCase ( ) + str . substr ( 1 ) ; const toCmdRequestName = ( commandName ) => `${ toTitleCase ( commandName ) }Request` ; const toCmdResponseName = ( commandName ) => `${ toTitleCase ( commandName ) }Response` ; const emitCommand = ( command ) => { emitInlineEnumsForCommands ( command ) ; if ( command . parameters ) { emitLine ( ) ; emitInterface ( toCmdRequestName ( command . name ) , command . parameters ) ; } if ( command . returns ) { emitLine ( ) ; emitInterface ( toCmdResponseName ( command . name ) , command . returns ) ; } } ; const toEventPayloadName = ( eventName ) => `${ toTitleCase ( eventName ) }Event` ; const emitEvent = ( event ) => { if ( ! event . parameters ) { return ; } emitInlineEnumsForEvents ( event ) ; emitLine ( ) ; emitDescription ( event . description ) ; emitInterface ( toEventPayloadName ( event . name ) , event . parameters ) ; } ; const getEventMapping = ( event , domainName , modulePrefix ) : P . RefType & P . PropertyBaseType => { const payloadType = event . parameters ? `[${ modulePrefix }.${ domainName }.${ toEventPayloadName ( event . name ) }]` : '[]' ; return { name : `${ domainName }.${ event . name }` , description : event . description , $ref : payloadType } ; } ; const isWeakInterface = ( params ) : boolean => { return params . every ( p => ! ! p . optional ) ; } ; const getCommandMapping = ( command , domainName , modulePrefix ) : P . ObjectType & P . PropertyBaseType => { const prefix = `${ modulePrefix }.${ domainName }.` ; let requestType = '[]' ; if ( command . parameters ) { const optional = isWeakInterface ( command . parameters ) ? '?' : '' ; requestType = '[' + prefix + toCmdRequestName ( command . name ) + optional + ']' ; } const responseType = command . returns ? prefix + toCmdResponseName ( command . name ) : 'void' ; return { type : 'object' , name : `${ domainName }.${ command . name }` , description : command . description , properties : [ { name : 'paramsType' , $ref : requestType , } , { name : 'returnType' , $ref : responseType , } ] } ; } ; const flatten = < T > ( arr ) => ( [ ] as T [ ] ) . concat ( ... arr ) ; const emitMapping = ( moduleName , protocolModuleName , domains ) => { moduleName = toTitleCase ( moduleName ) ; emitHeaderComments ( ) ; emitLine ( `import Protocol from './${ protocolModuleName }'` ) ; emitLine ( ) ; emitDescription ( 'Mappings from protocol event and command names to the types required for them.' ) ; emitOpenBlock ( `export namespace ${ moduleName }` ) ; const protocolModulePrefix = toTitleCase ( protocolModuleName ) ; const eventDefs = flatten ( domains . map ( d => { const domainName = toTitleCase ( d . domain ) ; return ( d . events || [ ] ) . map ( e => getEventMapping ( e , domainName , protocolModulePrefix ) ) ; } ) ) ; emitInterface ( 'Events' , eventDefs ) ; emitLine ( ) ; const commandDefs = flatten ( domains . map ( d => { const domainName = toTitleCase ( d . domain ) ; return ( d . commands || [ ] ) . map ( c => getCommandMapping ( c , domainName , protocolModulePrefix ) ) ; } ) ) ; emitInterface ( 'Commands' , commandDefs ) ; emitCloseBlock ( ) ; emitLine ( ) ; emitLine ( `export default ${ moduleName };` ) ; } ; const emitApiCommand = ( command , domainName , modulePrefix ) => { const prefix = `${ modulePrefix }.${ domainName }.` ; emitDescription ( command . description ) ; const params = command . parameters ? `params: ${ prefix }${ toCmdRequestName ( command . name ) }` : '' ; const response = command . returns ? `${ prefix }${ toCmdResponseName ( command . name ) }` : 'void' ; emitLine ( `${ command . name }(${ params }): Promise<${ response }>;` ) ; emitLine ( ) ; } ; const emitApiEvent = ( event , domainName , modulePrefix ) => { const prefix = `${ modulePrefix }.${ domainName }.` ; emitDescription ( event . description ) ; const params = event . parameters ? `params: ${ prefix }${ toEventPayloadName ( event . name ) }` : '' ; emitLine ( `on(event: '${ event . name }', listener: (${ params }) => void): void;` ) ; emitLine ( ) ; } ; const emitDomainApi = ( domain , modulePrefix ) => { emitLine ( ) ; const domainName = toTitleCase ( domain . domain ) ; emitOpenBlock ( `export interface ${ domainName }Api` ) ; if ( domain . commands ) domain . commands . forEach ( c => emitApiCommand ( c , domainName , modulePrefix ) ) ; if ( domain . events ) domain . events . forEach ( e => emitApiEvent ( e , domainName , modulePrefix ) ) ; emitCloseBlock ( ) ; } ; const emitApi = ( moduleName , protocolModuleName , domains ) => { moduleName = toTitleCase ( moduleName ) ; emitHeaderComments ( ) ; emitLine ( `import Protocol from './${ protocolModuleName }'` ) ; emitLine ( ) ; emitDescription ( 'API generated from Protocol commands and events.' ) ; emitOpenBlock ( `export namespace ${ moduleName }` ) ; emitLine ( ) ; emitOpenBlock ( `export interface ProtocolApi` ) ; domains . forEach ( d => { emitLine ( `${ d . domain }: ${ d . domain }Api;` ) ; emitLine ( ) ; } ) ; emitCloseBlock ( ) ; emitLine ( ) ; const protocolModulePrefix = toTitleCase ( protocolModuleName ) ; domains . forEach ( d => emitDomainApi ( d , protocolModulePrefix ) ) ; emitCloseBlock ( ) ; emitLine ( ) ; emitLine ( `export default ${ moduleName };` ) ; } ; const flushEmitToFile = ( path ) => { console . log ( `Writing to ${ path }` ) ; fs . writeFileSync ( path , emitStr , { encoding : 'utf-8' } ) ; numIndents = 0 ; emitStr = '' ; } ; const destProtocolFilePath = `${ __dirname }/../types/protocol.d.ts` ; const protocolModuleName = path . basename ( destProtocolFilePath , '.d.ts' ) ; emitModule ( protocolModuleName , protocolDomains ) ; flushEmitToFile ( destProtocolFilePath ) ; const destMappingFilePath = `${ __dirname }/../types/protocol-mapping.d.ts` ; const mappingModuleName = 'ProtocolMapping' ; emitMapping ( mappingModuleName , protocolModuleName , protocolDomains ) ; flushEmitToFile ( destMappingFilePath ) ; const destApiFilePath = `${ __dirname }/../types/protocol-proxy-api.d.ts` ; const apiModuleName = 'ProtocolProxyApi' ; emitApi ( apiModuleName , protocolModuleName , protocolDomains ) ; flushEmitToFile ( destApiFilePath ) ;