blob: c6813adaf7d9aac907a5cb965434470379de0633 [file] [log] [blame]
/*
* 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.
*/
function defineCommonExtensionSymbols(apiPrivate) {
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'
};
}
/**
* @param {!ExtensionDescriptor} extensionInfo
* @param {string} inspectedTabId
* @param {string} themeName
* @param {!Array<number>} keysToForward
* @param {number} injectedScriptId
* @param {function(!Object, !Object)} testHook
* @suppressGlobalPropertiesCheck
*/
self.injectedExtensionAPI = function(
extensionInfo, inspectedTabId, themeName, keysToForward, testHook, injectedScriptId) {
const keysToForwardSet = new Set(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 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.
/**
* @constructor
*/
function EventSinkImpl(type, customDispatch) {
this._type = type;
this._listeners = [];
this._customDispatch = customDispatch;
}
EventSinkImpl.prototype = {
addListener: function(callback) {
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) {
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});
}
},
/**
* @param {...} vararg
*/
_fire: function(vararg) {
const listeners = this._listeners.slice();
for (let i = 0; i < listeners.length; ++i) {
listeners[i].apply(null, arguments);
}
},
_dispatch: function(request) {
if (this._customDispatch) {
this._customDispatch.call(this, request);
} else {
this._fire.apply(this, request.arguments);
}
}
};
/**
* @constructor
*/
function InspectorExtensionAPI() {
this.inspectedWindow = new InspectedWindow();
this.panels = new Panels();
this.network = new Network();
this.timeline = new Timeline();
defineDeprecatedProperty(this, 'webInspector', 'resources', 'network');
}
/**
* @constructor
*/
function Network() {
/**
* @this {EventSinkImpl}
*/
function dispatchRequestEvent(message) {
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) {
function callbackWrapper(result) {
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) {
extensionServer.sendRequest(
{command: commands.AddRequestHeaders, headers: headers, extensionId: window.location.hostname});
}
};
/**
* @constructor
*/
function RequestImpl(id) {
this._id = id;
}
RequestImpl.prototype = {
getContent: function(callback) {
function callbackWrapper(response) {
callback(response.content, response.encoding);
}
extensionServer.sendRequest({command: commands.GetRequestContent, id: this._id}, callback && callbackWrapper);
}
};
/**
* @constructor
*/
function Panels() {
const panels = {
elements: new ElementsPanel(),
sources: new SourcesPanel(),
};
function panelGetter(name) {
return panels[name];
}
for (const panel in panels) {
Object.defineProperty(this, panel, {get: panelGetter.bind(null, panel), enumerable: true});
}
this.applyStyleSheet = function(styleSheet) {
extensionServer.sendRequest({command: commands.ApplyStyleSheet, styleSheet: styleSheet});
};
}
Panels.prototype = {
create: function(title, icon, page, callback) {
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) {
const hadHandler = extensionServer.hasHandler(events.OpenResource);
function callbackWrapper(message) {
// 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': !!callback});
}
},
openResource: function(url, lineNumber, callback) {
extensionServer.sendRequest({command: commands.OpenResource, 'url': url, 'lineNumber': lineNumber}, callback);
},
get SearchAction() {
return apiPrivate.panels.SearchAction;
}
};
/**
* @constructor
*/
function ExtensionViewImpl(id) {
this._id = id;
/**
* @this {EventSinkImpl}
*/
function dispatchShowEvent(message) {
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}
* @param {string} hostPanelName
*/
function PanelWithSidebarImpl(hostPanelName) {
ExtensionViewImpl.call(this, null);
this._hostPanelName = hostPanelName;
this.onSelectionChanged = new EventSink(events.PanelObjectSelected + hostPanelName);
}
PanelWithSidebarImpl.prototype = {
createSidebarPane: function(title, callback) {
const id = 'extension-sidebar-' + extensionServer.nextObjectId();
const request = {command: commands.CreateSidebarPane, panel: this._hostPanelName, id: id, title: title};
function callbackWrapper() {
callback(new ExtensionSidebarPane(id));
}
extensionServer.sendRequest(request, callback && callbackWrapper);
},
__proto__: ExtensionViewImpl.prototype
};
function declareInterfaceClass(implConstructor) {
return function() {
const impl = {__proto__: implConstructor.prototype};
implConstructor.apply(impl, arguments);
populateInterfaceClass(this, impl);
};
}
function defineDeprecatedProperty(object, className, oldName, newName) {
let warningGiven = false;
function getter() {
if (!warningGiven) {
console.warn(className + '.' + oldName + ' is deprecated. Use ' + className + '.' + newName + ' instead');
warningGiven = true;
}
return object[newName];
}
object.__defineGetter__(oldName, getter);
}
function extractCallbackArgument(args) {
const lastArgument = args[args.length - 1];
return typeof lastArgument === 'function' ? lastArgument : undefined;
}
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) {
ExtensionViewImpl.call(this, id);
this.onSearch = new EventSink(events.PanelSearch + id);
}
ExtensionPanelImpl.prototype = {
/**
* @return {!Object}
*/
createStatusBarButton: function(iconPath, tooltipText, disabled) {
const id = 'button-' + extensionServer.nextObjectId();
const request = {
command: commands.CreateToolbarButton,
panel: this._id,
id: id,
icon: iconPath,
tooltip: tooltipText,
disabled: !!disabled
};
extensionServer.sendRequest(request);
return new Button(id);
},
show: function() {
if (!userAction) {
return;
}
const request = {command: commands.ShowPanel, id: this._id};
extensionServer.sendRequest(request);
},
__proto__: ExtensionViewImpl.prototype
};
/**
* @constructor
* @extends {ExtensionViewImpl}
*/
function ExtensionSidebarPaneImpl(id) {
ExtensionViewImpl.call(this, id);
}
ExtensionSidebarPaneImpl.prototype = {
setHeight: function(height) {
extensionServer.sendRequest({command: commands.SetSidebarHeight, id: this._id, height: height});
},
setExpression: function(expression, rootTitle, evaluateOptions) {
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, rootTitle, callback) {
extensionServer.sendRequest(
{command: commands.SetSidebarContent, id: this._id, expression: jsonObject, rootTitle: rootTitle}, callback);
},
setPage: function(page) {
extensionServer.sendRequest({command: commands.SetSidebarPage, id: this._id, page: page});
},
__proto__: ExtensionViewImpl.prototype
};
/**
* @constructor
*/
function ButtonImpl(id) {
this._id = id;
this.onClicked = new EventSink(events.ButtonClicked + id);
}
ButtonImpl.prototype = {
update: function(iconPath, tooltipText, disabled) {
const request =
{command: commands.UpdateButton, id: this._id, icon: iconPath, tooltip: tooltipText, disabled: !!disabled};
extensionServer.sendRequest(request);
}
};
/**
* @constructor
*/
function Timeline() {
}
Timeline.prototype = {
/**
* @param {string} categoryName
* @param {string} categoryTooltip
* @return {!TraceProvider}
*/
addTraceProvider: function(categoryName, categoryTooltip) {
const id = 'extension-trace-provider-' + extensionServer.nextObjectId();
extensionServer.sendRequest(
{command: commands.AddTraceProvider, id: id, categoryName: categoryName, categoryTooltip: categoryTooltip});
return new TraceProvider(id);
}
};
/**
* @constructor
* @param {string} id
*/
function TraceSessionImpl(id) {
this._id = id;
}
TraceSessionImpl.prototype = {
/**
* @param {string=} url
* @param {number=} timeOffset
*/
complete: function(url, timeOffset) {
const request =
{command: commands.CompleteTraceSession, id: this._id, url: url || '', timeOffset: timeOffset || 0};
extensionServer.sendRequest(request);
}
};
/**
* @constructor
* @param {string} id
*/
function TraceProvider(id) {
/**
* @this {EventSinkImpl}
*/
function dispatchRecordingStarted(message) {
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() {
/**
* @this {EventSinkImpl}
*/
function dispatchResourceEvent(message) {
this._fire(new Resource(message.arguments[0]));
}
/**
* @this {EventSinkImpl}
*/
function dispatchResourceContentEvent(message) {
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) {
let options = 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});
},
/**
* @return {?Object}
*/
eval: function(expression, evaluateOptions) {
const callback = extractCallbackArgument(arguments);
function callbackWrapper(result) {
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) {
function wrapResource(resourceData) {
return new Resource(resourceData);
}
function callbackWrapper(resources) {
callback(resources.map(wrapResource));
}
extensionServer.sendRequest({command: commands.GetPageResources}, callback && callbackWrapper);
}
};
/**
* @constructor
*/
function ResourceImpl(resourceData) {
this._url = resourceData.url;
this._type = resourceData.type;
}
ResourceImpl.prototype = {
get url() {
return this._url;
},
get type() {
return this._type;
},
getContent: function(callback) {
function callbackWrapper(response) {
callback(response.content, response.encoding);
}
extensionServer.sendRequest({command: commands.GetResourceContent, url: this._url}, callback && callbackWrapper);
},
setContent: function(content, commit, callback) {
extensionServer.sendRequest(
{command: commands.SetResourceContent, url: this._url, content: content, commit: commit}, callback);
}
};
function getTabId() {
return inspectedTabId;
}
let keyboardEventRequestQueue = [];
let forwardTimer = null;
/**
* @suppressGlobalPropertiesCheck
*/
function forwardKeyboardEvent(event) {
// 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() {
forwardTimer = null;
const request = {command: commands.ForwardKeyboardEvent, entries: keyboardEventRequestQueue};
extensionServer.sendRequest(request);
keyboardEventRequestQueue = [];
}
document.addEventListener('keydown', forwardKeyboardEvent, false);
/**
* @constructor
*/
function ExtensionServerClient() {
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 = {
/**
* @param {!Object} message
* @param {function()=} callback
*/
sendRequest: function(message, callback) {
if (typeof callback === 'function') {
message.requestId = this._registerCallback(callback);
}
this._port.postMessage(message);
},
/**
* @return {boolean}
*/
hasHandler: function(command) {
return !!this._handlers[command];
},
registerHandler: function(command, handler) {
this._handlers[command] = handler;
},
unregisterHandler: function(command) {
delete this._handlers[command];
},
/**
* @return {string}
*/
nextObjectId: function() {
return injectedScriptId.toString() + '_' + ++this._lastObjectId;
},
_registerCallback: function(callback) {
const id = ++this._lastRequestId;
this._callbacks[id] = callback;
return id;
},
_onCallback: function(request) {
if (request.requestId in this._callbacks) {
const callback = this._callbacks[request.requestId];
delete this._callbacks[request.requestId];
callback(request.result);
}
},
_onMessage: function(event) {
const request = event.data;
const handler = this._handlers[request.command];
if (handler) {
handler.call(this, request);
}
}
};
function populateInterfaceClass(interfaze, implementation) {
for (const member in implementation) {
if (member.charAt(0) === '_') {
continue;
}
let descriptor = 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;
// 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);
};
/**
* @param {!ExtensionDescriptor} extensionInfo
* @param {string} inspectedTabId
* @param {string} themeName
* @param {!Array<number>} keysToForward
* @param {function(!Object, !Object)|undefined} testHook
* @return {string}
*/
self.buildExtensionAPIInjectedScript = function(extensionInfo, inspectedTabId, themeName, keysToForward, testHook) {
const argumentsJSON = [extensionInfo, inspectedTabId || null, themeName, keysToForward].map(_ => JSON.stringify(_)).join(',');
if (!testHook) {
testHook = () => {};
}
return '(function(injectedScriptId){ ' + defineCommonExtensionSymbols.toString() + ';' +
'(' + self.injectedExtensionAPI.toString() + ')(' + argumentsJSON + ',' + testHook + ', injectedScriptId);' +
'})';
};
/* Legacy exported object */
self.Extensions = self.Extensions || {};
/* Legacy exported object */
Extensions = Extensions || {};
Extensions.extensionAPI = {};
defineCommonExtensionSymbols(Extensions.extensionAPI);