зеркало из https://github.com/mozilla/gecko-dev.git
476 строки
14 KiB
JavaScript
476 строки
14 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
"use strict";
|
|
|
|
const {
|
|
createElement,
|
|
createFactory,
|
|
} = require("resource://devtools/client/shared/vendor/react.js");
|
|
const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.js");
|
|
const {
|
|
Provider,
|
|
createProvider,
|
|
} = require("resource://devtools/client/shared/vendor/react-redux.js");
|
|
|
|
const actions = require("resource://devtools/client/webconsole/actions/index.js");
|
|
const {
|
|
configureStore,
|
|
} = require("resource://devtools/client/webconsole/store.js");
|
|
|
|
const {
|
|
isPacketPrivate,
|
|
} = require("resource://devtools/client/webconsole/utils/messages.js");
|
|
const {
|
|
getMutableMessagesById,
|
|
getMessage,
|
|
getAllNetworkMessagesUpdateById,
|
|
} = require("resource://devtools/client/webconsole/selectors/messages.js");
|
|
|
|
const EventEmitter = require("resource://devtools/shared/event-emitter.js");
|
|
const App = createFactory(
|
|
require("resource://devtools/client/webconsole/components/App.js")
|
|
);
|
|
|
|
loader.lazyGetter(this, "AppErrorBoundary", () =>
|
|
createFactory(
|
|
require("resource://devtools/client/shared/components/AppErrorBoundary.js")
|
|
)
|
|
);
|
|
|
|
const {
|
|
setupServiceContainer,
|
|
} = require("resource://devtools/client/webconsole/service-container.js");
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"Constants",
|
|
"resource://devtools/client/webconsole/constants.js"
|
|
);
|
|
|
|
// Localized strings for (devtools/client/locales/en-US/startup.properties)
|
|
loader.lazyGetter(this, "L10N", function () {
|
|
const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
|
|
return new LocalizationHelper("devtools/client/locales/startup.properties");
|
|
});
|
|
|
|
// Only Browser Console needs Fluent bundles at the moment
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"FluentL10n",
|
|
"resource://devtools/client/shared/fluent-l10n/fluent-l10n.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"LocalizationProvider",
|
|
"resource://devtools/client/shared/vendor/fluent-react.js",
|
|
true
|
|
);
|
|
|
|
let store = null;
|
|
|
|
class WebConsoleWrapper {
|
|
/**
|
|
*
|
|
* @param {HTMLElement} parentNode
|
|
* @param {WebConsoleUI} webConsoleUI
|
|
* @param {Toolbox} toolbox
|
|
* @param {Document} document
|
|
*
|
|
*/
|
|
constructor(parentNode, webConsoleUI, toolbox, document) {
|
|
EventEmitter.decorate(this);
|
|
|
|
this.parentNode = parentNode;
|
|
this.webConsoleUI = webConsoleUI;
|
|
this.toolbox = toolbox;
|
|
this.hud = this.webConsoleUI.hud;
|
|
this.document = document;
|
|
|
|
this.init = this.init.bind(this);
|
|
|
|
this.queuedMessageAdds = [];
|
|
this.queuedMessageUpdates = [];
|
|
this.queuedRequestUpdates = [];
|
|
this.throttledDispatchPromise = null;
|
|
|
|
this.telemetry = this.hud.telemetry;
|
|
}
|
|
|
|
#serviceContainer;
|
|
|
|
async init() {
|
|
const { webConsoleUI } = this;
|
|
|
|
let fluentBundles;
|
|
if (webConsoleUI.isBrowserConsole) {
|
|
const fluentL10n = new FluentL10n();
|
|
await fluentL10n.init(["devtools/client/toolbox.ftl"]);
|
|
fluentBundles = fluentL10n.getBundles();
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
store = configureStore(this.webConsoleUI, {
|
|
// We may not have access to the toolbox (e.g. in the browser console).
|
|
telemetry: this.telemetry,
|
|
thunkArgs: {
|
|
webConsoleUI,
|
|
hud: this.hud,
|
|
toolbox: this.toolbox,
|
|
commands: this.hud.commands,
|
|
},
|
|
});
|
|
|
|
const app = AppErrorBoundary(
|
|
{
|
|
componentName: "Console",
|
|
panel: L10N.getStr("ToolboxTabWebconsole.label"),
|
|
},
|
|
App({
|
|
serviceContainer: this.getServiceContainer(),
|
|
webConsoleUI,
|
|
onFirstMeaningfulPaint: resolve,
|
|
closeSplitConsole: this.closeSplitConsole.bind(this),
|
|
inputEnabled:
|
|
!webConsoleUI.isBrowserConsole ||
|
|
Services.prefs.getBoolPref("devtools.chrome.enabled"),
|
|
})
|
|
);
|
|
|
|
// Render the root Application component.
|
|
if (this.parentNode) {
|
|
const maybeLocalizedElement = fluentBundles
|
|
? createElement(LocalizationProvider, { bundles: fluentBundles }, app)
|
|
: app;
|
|
|
|
this.body = ReactDOM.render(
|
|
createElement(
|
|
Provider,
|
|
{ store },
|
|
createElement(
|
|
createProvider(this.hud.commands.targetCommand.storeId),
|
|
{ store: this.hud.commands.targetCommand.store },
|
|
maybeLocalizedElement
|
|
)
|
|
),
|
|
this.parentNode
|
|
);
|
|
} else {
|
|
// If there's no parentNode, we are in a test. So we can resolve immediately.
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
destroy() {
|
|
// This component can be instantiated from jest test, in which case we don't have
|
|
// a parentNode reference.
|
|
if (this.parentNode) {
|
|
ReactDOM.unmountComponentAtNode(this.parentNode);
|
|
}
|
|
}
|
|
|
|
dispatchMessageAdd(packet) {
|
|
this.batchedMessagesAdd([packet]);
|
|
}
|
|
|
|
dispatchMessagesAdd(messages) {
|
|
this.batchedMessagesAdd(messages);
|
|
}
|
|
|
|
dispatchNetworkMessagesDisable() {
|
|
const networkMessageIds = Object.keys(
|
|
getAllNetworkMessagesUpdateById(store.getState())
|
|
);
|
|
store.dispatch(actions.messagesDisable(networkMessageIds));
|
|
}
|
|
|
|
dispatchMessagesClear() {
|
|
// We might still have pending message additions and updates when the clear action is
|
|
// triggered, so we need to flush them to make sure we don't have unexpected behavior
|
|
// in the ConsoleOutput. *But* we want to keep any pending navigation request,
|
|
// as we want to keep displaying them even if we received a clear request.
|
|
function filter(l) {
|
|
return l.filter(update => update.isNavigationRequest);
|
|
}
|
|
this.queuedMessageAdds = filter(this.queuedMessageAdds);
|
|
this.queuedMessageUpdates = filter(this.queuedMessageUpdates);
|
|
this.queuedRequestUpdates = this.queuedRequestUpdates.filter(
|
|
update => update.data.isNavigationRequest
|
|
);
|
|
|
|
store?.dispatch(actions.messagesClear());
|
|
this.webConsoleUI.emitForTests("messages-cleared");
|
|
}
|
|
|
|
dispatchPrivateMessagesClear() {
|
|
// We might still have pending private message additions when the private messages
|
|
// clear action is triggered. We need to remove any private-window-issued packets from
|
|
// the queue so they won't appear in the output.
|
|
|
|
// For (network) message updates, we need to check both messages queue and the state
|
|
// since we can receive updates even if the message isn't rendered yet.
|
|
const messages = [...getMutableMessagesById(store.getState()).values()];
|
|
this.queuedMessageUpdates = this.queuedMessageUpdates.filter(
|
|
({ actor }) => {
|
|
const queuedNetworkMessage = this.queuedMessageAdds.find(
|
|
p => p.actor === actor
|
|
);
|
|
if (queuedNetworkMessage && isPacketPrivate(queuedNetworkMessage)) {
|
|
return false;
|
|
}
|
|
|
|
const requestMessage = messages.find(
|
|
message => actor === message.actor
|
|
);
|
|
if (requestMessage && requestMessage.private === true) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
);
|
|
|
|
// For (network) requests updates, we can check only the state, since there must be a
|
|
// user interaction to get an update (i.e. the network message is displayed and thus
|
|
// in the state).
|
|
this.queuedRequestUpdates = this.queuedRequestUpdates.filter(({ id }) => {
|
|
const requestMessage = getMessage(store.getState(), id);
|
|
if (requestMessage && requestMessage.private === true) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
// Finally we clear the messages queue. This needs to be done here since we use it to
|
|
// clean the other queues.
|
|
this.queuedMessageAdds = this.queuedMessageAdds.filter(
|
|
p => !isPacketPrivate(p)
|
|
);
|
|
|
|
store.dispatch(actions.privateMessagesClear());
|
|
}
|
|
|
|
dispatchTargetMessagesRemove(targetFront) {
|
|
// We might still have pending packets in the queues from the target that we need to remove
|
|
// to prevent messages appearing in the output.
|
|
|
|
for (let i = this.queuedMessageUpdates.length - 1; i >= 0; i--) {
|
|
const packet = this.queuedMessageUpdates[i];
|
|
if (packet.targetFront == targetFront) {
|
|
this.queuedMessageUpdates.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
for (let i = this.queuedRequestUpdates.length - 1; i >= 0; i--) {
|
|
const packet = this.queuedRequestUpdates[i];
|
|
if (packet.data.targetFront == targetFront) {
|
|
this.queuedRequestUpdates.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
for (let i = this.queuedMessageAdds.length - 1; i >= 0; i--) {
|
|
const packet = this.queuedMessageAdds[i];
|
|
// Keep in sync with the check done in the reducer for the TARGET_MESSAGES_REMOVE action.
|
|
if (
|
|
packet.targetFront == targetFront &&
|
|
packet.type !== Constants.MESSAGE_TYPE.COMMAND &&
|
|
packet.type !== Constants.MESSAGE_TYPE.RESULT
|
|
) {
|
|
this.queuedMessageAdds.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
store.dispatch(actions.targetMessagesRemove(targetFront));
|
|
}
|
|
|
|
dispatchMessagesUpdate(messages) {
|
|
this.batchedMessagesUpdates(messages);
|
|
}
|
|
|
|
dispatchSidebarClose() {
|
|
store.dispatch(actions.sidebarClose());
|
|
}
|
|
|
|
dispatchSplitConsoleCloseButtonToggle() {
|
|
store.dispatch(
|
|
actions.splitConsoleCloseButtonToggle(
|
|
this.toolbox && this.toolbox.currentToolId !== "webconsole"
|
|
)
|
|
);
|
|
}
|
|
|
|
dispatchTabWillNavigate(packet) {
|
|
const { ui } = store.getState();
|
|
|
|
// For the browser console, we receive tab navigation
|
|
// when the original top level window we attached to is closed,
|
|
// but we don't want to reset console history and just switch to
|
|
// the next available window.
|
|
if (ui.persistLogs || this.webConsoleUI.isBrowserConsole) {
|
|
// Add a type in order for this event packet to be identified by
|
|
// utils/messages.js's `transformPacket`
|
|
packet.type = "will-navigate";
|
|
this.dispatchMessageAdd(packet);
|
|
} else {
|
|
this.dispatchMessagesClear();
|
|
store.dispatch({
|
|
type: Constants.WILL_NAVIGATE,
|
|
});
|
|
}
|
|
}
|
|
|
|
batchedMessagesUpdates(messages) {
|
|
if (messages.length) {
|
|
this.queuedMessageUpdates.push(...messages);
|
|
this.setTimeoutIfNeeded();
|
|
}
|
|
}
|
|
|
|
batchedRequestUpdates(message) {
|
|
this.queuedRequestUpdates.push(message);
|
|
return this.setTimeoutIfNeeded();
|
|
}
|
|
|
|
batchedMessagesAdd(messages) {
|
|
if (messages.length) {
|
|
this.queuedMessageAdds.push(...messages);
|
|
this.setTimeoutIfNeeded();
|
|
}
|
|
}
|
|
|
|
dispatchClearHistory() {
|
|
store.dispatch(actions.clearHistory());
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {String} expression: The expression to evaluate
|
|
*/
|
|
dispatchEvaluateExpression(expression) {
|
|
store.dispatch(actions.evaluateExpression(expression));
|
|
}
|
|
|
|
dispatchUpdateInstantEvaluationResultForCurrentExpression() {
|
|
store.dispatch(actions.updateInstantEvaluationResultForCurrentExpression());
|
|
}
|
|
|
|
/**
|
|
* Returns a Promise that resolves once any async dispatch is finally dispatched.
|
|
*/
|
|
waitAsyncDispatches() {
|
|
if (!this.throttledDispatchPromise) {
|
|
return Promise.resolve();
|
|
}
|
|
// When closing the console during initialization,
|
|
// setTimeoutIfNeeded may never resolve its promise
|
|
// as window.setTimeout will be disabled on document destruction.
|
|
const onUnload = new Promise(r =>
|
|
window.addEventListener("unload", r, { once: true })
|
|
);
|
|
return Promise.race([this.throttledDispatchPromise, onUnload]);
|
|
}
|
|
|
|
setTimeoutIfNeeded() {
|
|
if (this.throttledDispatchPromise) {
|
|
return this.throttledDispatchPromise;
|
|
}
|
|
this.throttledDispatchPromise = new Promise(done => {
|
|
setTimeout(async () => {
|
|
this.throttledDispatchPromise = null;
|
|
|
|
if (!store) {
|
|
// The store is not initialized yet, we can call setTimeoutIfNeeded so the
|
|
// messages will be handled in the next timeout when the store is ready.
|
|
this.setTimeoutIfNeeded();
|
|
done();
|
|
return;
|
|
}
|
|
|
|
store.dispatch(actions.messagesAdd(this.queuedMessageAdds));
|
|
|
|
const { length } = this.queuedMessageAdds;
|
|
|
|
// This telemetry event is only useful when we have a toolbox so only
|
|
// send it when we have one.
|
|
if (this.toolbox) {
|
|
this.telemetry.addEventProperty(
|
|
this.toolbox,
|
|
"enter",
|
|
"webconsole",
|
|
null,
|
|
"message_count",
|
|
length
|
|
);
|
|
}
|
|
|
|
this.queuedMessageAdds = [];
|
|
|
|
if (this.queuedMessageUpdates.length) {
|
|
await store.dispatch(
|
|
actions.networkMessageUpdates(this.queuedMessageUpdates, null)
|
|
);
|
|
this.webConsoleUI.emitForTests("network-messages-updated");
|
|
this.queuedMessageUpdates = [];
|
|
}
|
|
if (this.queuedRequestUpdates.length) {
|
|
await store.dispatch(
|
|
actions.networkUpdateRequests(this.queuedRequestUpdates)
|
|
);
|
|
const updateCount = this.queuedRequestUpdates.length;
|
|
this.queuedRequestUpdates = [];
|
|
|
|
// Fire an event indicating that all data fetched from
|
|
// the backend has been received. This is based on
|
|
// 'FirefoxDataProvider.isQueuePayloadReady', see more
|
|
// comments in that method.
|
|
// (netmonitor/src/connector/firefox-data-provider).
|
|
// This event might be utilized in tests to find the right
|
|
// time when to finish.
|
|
|
|
this.webConsoleUI.emitForTests(
|
|
"network-request-payload-ready",
|
|
updateCount
|
|
);
|
|
}
|
|
done();
|
|
}, 50);
|
|
});
|
|
return this.throttledDispatchPromise;
|
|
}
|
|
|
|
getStore() {
|
|
return store;
|
|
}
|
|
|
|
getServiceContainer() {
|
|
if (!this.#serviceContainer) {
|
|
this.#serviceContainer = setupServiceContainer({
|
|
webConsoleUI: this.webConsoleUI,
|
|
toolbox: this.toolbox,
|
|
hud: this.hud,
|
|
webConsoleWrapper: this,
|
|
});
|
|
}
|
|
return this.#serviceContainer;
|
|
}
|
|
|
|
subscribeToStore(callback) {
|
|
store.subscribe(() => callback(store.getState()));
|
|
}
|
|
|
|
createElement(nodename) {
|
|
return this.document.createElement(nodename);
|
|
}
|
|
|
|
// Called by pushing close button.
|
|
closeSplitConsole() {
|
|
this.toolbox.closeSplitConsole();
|
|
}
|
|
}
|
|
|
|
// Exports from this module
|
|
module.exports = WebConsoleWrapper;
|