[JSDOC2TS]: Migrate extensions

Bug: chromium:1158760
Change-Id: I1e7be5b67bc4d0a03d26ad52793a79425bf1e6a4
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2789337
Reviewed-by: Tim van der Lippe <[email protected]>
Commit-Queue: Jan Scheffler <[email protected]>
diff --git a/front_end/extensions/ExtensionAPI.ts b/front_end/extensions/ExtensionAPI.ts
new file mode 100644
index 0000000..e35e664
--- /dev/null
+++ b/front_end/extensions/ExtensionAPI.ts
@@ -0,0 +1,930 @@
+/*
+ * Copyright (C) 2012 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// @ts-nocheck
+// TODO(crbug.com/1011811): Enable TypeScript compiler checks
+
+// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
+/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/naming-convention */
+
+export function defineCommonExtensionSymbols(apiPrivate: any): void {
+  if (!apiPrivate.panels) {
+    apiPrivate.panels = {};
+  }
+  apiPrivate.panels.SearchAction = {
+    CancelSearch: 'cancelSearch',
+    PerformSearch: 'performSearch',
+    NextSearchResult: 'nextSearchResult',
+    PreviousSearchResult: 'previousSearchResult',
+  };
+
+  /** @enum {string} */
+  apiPrivate.Events = {
+    ButtonClicked: 'button-clicked-',
+    PanelObjectSelected: 'panel-objectSelected-',
+    NetworkRequestFinished: 'network-request-finished',
+    OpenResource: 'open-resource',
+    PanelSearch: 'panel-search-',
+    RecordingStarted: 'trace-recording-started-',
+    RecordingStopped: 'trace-recording-stopped-',
+    ResourceAdded: 'resource-added',
+    ResourceContentCommitted: 'resource-content-committed',
+    ViewShown: 'view-shown-',
+    ViewHidden: 'view-hidden-',
+  };
+
+  /** @enum {string} */
+  apiPrivate.Commands = {
+    AddRequestHeaders: 'addRequestHeaders',
+    AddTraceProvider: 'addTraceProvider',
+    ApplyStyleSheet: 'applyStyleSheet',
+    CompleteTraceSession: 'completeTraceSession',
+    CreatePanel: 'createPanel',
+    CreateSidebarPane: 'createSidebarPane',
+    CreateToolbarButton: 'createToolbarButton',
+    EvaluateOnInspectedPage: 'evaluateOnInspectedPage',
+    ForwardKeyboardEvent: '_forwardKeyboardEvent',
+    GetHAR: 'getHAR',
+    GetPageResources: 'getPageResources',
+    GetRequestContent: 'getRequestContent',
+    GetResourceContent: 'getResourceContent',
+    InspectedURLChanged: 'inspectedURLChanged',
+    OpenResource: 'openResource',
+    Reload: 'Reload',
+    Subscribe: 'subscribe',
+    SetOpenResourceHandler: 'setOpenResourceHandler',
+    SetResourceContent: 'setResourceContent',
+    SetSidebarContent: 'setSidebarContent',
+    SetSidebarHeight: 'setSidebarHeight',
+    SetSidebarPage: 'setSidebarPage',
+    ShowPanel: 'showPanel',
+    Unsubscribe: 'unsubscribe',
+    UpdateButton: 'updateButton',
+    RegisterLanguageExtensionPlugin: 'registerLanguageExtensionPlugin',
+  };
+
+  /** @enum {string} */
+  apiPrivate.LanguageExtensionPluginCommands = {
+    AddRawModule: 'addRawModule',
+    RemoveRawModule: 'removeRawModule',
+    SourceLocationToRawLocation: 'sourceLocationToRawLocation',
+    RawLocationToSourceLocation: 'rawLocationToSourceLocation',
+    GetScopeInfo: 'getScopeInfo',
+    ListVariablesInScope: 'listVariablesInScope',
+    GetTypeInfo: 'getTypeInfo',
+    GetFormatter: 'getFormatter',
+    GetInspectableAddress: 'getInspectableAddress',
+    GetFunctionInfo: 'getFunctionInfo',
+    GetInlinedFunctionRanges: 'getInlinedFunctionRanges',
+    GetInlinedCalleesRanges: 'getInlinedCalleesRanges',
+    GetMappedLines: 'getMappedLines',
+  };
+
+  /** @enum {string} */
+  apiPrivate.LanguageExtensionPluginEvents = {
+    UnregisteredLanguageExtensionPlugin: 'unregisteredLanguageExtensionPlugin',
+  };
+}
+
+self.injectedExtensionAPI = function(
+    extensionInfo: any, inspectedTabId: string, themeName: string, keysToForward: number[],
+    testHook: (arg0: Object, arg1: Object) => any, injectedScriptId: number): void {
+  const keysToForwardSet = new Set<number>(keysToForward);
+  const chrome = window.chrome || {};
+
+  const devtools_descriptor = Object.getOwnPropertyDescriptor(chrome, 'devtools');
+  if (devtools_descriptor) {
+    return;
+  }
+
+  const apiPrivate = {};
+
+  defineCommonExtensionSymbols(apiPrivate);
+
+  const commands = apiPrivate.Commands;
+  const languageExtensionPluginCommands = apiPrivate.LanguageExtensionPluginCommands;
+  const languageExtensionPluginEvents = apiPrivate.LanguageExtensionPluginEvents;
+  const events = apiPrivate.Events;
+  let userAction = false;
+
+  // Here and below, all constructors are private to API implementation.
+  // For a public type Foo, if internal fields are present, these are on
+  // a private FooImpl type, an instance of FooImpl is used in a closure
+  // by Foo consutrctor to re-bind publicly exported members to an instance
+  // of Foo.
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/naming-convention
+  function EventSinkImpl(type: any, customDispatch: any): void {
+    this._type = type;
+    this._listeners = [];
+    this._customDispatch = customDispatch;
+  }
+
+  EventSinkImpl.prototype = {
+
+    addListener: function(callback: any): void {
+      if (typeof callback !== 'function') {
+        throw 'addListener: callback is not a function';
+      }
+      if (this._listeners.length === 0) {
+        extensionServer.sendRequest({command: commands.Subscribe, type: this._type});
+      }
+      this._listeners.push(callback);
+      extensionServer.registerHandler('notify-' + this._type, this._dispatch.bind(this));
+    },
+
+
+    removeListener: function(callback: any): void {
+      const listeners = this._listeners;
+
+      for (let i = 0; i < listeners.length; ++i) {
+        if (listeners[i] === callback) {
+          listeners.splice(i, 1);
+          break;
+        }
+      }
+      if (this._listeners.length === 0) {
+        extensionServer.sendRequest({command: commands.Unsubscribe, type: this._type});
+      }
+    },
+
+
+    _fire: function _(_vararg: any): void {
+      const listeners = this._listeners.slice();
+      for (let i = 0; i < listeners.length; ++i) {
+        listeners[i].apply(null, arguments);
+      }
+    },
+
+
+    _dispatch: function(request: any): void {
+      if (this._customDispatch) {
+        this._customDispatch.call(this, request);
+      } else {
+        this._fire.apply(this, request.arguments);
+      }
+    },
+  };
+
+  /**
+   * @constructor
+   */
+
+  function InspectorExtensionAPI(): void {
+    this.inspectedWindow = new InspectedWindow();
+    this.panels = new Panels();
+    this.network = new Network();
+    this.timeline = new Timeline();
+    this.languageServices = new LanguageServicesAPI();
+    defineDeprecatedProperty(this, 'webInspector', 'resources', 'network');
+  }
+
+  /**
+   * @constructor
+   */
+
+  function Network(): void {
+    function dispatchRequestEvent(this: EventSinkImpl, message: any): void {
+      const request = message.arguments[1];
+      request.__proto__ = new Request(message.arguments[0]);
+      this._fire(request);
+    }
+    this.onRequestFinished = new EventSink(events.NetworkRequestFinished, dispatchRequestEvent);
+    defineDeprecatedProperty(this, 'network', 'onFinished', 'onRequestFinished');
+    this.onNavigated = new EventSink(events.InspectedURLChanged);
+  }
+
+  Network.prototype = {
+    getHAR: function(callback: any): void {
+      function callbackWrapper(result: any): void {
+        const entries = (result && result.entries) || [];
+        for (let i = 0; i < entries.length; ++i) {
+          entries[i].__proto__ = new Request(entries[i]._requestId);
+          delete entries[i]._requestId;
+        }
+        callback(result);
+      }
+      extensionServer.sendRequest({command: commands.GetHAR}, callback && callbackWrapper);
+    },
+
+    addRequestHeaders: function(headers: any): void {
+      extensionServer.sendRequest(
+          {command: commands.AddRequestHeaders, headers: headers, extensionId: window.location.hostname});
+    },
+  };
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/naming-convention
+  function RequestImpl(id: any): void {
+    this._id = id;
+  }
+
+  RequestImpl.prototype = {
+    getContent: function(callback: any): void {
+      function callbackWrapper(response: any): void {
+        callback(response.content, response.encoding);
+      }
+      extensionServer.sendRequest({command: commands.GetRequestContent, id: this._id}, callback && callbackWrapper);
+    },
+  };
+
+  /**
+   * @constructor
+   */
+  function Panels(): void {
+    const panels = {
+      elements: new ElementsPanel(),
+      sources: new SourcesPanel(),
+    };
+
+    function panelGetter(name: any): any {
+      return panels[name];
+    }
+    for (const panel in panels) {
+      Object.defineProperty(this, panel, {get: panelGetter.bind(null, panel), enumerable: true});
+    }
+    this.applyStyleSheet = function(styleSheet: any): void {
+      extensionServer.sendRequest({command: commands.ApplyStyleSheet, styleSheet: styleSheet});
+    };
+  }
+
+  Panels.prototype = {
+    create: function(title: any, icon: any, page: any, callback: any): void {
+      const id = 'extension-panel-' + extensionServer.nextObjectId();
+      const request = {command: commands.CreatePanel, id: id, title: title, icon: icon, page: page};
+      extensionServer.sendRequest(request, callback && callback.bind(this, new ExtensionPanel(id)));
+    },
+
+    setOpenResourceHandler: function(callback: any): void {
+      const hadHandler = extensionServer.hasHandler(events.OpenResource);
+
+      function callbackWrapper(message: any): void {
+        // Allow the panel to show itself when handling the event.
+        userAction = true;
+        try {
+          callback.call(null, new Resource(message.resource), message.lineNumber);
+        } finally {
+          userAction = false;
+        }
+      }
+
+      if (!callback) {
+        extensionServer.unregisterHandler(events.OpenResource);
+      } else {
+        extensionServer.registerHandler(events.OpenResource, callbackWrapper);
+      }
+
+      // Only send command if we either removed an existing handler or added handler and had none before.
+      if (hadHandler === !callback) {
+        extensionServer.sendRequest({command: commands.SetOpenResourceHandler, 'handlerPresent': Boolean(callback)});
+      }
+    },
+
+    openResource: function(url: any, lineNumber: any, callback: any): void {
+      extensionServer.sendRequest({command: commands.OpenResource, 'url': url, 'lineNumber': lineNumber}, callback);
+    },
+
+    get SearchAction(): any {
+      return apiPrivate.panels.SearchAction;
+    },
+  };
+
+  /**
+   * @constructor
+   */
+  function ExtensionViewImpl(id: any): void {
+    this._id = id;
+
+    function dispatchShowEvent(this: EventSinkImpl, message: any): void {
+      const frameIndex = message.arguments[0];
+      if (typeof frameIndex === 'number') {
+        this._fire(window.parent.frames[frameIndex]);
+      } else {
+        this._fire();
+      }
+    }
+
+    if (id) {
+      this.onShown = new EventSink(events.ViewShown + id, dispatchShowEvent);
+      this.onHidden = new EventSink(events.ViewHidden + id);
+    }
+  }
+
+  /**
+   * @constructor
+   * @extends {ExtensionViewImpl}
+   */
+  function PanelWithSidebarImpl(hostPanelName: string): void {
+    ExtensionViewImpl.call(this, null);
+    this._hostPanelName = hostPanelName;
+    this.onSelectionChanged = new EventSink(events.PanelObjectSelected + hostPanelName);
+  }
+
+  PanelWithSidebarImpl.prototype = {
+    createSidebarPane: function(title: any, callback: any): void {
+      const id = 'extension-sidebar-' + extensionServer.nextObjectId();
+      const request = {command: commands.CreateSidebarPane, panel: this._hostPanelName, id: id, title: title};
+      function callbackWrapper(): void {
+        callback(new ExtensionSidebarPane(id));
+      }
+      extensionServer.sendRequest(request, callback && callbackWrapper);
+    },
+
+    __proto__: ExtensionViewImpl.prototype,
+  };
+
+  /**
+   * @constructor
+   */
+  function LanguageServicesAPIImpl(): void {
+    /** @type {!Map<*, !MessagePort>} */
+    this._plugins = new Map();
+  }
+
+  LanguageServicesAPIImpl.prototype = {
+    registerLanguageExtensionPlugin: async function(plugin: any, pluginName: string, supportedScriptTypes: {
+      language: string,
+      symbol_types: string[],
+    }): Promise<void> {
+      if (this._plugins.has(plugin)) {
+        throw new Error(`Tried to register plugin '${pluginName}' twice`);
+      }
+      const channel = new MessageChannel();
+      const port = channel.port1;
+      this._plugins.set(plugin, port);
+      port.onmessage = ({data: {requestId, method, parameters}}: MessageEvent<any>): void => {
+        console.time(`${requestId}: ${method}`);
+        dispatchMethodCall(method, parameters)
+            .then(result => port.postMessage({requestId, result}))
+            .catch(error => port.postMessage({requestId, error: {message: error.message}}))
+            .finally(() => console.timeEnd(`${requestId}: ${method}`));
+      };
+
+      function dispatchMethodCall(method: string, parameters: any): Promise<any> {
+        switch (method) {
+          case languageExtensionPluginCommands.AddRawModule:
+            return plugin.addRawModule(parameters.rawModuleId, parameters.symbolsURL, parameters.rawModule);
+          case languageExtensionPluginCommands.RemoveRawModule:
+            return plugin.removeRawModule(parameters.rawModuleId);
+          case languageExtensionPluginCommands.SourceLocationToRawLocation:
+            return plugin.sourceLocationToRawLocation(parameters.sourceLocation);
+          case languageExtensionPluginCommands.RawLocationToSourceLocation:
+            return plugin.rawLocationToSourceLocation(parameters.rawLocation);
+          case languageExtensionPluginCommands.GetScopeInfo:
+            return plugin.getScopeInfo(parameters.type);
+          case languageExtensionPluginCommands.ListVariablesInScope:
+            return plugin.listVariablesInScope(parameters.rawLocation);
+          case languageExtensionPluginCommands.GetTypeInfo:
+            return plugin.getTypeInfo(parameters.expression, parameters.context);
+          case languageExtensionPluginCommands.GetFormatter:
+            return plugin.getFormatter(parameters.expressionOrField, parameters.context);
+          case languageExtensionPluginCommands.GetInspectableAddress:
+            if ('getInspectableAddress' in plugin) {
+              return plugin.getInspectableAddress(parameters.field);
+            }
+            return Promise.resolve({js: ''});
+          case languageExtensionPluginCommands.GetFunctionInfo:
+            return plugin.getFunctionInfo(parameters.rawLocation);
+          case languageExtensionPluginCommands.GetInlinedFunctionRanges:
+            return plugin.getInlinedFunctionRanges(parameters.rawLocation);
+          case languageExtensionPluginCommands.GetInlinedCalleesRanges:
+            return plugin.getInlinedCalleesRanges(parameters.rawLocation);
+          case languageExtensionPluginCommands.GetMappedLines:
+            if ('getMappedLines' in plugin) {
+              return plugin.getMappedLines(parameters.rawModuleId, parameters.sourceFileURL);
+            }
+            return Promise.resolve(undefined);
+        }
+        throw new Error(`Unknown language plugin method ${method}`);
+      }
+
+      await new Promise(resolve => {
+        extensionServer.sendRequest(
+            {command: commands.RegisterLanguageExtensionPlugin, pluginName, port: channel.port2, supportedScriptTypes},
+            () => resolve(), [channel.port2]);
+      });
+    },
+
+    unregisterLanguageExtensionPlugin: async function(plugin: any): Promise<void> {
+      const port = this._plugins.get(plugin);
+      if (!port) {
+        throw new Error('Tried to unregister a plugin that was not previously registered');
+      }
+      this._plugins.delete(plugin);
+      port.postMessage({event: languageExtensionPluginEvents.UnregisteredLanguageExtensionPlugin});
+      port.close();
+    },
+  };
+
+  function declareInterfaceClass(implConstructor: any): (...args: any[]) => void {
+    return function(): void {
+      const impl = {__proto__: implConstructor.prototype};
+      implConstructor.apply(impl, arguments);
+      populateInterfaceClass(this, impl);
+    };
+  }
+
+  function defineDeprecatedProperty(object: any, className: any, oldName: any, newName: any): void {
+    let warningGiven = false;
+    function getter(): any {
+      if (!warningGiven) {
+        console.warn(className + '.' + oldName + ' is deprecated. Use ' + className + '.' + newName + ' instead');
+        warningGiven = true;
+      }
+      return object[newName];
+    }
+    object.__defineGetter__(oldName, getter);
+  }
+
+  function extractCallbackArgument(args: any): any {
+    const lastArgument = args[args.length - 1];
+    return typeof lastArgument === 'function' ? lastArgument : undefined;
+  }
+
+  const LanguageServicesAPI = declareInterfaceClass(LanguageServicesAPIImpl);
+  const Button = declareInterfaceClass(ButtonImpl);
+  const EventSink = declareInterfaceClass(EventSinkImpl);
+  const ExtensionPanel = declareInterfaceClass(ExtensionPanelImpl);
+  const ExtensionSidebarPane = declareInterfaceClass(ExtensionSidebarPaneImpl);
+  /**
+   * @constructor
+   * @param {string} hostPanelName
+   */
+  const PanelWithSidebarClass = declareInterfaceClass(PanelWithSidebarImpl);
+  const Request = declareInterfaceClass(RequestImpl);
+  const Resource = declareInterfaceClass(ResourceImpl);
+  const TraceSession = declareInterfaceClass(TraceSessionImpl);
+
+  class ElementsPanel extends PanelWithSidebarClass {
+    constructor() {
+      super('elements');
+    }
+  }
+
+  class SourcesPanel extends PanelWithSidebarClass {
+    constructor() {
+      super('sources');
+    }
+  }
+
+  /**
+   * @constructor
+   * @extends {ExtensionViewImpl}
+   */
+  function ExtensionPanelImpl(id: any): void {
+    ExtensionViewImpl.call(this, id);
+    this.onSearch = new EventSink(events.PanelSearch + id);
+  }
+
+  ExtensionPanelImpl.prototype = {
+    createStatusBarButton: function(iconPath: any, tooltipText: any, disabled: any): Object {
+      const id = 'button-' + extensionServer.nextObjectId();
+      const request = {
+        command: commands.CreateToolbarButton,
+        panel: this._id,
+        id: id,
+        icon: iconPath,
+        tooltip: tooltipText,
+        disabled: Boolean(disabled),
+      };
+      extensionServer.sendRequest(request);
+      return new Button(id);
+    },
+
+    show: function(): void {
+      if (!userAction) {
+        return;
+      }
+
+      const request = {command: commands.ShowPanel, id: this._id};
+      extensionServer.sendRequest(request);
+    },
+
+    __proto__: ExtensionViewImpl.prototype,
+  };
+
+  /**
+   * @constructor
+   * @extends {ExtensionViewImpl}
+   */
+  function ExtensionSidebarPaneImpl(id: any): void {
+    ExtensionViewImpl.call(this, id);
+  }
+
+  ExtensionSidebarPaneImpl.prototype = {
+    setHeight: function(height: any): void {
+      extensionServer.sendRequest({command: commands.SetSidebarHeight, id: this._id, height: height});
+    },
+
+    setExpression: function(expression: any, rootTitle: any, evaluateOptions: any): void {
+      const request = {
+        command: commands.SetSidebarContent,
+        id: this._id,
+        expression: expression,
+        rootTitle: rootTitle,
+        evaluateOnPage: true,
+      };
+      if (typeof evaluateOptions === 'object') {
+        request.evaluateOptions = evaluateOptions;
+      }
+      extensionServer.sendRequest(request, extractCallbackArgument(arguments));
+    },
+
+    setObject: function(jsonObject: any, rootTitle: any, callback: any): void {
+      extensionServer.sendRequest(
+          {command: commands.SetSidebarContent, id: this._id, expression: jsonObject, rootTitle: rootTitle}, callback);
+    },
+
+    setPage: function(page: any): void {
+      extensionServer.sendRequest({command: commands.SetSidebarPage, id: this._id, page: page});
+    },
+
+    __proto__: ExtensionViewImpl.prototype,
+  };
+
+  /**
+   * @constructor
+   */
+  function ButtonImpl(id: any): void {
+    this._id = id;
+    this.onClicked = new EventSink(events.ButtonClicked + id);
+  }
+
+  ButtonImpl.prototype = {
+    update: function(iconPath: any, tooltipText: any, disabled: any): void {
+      const request = {
+        command: commands.UpdateButton,
+        id: this._id,
+        icon: iconPath,
+        tooltip: tooltipText,
+        disabled: Boolean(disabled),
+      };
+      extensionServer.sendRequest(request);
+    },
+  };
+
+  /**
+   * @constructor
+   */
+  function Timeline(): void {
+  }
+
+  Timeline.prototype = {
+    addTraceProvider: function(categoryName: string, categoryTooltip: string): TraceProvider {
+      const id = 'extension-trace-provider-' + extensionServer.nextObjectId();
+      extensionServer.sendRequest(
+          {command: commands.AddTraceProvider, id: id, categoryName: categoryName, categoryTooltip: categoryTooltip});
+      return new TraceProvider(id);
+    },
+  };
+
+  /**
+   * @constructor
+   */
+  function TraceSessionImpl(id: string): void {
+    this._id = id;
+  }
+
+  TraceSessionImpl.prototype = {
+    complete: function(url?: string, timeOffset?: number): void {
+      const request =
+          {command: commands.CompleteTraceSession, id: this._id, url: url || '', timeOffset: timeOffset || 0};
+      extensionServer.sendRequest(request);
+    },
+  };
+
+  /**
+   * @constructor
+   */
+  function TraceProvider(id: string): void {
+    function dispatchRecordingStarted(this: EventSinkImpl, message: any): void {
+      const sessionId = message.arguments[0];
+      this._fire(new TraceSession(sessionId));
+    }
+
+    this.onRecordingStarted = new EventSink(events.RecordingStarted + id, dispatchRecordingStarted);
+    this.onRecordingStopped = new EventSink(events.RecordingStopped + id);
+  }
+
+  /**
+   * @constructor
+   */
+  function InspectedWindow(): void {
+    function dispatchResourceEvent(this: EventSinkImpl, message: any): void {
+      this._fire(new Resource(message.arguments[0]));
+    }
+
+    function dispatchResourceContentEvent(this: EventSinkImpl, message: any): void {
+      this._fire(new Resource(message.arguments[0]), message.arguments[1]);
+    }
+
+    this.onResourceAdded = new EventSink(events.ResourceAdded, dispatchResourceEvent);
+    this.onResourceContentCommitted = new EventSink(events.ResourceContentCommitted, dispatchResourceContentEvent);
+  }
+
+  InspectedWindow.prototype = {
+    reload: function(optionsOrUserAgent: any): void {
+      let options: {
+        userAgent: string,
+      }|null = null;
+      if (typeof optionsOrUserAgent === 'object') {
+        options = optionsOrUserAgent;
+      } else if (typeof optionsOrUserAgent === 'string') {
+        options = {userAgent: optionsOrUserAgent};
+        console.warn(
+            'Passing userAgent as string parameter to inspectedWindow.reload() is deprecated. ' +
+            'Use inspectedWindow.reload({ userAgent: value}) instead.');
+      }
+      extensionServer.sendRequest({command: commands.Reload, options: options});
+    },
+
+    eval: function(expression: any, evaluateOptions: any): Object |
+        null {
+          const callback = extractCallbackArgument(arguments);
+          function callbackWrapper(result: any): void {
+            if (result.isError || result.isException) {
+              callback(undefined, result);
+            } else {
+              callback(result.value);
+            }
+          }
+          const request = {command: commands.EvaluateOnInspectedPage, expression: expression};
+          if (typeof evaluateOptions === 'object') {
+            request.evaluateOptions = evaluateOptions;
+          }
+          extensionServer.sendRequest(request, callback && callbackWrapper);
+          return null;
+        },
+
+    getResources: function(callback: any): void {
+      function wrapResource(resourceData: any): any {
+        return new Resource(resourceData);
+      }
+      function callbackWrapper(resources: any): void {
+        callback(resources.map(wrapResource));
+      }
+      extensionServer.sendRequest({command: commands.GetPageResources}, callback && callbackWrapper);
+    },
+  };
+
+  /**
+   * @constructor
+   */
+  function ResourceImpl(resourceData: any): void {
+    this._url = resourceData.url;
+    this._type = resourceData.type;
+  }
+
+  ResourceImpl.prototype = {
+    get url(): string {
+      return this._url;
+    },
+
+    get type(): string {
+      return this._type;
+    },
+
+    getContent: function(callback: any): void {
+      function callbackWrapper(response: any): void {
+        callback(response.content, response.encoding);
+      }
+
+      extensionServer.sendRequest({command: commands.GetResourceContent, url: this._url}, callback && callbackWrapper);
+    },
+
+    setContent: function(content: any, commit: any, callback: any): void {
+      extensionServer.sendRequest(
+          {command: commands.SetResourceContent, url: this._url, content: content, commit: commit}, callback);
+    },
+  };
+
+  function getTabId(): string {
+    return inspectedTabId;
+  }
+
+  let keyboardEventRequestQueue: any[] = [];
+  let forwardTimer: number|null = null;
+  function forwardKeyboardEvent(event: any): void {
+    // Check if the event should be forwarded.
+    // This is a workaround for crbug.com/923338.
+    const focused = document.activeElement;
+    if (focused) {
+      const isInput = focused.nodeName === 'INPUT' || focused.nodeName === 'TEXTAREA';
+      if (isInput && !(event.ctrlKey || event.altKey || event.metaKey)) {
+        return;
+      }
+    }
+
+    let modifiers = 0;
+    if (event.shiftKey) {
+      modifiers |= 1;
+    }
+    if (event.ctrlKey) {
+      modifiers |= 2;
+    }
+    if (event.altKey) {
+      modifiers |= 4;
+    }
+    if (event.metaKey) {
+      modifiers |= 8;
+    }
+    const num = (event.keyCode & 255) | (modifiers << 8);
+    // We only care about global hotkeys, not about random text
+    if (!keysToForwardSet.has(num)) {
+      return;
+    }
+    event.preventDefault();
+    const requestPayload = {
+      eventType: event.type,
+      ctrlKey: event.ctrlKey,
+      altKey: event.altKey,
+      metaKey: event.metaKey,
+      shiftKey: event.shiftKey,
+      keyIdentifier: event.keyIdentifier,
+      key: event.key,
+      code: event.code,
+      location: event.location,
+      keyCode: event.keyCode,
+    };
+    keyboardEventRequestQueue.push(requestPayload);
+    if (!forwardTimer) {
+      forwardTimer = setTimeout(forwardEventQueue, 0);
+    }
+  }
+
+  function forwardEventQueue(): void {
+    forwardTimer = null;
+    const request = {command: commands.ForwardKeyboardEvent, entries: keyboardEventRequestQueue};
+    extensionServer.sendRequest(request);
+    keyboardEventRequestQueue = [];
+  }
+
+  document.addEventListener('keydown', forwardKeyboardEvent, false);
+
+  /**
+   * @constructor
+   */
+  function ExtensionServerClient(): void {
+    this._callbacks = {};
+    this._handlers = {};
+    this._lastRequestId = 0;
+    this._lastObjectId = 0;
+
+    this.registerHandler('callback', this._onCallback.bind(this));
+
+    const channel = new MessageChannel();
+    this._port = channel.port1;
+    this._port.addEventListener('message', this._onMessage.bind(this), false);
+    this._port.start();
+
+    window.parent.postMessage('registerExtension', '*', [channel.port2]);
+  }
+
+  ExtensionServerClient.prototype = {
+    sendRequest: function(message: Object, callback?: (() => any), transfers?: any[]): void {
+      if (typeof callback === 'function') {
+        message.requestId = this._registerCallback(callback);
+      }
+      this._port.postMessage(message, transfers);
+    },
+
+    hasHandler: function(command: any): boolean {
+      return Boolean(this._handlers[command]);
+    },
+
+    registerHandler: function(command: any, handler: any): void {
+      this._handlers[command] = handler;
+    },
+
+    unregisterHandler: function(command: any): void {
+      delete this._handlers[command];
+    },
+
+    nextObjectId: function(): string {
+      return injectedScriptId.toString() + '_' + ++this._lastObjectId;
+    },
+
+    _registerCallback: function(callback: any): number {
+      const id = ++this._lastRequestId;
+      this._callbacks[id] = callback;
+      return id;
+    },
+
+    _onCallback: function(request: any): void {
+      if (request.requestId in this._callbacks) {
+        const callback = this._callbacks[request.requestId];
+        delete this._callbacks[request.requestId];
+        callback(request.result);
+      }
+    },
+
+    _onMessage: function(event: any): void {
+      const request = event.data;
+      const handler = this._handlers[request.command];
+      if (handler) {
+        handler.call(this, request);
+      }
+    },
+  };
+
+  function populateInterfaceClass(interfaze: any, implementation: any): void {
+    for (const member in implementation) {
+      if (member.charAt(0) === '_') {
+        continue;
+      }
+      let descriptor: (PropertyDescriptor|undefined)|null = null;
+      // Traverse prototype chain until we find the owner.
+      for (let owner = implementation; owner && !descriptor; owner = owner.__proto__) {
+        descriptor = Object.getOwnPropertyDescriptor(owner, member);
+      }
+      if (!descriptor) {
+        continue;
+      }
+      if (typeof descriptor.value === 'function') {
+        interfaze[member] = descriptor.value.bind(implementation);
+      } else if (typeof descriptor.get === 'function') {
+        interfaze.__defineGetter__(member, descriptor.get.bind(implementation));
+      } else {
+        Object.defineProperty(interfaze, member, descriptor);
+      }
+    }
+  }
+
+  const extensionServer = new ExtensionServerClient();
+  const coreAPI = new InspectorExtensionAPI();
+
+  Object.defineProperty(chrome, 'devtools', {value: {}, enumerable: true});
+
+  // Only expose tabId on chrome.devtools.inspectedWindow, not webInspector.inspectedWindow.
+  chrome.devtools.inspectedWindow = {};
+  Object.defineProperty(chrome.devtools.inspectedWindow, 'tabId', {get: getTabId});
+  chrome.devtools.inspectedWindow.__proto__ = coreAPI.inspectedWindow;
+  chrome.devtools.network = coreAPI.network;
+  chrome.devtools.panels = coreAPI.panels;
+  chrome.devtools.panels.themeName = themeName;
+  chrome.devtools.languageServices = new LanguageServicesAPI();
+
+  // default to expose experimental APIs for now.
+  if (extensionInfo.exposeExperimentalAPIs !== false) {
+    chrome.experimental = chrome.experimental || {};
+    chrome.experimental.devtools = chrome.experimental.devtools || {};
+
+    const properties = Object.getOwnPropertyNames(coreAPI);
+    for (let i = 0; i < properties.length; ++i) {
+      const descriptor = Object.getOwnPropertyDescriptor(coreAPI, properties[i]);
+      if (descriptor) {
+        Object.defineProperty(chrome.experimental.devtools, properties[i], descriptor);
+      }
+    }
+    chrome.experimental.devtools.inspectedWindow = chrome.devtools.inspectedWindow;
+  }
+
+  if (extensionInfo.exposeWebInspectorNamespace) {
+    window.webInspector = coreAPI;
+  }
+  testHook(extensionServer, coreAPI);
+};
+
+self.buildExtensionAPIInjectedScript = function(
+    extensionInfo: {
+      startPage: string,
+      name: string,
+      exposeExperimentalAPIs: boolean,
+    },
+    inspectedTabId: string, themeName: string, keysToForward: number[],
+    testHook: ((arg0: Object, arg1: Object) => any)|undefined): string {
+  const argumentsJSON =
+      [extensionInfo, inspectedTabId || null, themeName, keysToForward].map(_ => JSON.stringify(_)).join(',');
+  if (!testHook) {
+    testHook = (): void => {};
+  }
+  return '(function(injectedScriptId){ ' + defineCommonExtensionSymbols.toString() + ';' +
+      '(' + self.injectedExtensionAPI.toString() + ')(' + argumentsJSON + ',' + testHook + ', injectedScriptId);' +
+      '})';
+};