From e26b2cf0f289ed9f7bbb9f40bc2077401b652163 Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Fri, 30 Mar 2018 13:27:53 -0700 Subject: [PATCH] core(tsc): add type checking to use of CRDP events (#4886) --- .../gather/connections/connection.js | 63 +++--- .../gather/connections/extension.js | 6 +- lighthouse-core/gather/driver.js | 108 +++++----- .../scripts/extract-crdp-mapping.js | 192 ++++++++++++++++++ lighthouse-core/test/gather/driver-test.js | 2 +- package.json | 3 +- .../strict-event-emitter-types/LICENSE | 13 ++ .../strict-event-emitter-types/index.d.ts | 78 +++++++ typings/crdp-mapping.d.ts | 133 ++++++++++++ typings/externs.d.ts | 5 + typings/protocol.d.ts | 54 +++++ yarn.lock | 6 +- 12 files changed, 577 insertions(+), 86 deletions(-) create mode 100644 lighthouse-core/scripts/extract-crdp-mapping.js create mode 100644 third-party/strict-event-emitter-types/LICENSE create mode 100644 third-party/strict-event-emitter-types/index.d.ts create mode 100644 typings/crdp-mapping.d.ts create mode 100644 typings/protocol.d.ts diff --git a/lighthouse-core/gather/connections/connection.js b/lighthouse-core/gather/connections/connection.js index 8729a9b18ad1..126f0bc9aa3c 100644 --- a/lighthouse-core/gather/connections/connection.js +++ b/lighthouse-core/gather/connections/connection.js @@ -9,11 +9,17 @@ const EventEmitter = require('events').EventEmitter; const log = require('lighthouse-logger'); const LHError = require('../../lib/errors'); +/** + * @typedef {LH.StrictEventEmitter<{'protocolevent': LH.Protocol.RawEventMessage}>} CrdpEventMessageEmitter + */ + class Connection { constructor() { this._lastCommandId = 0; /** @type {Map), method: string, options: {silent?: boolean}}>}*/ this._callbacks = new Map(); + + /** @type {?CrdpEventMessageEmitter} */ this._eventEmitter = new EventEmitter(); } @@ -31,7 +37,6 @@ class Connection { return Promise.reject(new Error('Not implemented')); } - /** * @return {Promise} */ @@ -39,7 +44,6 @@ class Connection { return Promise.reject(new Error('Not implemented')); } - /** * Call protocol methods * @param {string} method @@ -57,22 +61,6 @@ class Connection { }); } - /** - * Bind listeners for connection events - * @param {'notification'} eventName - * @param {(body: {method: string, params: object}) => void} cb - */ - on(eventName, cb) { - if (eventName !== 'notification') { - throw new Error('Only supports "notification" events'); - } - - if (!this._eventEmitter) { - throw new Error('Attempted to add event listener after connection disposed.'); - } - this._eventEmitter.on(eventName, cb); - } - /* eslint-disable no-unused-vars */ /** @@ -91,13 +79,15 @@ class Connection { * @protected */ handleRawMessage(message) { - const object = JSON.parse(message); - // Remote debugging protocol is JSON RPC 2.0 compiant. In terms of that transport, - // responses to the commands carry "id" property, while notifications do not. - if (!object.id) { + const object = /** @type {LH.Protocol.RawMessage} */(JSON.parse(message)); + + // Responses to commands carry "id" property, while events do not. + if (!('id' in object)) { + // tsc doesn't currently narrow type in !in branch, so manually cast. + const eventMessage = /** @type {LH.Protocol.RawEventMessage} */(object); log.formatProtocol('<= event', - {method: object.method, params: object.params}, 'verbose'); - this.emitNotification(object.method, object.params); + {method: eventMessage.method, params: eventMessage.params}, 'verbose'); + this.emitProtocolEvent(eventMessage); return; } @@ -126,15 +116,14 @@ class Connection { } /** - * @param {string} method - * @param {object=} params - * @protected + * @param {LH.Protocol.RawEventMessage} eventMessage */ - emitNotification(method, params) { + emitProtocolEvent(eventMessage) { if (!this._eventEmitter) { throw new Error('Attempted to emit event after connection disposed.'); } - this._eventEmitter.emit('notification', {method, params}); + + this._eventEmitter.emit('protocolevent', eventMessage); } /** @@ -148,4 +137,20 @@ class Connection { } } +// Declared outside class body because function expressions can be typed via more expressive @type +/** + * Bind listeners for connection events + * @type {CrdpEventMessageEmitter['on']} + */ +Connection.prototype.on = function on(eventName, cb) { + if (eventName !== 'protocolevent') { + throw new Error('Only supports "protocolevent" events'); + } + + if (!this._eventEmitter) { + throw new Error('Attempted to add event listener after connection disposed.'); + } + this._eventEmitter.on(eventName, cb); +}; + module.exports = Connection; diff --git a/lighthouse-core/gather/connections/extension.js b/lighthouse-core/gather/connections/extension.js index 289a68dec37e..307f81eb3587 100644 --- a/lighthouse-core/gather/connections/extension.js +++ b/lighthouse-core/gather/connections/extension.js @@ -30,7 +30,11 @@ class ExtensionConnection extends Connection { _onEvent(source, method, params) { // log events received log.log('<=', method, params); - this.emitNotification(method, params); + + // Warning: type cast, assuming that debugger API is giving us a valid protocol event. + // Must be cast together since types of `params` and `method` come as a pair. + const eventMessage = /** @type {LH.Protocol.RawEventMessage} */({method, params}); + this.emitProtocolEvent(eventMessage); } /** diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js index 3a49bff88ebc..bc7eabdbc259 100644 --- a/lighthouse-core/gather/driver.js +++ b/lighthouse-core/gather/driver.js @@ -29,6 +29,10 @@ const DEFAULT_NETWORK_QUIET_THRESHOLD = 5000; // Controls how long to wait between longtasks before determining the CPU is idle, off by default const DEFAULT_CPU_QUIET_THRESHOLD = 0; +/** + * @typedef {LH.StrictEventEmitter} CrdpEventEmitter + */ + class Driver { static get MAX_WAIT_FOR_FULLY_LOADED() { return 45 * 1000; @@ -39,6 +43,10 @@ class Driver { */ constructor(connection) { this._traceCategories = Driver.traceCategories; + /** + * An event emitter that enforces mapping between Crdp event names and payload types. + * @type {CrdpEventEmitter} + */ this._eventEmitter = new EventEmitter(); this._connection = connection; // currently only used by WPT where just Page and Network are needed @@ -63,7 +71,7 @@ class Driver { */ this._monitoredUrl = null; - connection.on('notification', event => { + connection.on('protocolevent', event => { this._devtoolsLog.record(event); if (this._networkStatusMonitor) { this._networkStatusMonitor.dispatch(event.method, event.params); @@ -124,49 +132,6 @@ class Driver { return this._connection.wsEndpoint(); } - /** - * Bind listeners for protocol events - * @param {string} eventName - * @param {(event: any) => void} cb - */ - on(eventName, cb) { - if (this._eventEmitter === null) { - throw new Error('connect() must be called before attempting to listen to events.'); - } - - // log event listeners being bound - log.formatProtocol('listen for event =>', {method: eventName}, 'verbose'); - this._eventEmitter.on(eventName, cb); - } - - /** - * Bind a one-time listener for protocol events. Listener is removed once it - * has been called. - * @param {string} eventName - * @param {(event: any) => void} cb - */ - once(eventName, cb) { - if (this._eventEmitter === null) { - throw new Error('connect() must be called before attempting to listen to events.'); - } - // log event listeners being bound - log.formatProtocol('listen once for event =>', {method: eventName}, 'verbose'); - this._eventEmitter.once(eventName, cb); - } - - /** - * Unbind event listeners - * @param {string} eventName - * @param {(event: any) => void} cb - */ - off(eventName, cb) { - if (this._eventEmitter === null) { - throw new Error('connect() must be called before attempting to remove an event listener.'); - } - - this._eventEmitter.removeListener(eventName, cb); - } - /** * Debounce enabling or disabling domains to prevent driver users from * stomping on each other. Maintains an internal count of the times a domain @@ -495,7 +460,7 @@ class Driver { const checkForQuietExpression = `(${pageFunctions.checkTimeSinceLastLongTask.toString()})()`; /** * @param {Driver} driver - * @param {(value: void) => void} resolve + * @param {() => void} resolve */ function checkForQuiet(driver, resolve) { if (cancelled) return; @@ -893,7 +858,7 @@ class Driver { } /** - * @param {{additionalTraceCategories: string=}=} settings + * @param {{additionalTraceCategories?: string}=} settings * @return {Promise} */ beginTrace(settings) { @@ -946,8 +911,8 @@ class Driver { endTrace() { return new Promise((resolve, reject) => { // When the tracing has ended this will fire with a stream handle. - this.once('Tracing.tracingComplete', streamHandle => { - this._readTraceFromStream(streamHandle) + this.once('Tracing.tracingComplete', completeEvent => { + this._readTraceFromStream(completeEvent) .then(traceContents => resolve(traceContents), reject); }); @@ -957,15 +922,15 @@ class Driver { } /** - * @param {LH.Crdp.Tracing.TracingCompleteEvent} streamHandle + * @param {LH.Crdp.Tracing.TracingCompleteEvent} traceCompleteEvent */ - _readTraceFromStream(streamHandle) { + _readTraceFromStream(traceCompleteEvent) { return new Promise((resolve, reject) => { let isEOF = false; const parser = new TraceParser(); const readArguments = { - handle: streamHandle.stream, + handle: traceCompleteEvent.stream, }; /** @@ -1205,4 +1170,45 @@ class Driver { } } +// Declared outside class body because function expressions can be typed via more expressive @type +/** + * Bind listeners for protocol events. + * @type {CrdpEventEmitter['on']} + */ +Driver.prototype.on = function on(eventName, cb) { + if (this._eventEmitter === null) { + throw new Error('connect() must be called before attempting to listen to events.'); + } + + // log event listeners being bound + log.formatProtocol('listen for event =>', {method: eventName}, 'verbose'); + this._eventEmitter.on(eventName, cb); +}; + +/** + * Bind a one-time listener for protocol events. Listener is removed once it + * has been called. + * @type {CrdpEventEmitter['once']} + */ +Driver.prototype.once = function once(eventName, cb) { + if (this._eventEmitter === null) { + throw new Error('connect() must be called before attempting to listen to events.'); + } + // log event listeners being bound + log.formatProtocol('listen once for event =>', {method: eventName}, 'verbose'); + this._eventEmitter.once(eventName, cb); +}; + +/** + * Unbind event listener. + * @type {CrdpEventEmitter['removeListener']} + */ +Driver.prototype.off = function off(eventName, cb) { + if (this._eventEmitter === null) { + throw new Error('connect() must be called before attempting to remove an event listener.'); + } + + this._eventEmitter.removeListener(eventName, cb); +}; + module.exports = Driver; diff --git a/lighthouse-core/scripts/extract-crdp-mapping.js b/lighthouse-core/scripts/extract-crdp-mapping.js new file mode 100644 index 000000000000..d012ac984a65 --- /dev/null +++ b/lighthouse-core/scripts/extract-crdp-mapping.js @@ -0,0 +1,192 @@ +/** + * @license Copyright 2018 Google Inc. All Rights Reserved. + * 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. + */ +'use strict'; + +/** + * @fileOverview Used to pull a mapping of Chrome Remote Debugging Protocol + * event and command requests and responses for type checking of interactions. + * See typings/protocol.d.ts for how these are used. + */ + +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); + +const crdpTypingFile = require.resolve('vscode-chrome-debug-core/lib/crdp/crdp.d.ts'); +const lhCrdpExternsOutputFile = path.resolve(__dirname, '../../typings/crdp-mapping.d.ts'); + +/* eslint-disable max-len */ +const headerBlock = `/** + * @license Copyright 2018 Google Inc. All Rights Reserved. + * 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. + */ + +// Generated by \`yarn update:crdp-typings\` +`; +/* eslint-enable max-len */ + +const eventInterfaceName = 'CrdpEvents'; + +/** + * DFS of AST, returning the first interface found with matching target name. + * @param {ts.Node} rootNode + * @param {string} targetName + * @return {ts.Node|undefined} + */ +function findFirstInterface(rootNode, targetName) { + /** + * @param {ts.Node} node + * @return {ts.Node|undefined} + */ + function walker(node) { + if (ts.isInterfaceDeclaration(node)) { + if (node.name.escapedText === targetName) { + return node; + } + } + + return ts.forEachChild(node, walker); + } + + return walker(rootNode); +} + +/** + * Expects to start at root node. + * @param {ts.Node} node + * @return {Array} + */ +function getCrdpDomainNames(node) { + const crdpClientInterface = findFirstInterface(node, 'CrdpClient'); + if (!crdpClientInterface) { + throw new Error('no `interface CrdpClient` found in typing file'); + } + + /** @type {Array} */ + const domainProperties = []; + ts.forEachChild(crdpClientInterface, node => { + if (ts.isPropertySignature(node)) { + if (ts.isIdentifier(node.name)) { + domainProperties.push(node.name.text); + } + } + }); + + return domainProperties; +} + +/** + * Validates that this is an event listener we're expecting and returns the + * type name of its payload. + * @param {ts.MethodSignature} methodNode + * @param {string} methodName + * @return {string} + */ +function getEventListenerParamType(methodNode, methodName) { + if (methodNode.parameters.length > 1) { + throw new Error(`found ${methodNode.parameters.length} parameters passed to ${methodName}.`); + } + const listenerTypeNode = methodNode.parameters[0].type; + if (!listenerTypeNode || !ts.isFunctionTypeNode(listenerTypeNode)) { + throw new Error(`found unexpected argument passed to ${methodName}.`); + } + + if (listenerTypeNode.parameters.length === 1) { + const listenerParamType = listenerTypeNode.parameters[0].type; + if (listenerParamType && ts.isTypeReferenceNode(listenerParamType)) { + if (ts.isQualifiedName(listenerParamType.typeName)) { + return listenerParamType.typeName.right.text; + } + } + + throw new Error(`unexpected listener param passed to ${methodName}`); + } else if (listenerTypeNode.parameters.length > 1) { + const paramCount = methodNode.parameters.length; + throw new Error(`found ${paramCount} parameters passed to ${methodName}.`); + } + + return 'void'; +} + + +/** + * Returns a Map of events for given domain + * @param {ts.Node} sourceRoot + * @param {string} domainName + * @return {Map} + */ +function getEventMap(sourceRoot, domainName) { + // We want 'DomainNameClient' interface, sibling to domain module, for event info. + const eventInterfaceName = domainName + 'Client'; + const eventInterface = findFirstInterface(sourceRoot, eventInterfaceName); + + if (!eventInterface || !ts.isInterfaceDeclaration(eventInterface)) { + throw new Error(`Events interface not found for domain '${domainName}'.`); + } + + /** @type {Map} */ + const eventMap = new Map(); + + for (const member of eventInterface.members) { + if (!ts.isMethodSignature(member)) { + continue; + } + + if (!ts.isIdentifier(member.name)) { + throw new Error('Bad event method found' + member); + } + const methodName = member.name.text; + if (!/^on[A-Z]/.test(methodName)) { + throw new Error('bad method name found: ' + methodName); + } + const eventString = methodName[2].toLowerCase() + methodName.slice(3); + const eventName = `${domainName}.${eventString}`; + + const rawEventType = getEventListenerParamType(member, methodName); + // Don't append type path name to void event payload types + const eventType = rawEventType === 'void' ? 'void' : + `Crdp.${domainName}.${rawEventType}`; + + eventMap.set(eventName, eventType); + } + + return eventMap; +} + +const source = fs.readFileSync(crdpTypingFile, 'utf8'); +const sourceRoot = ts.createSourceFile(crdpTypingFile, source, ts.ScriptTarget.ES2017, false); + +const crdpDomainNames = getCrdpDomainNames(sourceRoot); +/** @type {Map} */ +let allEvents = new Map(); +for (const domainName of crdpDomainNames) { + const eventMap = getEventMap(sourceRoot, domainName); + allEvents = new Map([...allEvents, ...eventMap]); +} + +let crdpStr = headerBlock; +crdpStr += ` +declare global { + module LH { + export interface ${eventInterfaceName} {`; + +for (const [eventName, eventType] of allEvents) { + crdpStr += `\n '${eventName}': ${eventType};`; +} + +crdpStr += ` + } + } +} + +// empty export to keep file a module +export {} +`; + +// eslint-disable-next-line no-console +console.log('crdp mappings generated'); +fs.writeFileSync(lhCrdpExternsOutputFile, crdpStr); diff --git a/lighthouse-core/test/gather/driver-test.js b/lighthouse-core/test/gather/driver-test.js index a61f451e6ac2..929075564286 100644 --- a/lighthouse-core/test/gather/driver-test.js +++ b/lighthouse-core/test/gather/driver-test.js @@ -187,7 +187,7 @@ describe('Browser Driver', () => { return Promise.resolve(); } replayLog() { - redirectDevtoolsLog.forEach(msg => this.emit('notification', msg)); + redirectDevtoolsLog.forEach(msg => this.emit('protocolevent', msg)); } sendCommand(method) { const resolve = Promise.resolve(); diff --git a/package.json b/package.json index f75ad020aab4..9682f6446c26 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "type-check": "tsc -p .", "update:sample-artifacts": "node lighthouse-core/scripts/update-report-fixtures.js -G", "update:sample-json": "node ./lighthouse-cli -A=./lighthouse-core/test/results/artifacts --output=json --output-path=./lighthouse-core/test/results/sample_v2.json http://localhost/dobetterweb/dbw_tester.html", + "update:crdp-typings": "node lighthouse-core/scripts/extract-crdp-mapping.js", "mixed-content": "./lighthouse-cli/index.js --chrome-flags='--headless' --config-path=./lighthouse-core/config/mixed-content.js" }, "devDependencies": { @@ -82,7 +83,7 @@ "postinstall-prepare": "^1.0.1", "puppeteer": "^1.1.1", "sinon": "^2.3.5", - "typescript": "^2.8.0-rc", + "typescript": "2.9.0-dev.20180323", "vscode-chrome-debug-core": "^3.23.8", "zone.js": "^0.7.3" }, diff --git a/third-party/strict-event-emitter-types/LICENSE b/third-party/strict-event-emitter-types/LICENSE new file mode 100644 index 000000000000..5de634ee5354 --- /dev/null +++ b/third-party/strict-event-emitter-types/LICENSE @@ -0,0 +1,13 @@ +Copyright 2018 Brian Terlson + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. \ No newline at end of file diff --git a/third-party/strict-event-emitter-types/index.d.ts b/third-party/strict-event-emitter-types/index.d.ts new file mode 100644 index 000000000000..01cc94d76713 --- /dev/null +++ b/third-party/strict-event-emitter-types/index.d.ts @@ -0,0 +1,78 @@ +// Modified from +// https://github.com/bterlson/strict-event-emitter-types/blob/96b30cae8d128c166b575c4c9a524c997ab4f040/src/index.ts +// for better JSDoc support (as of TS 2.8, JSDoc does not support method overrides +// in JS, so not possible to implement the original OverriddenMethods). + +// Returns any keys of TRecord with the type of TMatch +export type MatchingKeys< + TRecord, + TMatch, + K extends keyof TRecord = keyof TRecord + > = K extends (TRecord[K] extends TMatch ? K : never) ? K : never; + +// Returns any property keys of Record with a void type +export type VoidKeys = MatchingKeys; + +// TODO: Stash under a symbol key once TS compiler bug is fixed +export interface TypeRecord { + ' _emitterType'?: T, + ' _eventsType'?: U, + ' _emitType'?: V +} + + +// EventEmitter method overrides, modified so no overloaded methods. +export type OverriddenMethods< + TEventRecord, + TEmitRecord = TEventRecord + > = { + on

(event: P, listener: TEventRecord[P] extends void ? () => void : (m: TEventRecord[P], ...args: any[]) => void): void + + addListener

(event: P, listener: TEventRecord[P] extends void ? () => void : (m: TEventRecord[P], ...args: any[]) => void): void + + addEventListener

(event: P, listener: TEventRecord[P] extends void ? () => void : (m: TEventRecord[P], ...args: any[]) => void): void + + removeListener

(event: P, listener: Function): any; + + once

(event: P, listener: TEventRecord[P] extends void ? () => void : (m: TEventRecord[P], ...args: any[]) => void): void + + // TODO(bckenny): breaking change from original. A void TEmitRecord[P] meant + // no second parameter, but now a second one is always required and must + // extend `void` (e.g. `undefined`). + emit

(event: P, request: TEmitRecord[P]): void; + } + +export type OverriddenKeys = keyof OverriddenMethods + +export type StrictEventEmitter< + TEmitterType, + TEventRecord, + TEmitRecord = TEventRecord, + UnneededMethods extends Exclude + = Exclude, + NeededMethods extends Exclude + = Exclude + > = + // Store the type parameters we've instantiated with so we can refer to them later + TypeRecord & + + // Pick all the methods on the original type we aren't going to override + Pick> & + + // Finally, pick the needed overrides (taking care not to add an override for a method + // that doesn't exist) + Pick, NeededMethods>; + +export default StrictEventEmitter; + +export type NoUndefined = T extends undefined ? never : T; + +export type StrictBroadcast< + TEmitter extends TypeRecord, + TEmitRecord extends NoUndefined = NoUndefined, + VK extends VoidKeys = VoidKeys, + NVK extends Exclude = Exclude + > = { + (event: E, request: TEmitRecord[E]): void; + (event: E): void; + } diff --git a/typings/crdp-mapping.d.ts b/typings/crdp-mapping.d.ts new file mode 100644 index 000000000000..85734284e9ee --- /dev/null +++ b/typings/crdp-mapping.d.ts @@ -0,0 +1,133 @@ +/** + * @license Copyright 2018 Google Inc. All Rights Reserved. + * 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. + */ + +// Generated by `yarn update:crdp-typings` + +declare global { + module LH { + export interface CrdpEvents { + 'Console.messageAdded': Crdp.Console.MessageAddedEvent; + 'Debugger.breakpointResolved': Crdp.Debugger.BreakpointResolvedEvent; + 'Debugger.paused': Crdp.Debugger.PausedEvent; + 'Debugger.resumed': void; + 'Debugger.scriptFailedToParse': Crdp.Debugger.ScriptFailedToParseEvent; + 'Debugger.scriptParsed': Crdp.Debugger.ScriptParsedEvent; + 'HeapProfiler.addHeapSnapshotChunk': Crdp.HeapProfiler.AddHeapSnapshotChunkEvent; + 'HeapProfiler.heapStatsUpdate': Crdp.HeapProfiler.HeapStatsUpdateEvent; + 'HeapProfiler.lastSeenObjectId': Crdp.HeapProfiler.LastSeenObjectIdEvent; + 'HeapProfiler.reportHeapSnapshotProgress': Crdp.HeapProfiler.ReportHeapSnapshotProgressEvent; + 'HeapProfiler.resetProfiles': void; + 'Profiler.consoleProfileFinished': Crdp.Profiler.ConsoleProfileFinishedEvent; + 'Profiler.consoleProfileStarted': Crdp.Profiler.ConsoleProfileStartedEvent; + 'Runtime.consoleAPICalled': Crdp.Runtime.ConsoleAPICalledEvent; + 'Runtime.exceptionRevoked': Crdp.Runtime.ExceptionRevokedEvent; + 'Runtime.exceptionThrown': Crdp.Runtime.ExceptionThrownEvent; + 'Runtime.executionContextCreated': Crdp.Runtime.ExecutionContextCreatedEvent; + 'Runtime.executionContextDestroyed': Crdp.Runtime.ExecutionContextDestroyedEvent; + 'Runtime.executionContextsCleared': void; + 'Runtime.inspectRequested': Crdp.Runtime.InspectRequestedEvent; + 'Animation.animationCanceled': Crdp.Animation.AnimationCanceledEvent; + 'Animation.animationCreated': Crdp.Animation.AnimationCreatedEvent; + 'Animation.animationStarted': Crdp.Animation.AnimationStartedEvent; + 'ApplicationCache.applicationCacheStatusUpdated': Crdp.ApplicationCache.ApplicationCacheStatusUpdatedEvent; + 'ApplicationCache.networkStateUpdated': Crdp.ApplicationCache.NetworkStateUpdatedEvent; + 'CSS.fontsUpdated': void; + 'CSS.mediaQueryResultChanged': void; + 'CSS.styleSheetAdded': Crdp.CSS.StyleSheetAddedEvent; + 'CSS.styleSheetChanged': Crdp.CSS.StyleSheetChangedEvent; + 'CSS.styleSheetRemoved': Crdp.CSS.StyleSheetRemovedEvent; + 'DOM.attributeModified': Crdp.DOM.AttributeModifiedEvent; + 'DOM.attributeRemoved': Crdp.DOM.AttributeRemovedEvent; + 'DOM.characterDataModified': Crdp.DOM.CharacterDataModifiedEvent; + 'DOM.childNodeCountUpdated': Crdp.DOM.ChildNodeCountUpdatedEvent; + 'DOM.childNodeInserted': Crdp.DOM.ChildNodeInsertedEvent; + 'DOM.childNodeRemoved': Crdp.DOM.ChildNodeRemovedEvent; + 'DOM.distributedNodesUpdated': Crdp.DOM.DistributedNodesUpdatedEvent; + 'DOM.documentUpdated': void; + 'DOM.inlineStyleInvalidated': Crdp.DOM.InlineStyleInvalidatedEvent; + 'DOM.pseudoElementAdded': Crdp.DOM.PseudoElementAddedEvent; + 'DOM.pseudoElementRemoved': Crdp.DOM.PseudoElementRemovedEvent; + 'DOM.setChildNodes': Crdp.DOM.SetChildNodesEvent; + 'DOM.shadowRootPopped': Crdp.DOM.ShadowRootPoppedEvent; + 'DOM.shadowRootPushed': Crdp.DOM.ShadowRootPushedEvent; + 'DOMStorage.domStorageItemAdded': Crdp.DOMStorage.DomStorageItemAddedEvent; + 'DOMStorage.domStorageItemRemoved': Crdp.DOMStorage.DomStorageItemRemovedEvent; + 'DOMStorage.domStorageItemUpdated': Crdp.DOMStorage.DomStorageItemUpdatedEvent; + 'DOMStorage.domStorageItemsCleared': Crdp.DOMStorage.DomStorageItemsClearedEvent; + 'Database.addDatabase': Crdp.Database.AddDatabaseEvent; + 'Emulation.virtualTimeAdvanced': Crdp.Emulation.VirtualTimeAdvancedEvent; + 'Emulation.virtualTimeBudgetExpired': void; + 'Emulation.virtualTimePaused': Crdp.Emulation.VirtualTimePausedEvent; + 'HeadlessExperimental.mainFrameReadyForScreenshots': void; + 'HeadlessExperimental.needsBeginFramesChanged': Crdp.HeadlessExperimental.NeedsBeginFramesChangedEvent; + 'Inspector.detached': Crdp.Inspector.DetachedEvent; + 'Inspector.targetCrashed': void; + 'LayerTree.layerPainted': Crdp.LayerTree.LayerPaintedEvent; + 'LayerTree.layerTreeDidChange': Crdp.LayerTree.LayerTreeDidChangeEvent; + 'Log.entryAdded': Crdp.Log.EntryAddedEvent; + 'Network.dataReceived': Crdp.Network.DataReceivedEvent; + 'Network.eventSourceMessageReceived': Crdp.Network.EventSourceMessageReceivedEvent; + 'Network.loadingFailed': Crdp.Network.LoadingFailedEvent; + 'Network.loadingFinished': Crdp.Network.LoadingFinishedEvent; + 'Network.requestIntercepted': Crdp.Network.RequestInterceptedEvent; + 'Network.requestServedFromCache': Crdp.Network.RequestServedFromCacheEvent; + 'Network.requestWillBeSent': Crdp.Network.RequestWillBeSentEvent; + 'Network.resourceChangedPriority': Crdp.Network.ResourceChangedPriorityEvent; + 'Network.responseReceived': Crdp.Network.ResponseReceivedEvent; + 'Network.webSocketClosed': Crdp.Network.WebSocketClosedEvent; + 'Network.webSocketCreated': Crdp.Network.WebSocketCreatedEvent; + 'Network.webSocketFrameError': Crdp.Network.WebSocketFrameErrorEvent; + 'Network.webSocketFrameReceived': Crdp.Network.WebSocketFrameReceivedEvent; + 'Network.webSocketFrameSent': Crdp.Network.WebSocketFrameSentEvent; + 'Network.webSocketHandshakeResponseReceived': Crdp.Network.WebSocketHandshakeResponseReceivedEvent; + 'Network.webSocketWillSendHandshakeRequest': Crdp.Network.WebSocketWillSendHandshakeRequestEvent; + 'Overlay.inspectNodeRequested': Crdp.Overlay.InspectNodeRequestedEvent; + 'Overlay.nodeHighlightRequested': Crdp.Overlay.NodeHighlightRequestedEvent; + 'Overlay.screenshotRequested': Crdp.Overlay.ScreenshotRequestedEvent; + 'Page.domContentEventFired': Crdp.Page.DomContentEventFiredEvent; + 'Page.frameAttached': Crdp.Page.FrameAttachedEvent; + 'Page.frameClearedScheduledNavigation': Crdp.Page.FrameClearedScheduledNavigationEvent; + 'Page.frameDetached': Crdp.Page.FrameDetachedEvent; + 'Page.frameNavigated': Crdp.Page.FrameNavigatedEvent; + 'Page.frameResized': void; + 'Page.frameScheduledNavigation': Crdp.Page.FrameScheduledNavigationEvent; + 'Page.frameStartedLoading': Crdp.Page.FrameStartedLoadingEvent; + 'Page.frameStoppedLoading': Crdp.Page.FrameStoppedLoadingEvent; + 'Page.interstitialHidden': void; + 'Page.interstitialShown': void; + 'Page.javascriptDialogClosed': Crdp.Page.JavascriptDialogClosedEvent; + 'Page.javascriptDialogOpening': Crdp.Page.JavascriptDialogOpeningEvent; + 'Page.lifecycleEvent': Crdp.Page.LifecycleEventEvent; + 'Page.loadEventFired': Crdp.Page.LoadEventFiredEvent; + 'Page.screencastFrame': Crdp.Page.ScreencastFrameEvent; + 'Page.screencastVisibilityChanged': Crdp.Page.ScreencastVisibilityChangedEvent; + 'Page.windowOpen': Crdp.Page.WindowOpenEvent; + 'Performance.metrics': Crdp.Performance.MetricsEvent; + 'Security.certificateError': Crdp.Security.CertificateErrorEvent; + 'Security.securityStateChanged': Crdp.Security.SecurityStateChangedEvent; + 'ServiceWorker.workerErrorReported': Crdp.ServiceWorker.WorkerErrorReportedEvent; + 'ServiceWorker.workerRegistrationUpdated': Crdp.ServiceWorker.WorkerRegistrationUpdatedEvent; + 'ServiceWorker.workerVersionUpdated': Crdp.ServiceWorker.WorkerVersionUpdatedEvent; + 'Storage.cacheStorageContentUpdated': Crdp.Storage.CacheStorageContentUpdatedEvent; + 'Storage.cacheStorageListUpdated': Crdp.Storage.CacheStorageListUpdatedEvent; + 'Storage.indexedDBContentUpdated': Crdp.Storage.IndexedDBContentUpdatedEvent; + 'Storage.indexedDBListUpdated': Crdp.Storage.IndexedDBListUpdatedEvent; + 'Target.attachedToTarget': Crdp.Target.AttachedToTargetEvent; + 'Target.detachedFromTarget': Crdp.Target.DetachedFromTargetEvent; + 'Target.receivedMessageFromTarget': Crdp.Target.ReceivedMessageFromTargetEvent; + 'Target.targetCreated': Crdp.Target.TargetCreatedEvent; + 'Target.targetDestroyed': Crdp.Target.TargetDestroyedEvent; + 'Target.targetInfoChanged': Crdp.Target.TargetInfoChangedEvent; + 'Tethering.accepted': Crdp.Tethering.AcceptedEvent; + 'Tracing.bufferUsage': Crdp.Tracing.BufferUsageEvent; + 'Tracing.dataCollected': Crdp.Tracing.DataCollectedEvent; + 'Tracing.tracingComplete': Crdp.Tracing.TracingCompleteEvent; + } + } +} + +// empty export to keep file a module +export {} diff --git a/typings/externs.d.ts b/typings/externs.d.ts index 9022325d9a20..0873ca119504 100644 --- a/typings/externs.d.ts +++ b/typings/externs.d.ts @@ -5,10 +5,15 @@ */ import _Crdp from '../node_modules/vscode-chrome-debug-core/lib/crdp/crdp'; +import _StrictEventEmitter from '../third-party/strict-event-emitter-types/index'; +import { EventEmitter } from 'events'; declare global { module LH { + // re-export useful type modules under global LH module. export import Crdp = _Crdp; + export type StrictEventEmitter = + _StrictEventEmitter; interface ThrottlingSettings { // simulation settings diff --git a/typings/protocol.d.ts b/typings/protocol.d.ts new file mode 100644 index 000000000000..fc68e89a3848 --- /dev/null +++ b/typings/protocol.d.ts @@ -0,0 +1,54 @@ +/** + * @license Copyright 2018 Google Inc. All Rights Reserved. + * 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. + */ + +declare global { + module LH.Protocol { + /** + * Union of raw (over the wire) message format of all possible Crdp events, + * of the form `{method: 'Domain.event', params: eventPayload}`. + * TODO(bckenny): currently any `void` payloads are defined as having value + * `undefined`, even though CrdpEventEmitter will treat those payloads as + * not existing. This is due to how we have to expose these emitters to JS. + * See https://github.com/bterlson/strict-event-emitter-types/issues/1 for + * search for the fix here. Complication should be entirely isolated to + * connection.js. + */ + export type RawEventMessage = RawEventMessageRecord[keyof RawEventMessageRecord]; + + /** + * Raw (over the wire) message format of all possible Crdp command responses. + * TODO(bckenny): add types for commands. + */ + export type RawCommandMessage = { + id: number; + [prop: string]: any; + } + + /** + * Raw (over the wire) message format of all possible Crdp events and command + * responses. + */ + export type RawMessage = RawCommandMessage | RawEventMessage; + } +} + +/** + * An intermediate type, used to create a record of all possible Crdp raw event + * messages, keyed on method. e.g. { + * 'Domain.method1Name': {method: 'Domain.method1Name', params: EventPayload1}, + * 'Domain.method2Name': {method: 'Domain.method2Name', params: EventPayload2}, + * } + */ +type RawEventMessageRecord = { + [K in keyof LH.CrdpEvents]: { + method: K, + // Drop void for `undefined` (so a JS value is valid). See above TODO + params: LH.CrdpEvents[K] extends void ? undefined: LH.CrdpEvents[K] + }; +} + +// empty export to keep file a module +export {} diff --git a/yarn.lock b/yarn.lock index 13e79848e12e..1ed04a294635 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4188,9 +4188,9 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -typescript@^2.8.0-rc: - version "2.8.0-rc" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.8.0-rc.tgz#a0256b7d1d39fb7493ba0403f55e95d31e8bc374" +typescript@2.9.0-dev.20180323: + version "2.9.0-dev.20180323" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.0-dev.20180323.tgz#9b33b1366a2b9af88e5e4de9a8e502f1df8b5aad" uglify-js@^2.6: version "2.7.3"