/* 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 . */ /** * Redux actions for the sources state * @module actions/sources */ import { insertSourceActors } from "../../actions/source-actors"; import { makeSourceId, createGeneratedSource, createSourceMapOriginalSource, createSourceActor, } from "../../client/firefox/create"; import { toggleBlackBox } from "./blackbox"; import { syncPendingBreakpoint } from "../breakpoints/index"; import { loadSourceText } from "./loadSourceText"; import { prettyPrintAndSelectSource } from "./prettyPrint"; import { toggleSourceMapIgnoreList } from "../ui"; import { selectLocation, setBreakableLines } from "../sources/index"; import { getRawSourceURL, isPrettyURL } from "../../utils/source"; import { createLocation } from "../../utils/location"; import { getBlackBoxRanges, getSource, getSourceFromId, hasSourceActor, getSourceByActorId, getPendingSelectedLocation, getPendingBreakpointsForSource, getSelectedLocation, } from "../../selectors/index"; import { prefs } from "../../utils/prefs"; import sourceQueue from "../../utils/source-queue"; import { validateSourceActor, ContextError } from "../../utils/context"; function loadSourceMapsForSourceActors(sourceActors) { return async function ({ dispatch }) { try { await Promise.all( sourceActors.map(sourceActor => dispatch(loadSourceMap(sourceActor))) ); } catch (error) { // This may throw a context error if we navigated while processing the source maps if (!(error instanceof ContextError)) { throw error; } } // Once all the source maps, of all the bulk of new source actors are processed, // flush the SourceQueue. This help aggregate all the original sources in one action. await sourceQueue.flush(); }; } /** * @memberof actions/sources * @static */ function loadSourceMap(sourceActor) { return async function ({ dispatch, getState, sourceMapLoader, panel }) { if (!prefs.clientSourceMapsEnabled || !sourceActor.sourceMapURL) { return; } let sources, ignoreListUrls, resolvedSourceMapURL, exception; try { // Ignore sourceMapURL on scripts that are part of HTML files, since // we currently treat sourcemaps as Source-wide, not SourceActor-specific. const source = getSourceByActorId(getState(), sourceActor.id); if (source) { ({ sources, ignoreListUrls, resolvedSourceMapURL, exception } = await sourceMapLoader.loadSourceMap({ // Using source ID here is historical and eventually we'll want to // switch to all of this being per-source-actor. id: source.id, url: sourceActor.url || "", sourceMapBaseURL: sourceActor.sourceMapBaseURL || "", sourceMapURL: sourceActor.sourceMapURL || "", isWasm: sourceActor.introductionType === "wasm", })); } } catch (e) { exception = `Internal error: ${e.message}`; } if (resolvedSourceMapURL) { dispatch({ type: "RESOLVED_SOURCEMAP_URL", sourceActor, resolvedSourceMapURL, }); } if (ignoreListUrls?.length) { dispatch({ type: "ADD_SOURCEMAP_IGNORE_LIST_SOURCES", ignoreListUrls, }); } if (exception) { // Catch all errors and log them to the Web Console for users to see. const message = L10N.getFormatStr( "toolbox.sourceMapFailure", exception, sourceActor.url, sourceActor.sourceMapURL ); panel.toolbox.commands.targetCommand.targetFront.logWarningInPage( message, "source map", resolvedSourceMapURL ); dispatch({ type: "SOURCE_MAP_ERROR", sourceActor, errorMessage: exception, }); // If this source doesn't have a sourcemap or there are no original files // existing, enable it for pretty printing dispatch({ type: "CLEAR_SOURCE_ACTOR_MAP_URL", sourceActor, }); return; } // Before dispatching this action, ensure that the related sourceActor is still registered validateSourceActor(getState(), sourceActor); for (const originalSource of sources) { // The Source Map worker doesn't set the `sourceActor` attribute, // which is handy to know what is the related bundle. originalSource.sourceActor = sourceActor; } // Register all the new reported original sources in the queue to be flushed once all new bundles are processed. sourceQueue.queueOriginalSources(sources); }; } // If a request has been made to show this source, go ahead and // select it. function checkSelectedSource(sourceId) { return async ({ dispatch, getState }) => { const state = getState(); const pendingLocation = getPendingSelectedLocation(state); if (!pendingLocation || !pendingLocation.url) { return; } const source = getSource(state, sourceId); if (!source || !source.url) { return; } const pendingUrl = pendingLocation.url; const rawPendingUrl = getRawSourceURL(pendingUrl); if (rawPendingUrl === source.url) { if (isPrettyURL(pendingUrl)) { const prettySource = await dispatch(prettyPrintAndSelectSource(source)); dispatch(checkPendingBreakpoints(prettySource, null)); return; } await dispatch( selectLocation( createLocation({ source, line: typeof pendingLocation.line === "number" ? pendingLocation.line : 0, column: pendingLocation.column, }) ) ); } }; } function checkPendingBreakpoints(source, sourceActor) { return async ({ dispatch, getState }) => { const pendingBreakpoints = getPendingBreakpointsForSource( getState(), source ); if (pendingBreakpoints.length === 0) { return; } // load the source text if there is a pending breakpoint for it await dispatch(loadSourceText(source, sourceActor)); await dispatch(setBreakableLines(createLocation({ source, sourceActor }))); await Promise.all( pendingBreakpoints.map(pendingBp => { return dispatch(syncPendingBreakpoint(source, pendingBp)); }) ); }; } function restoreBlackBoxedSources(sources) { return async ({ dispatch, getState }) => { const currentRanges = getBlackBoxRanges(getState()); if (!Object.keys(currentRanges).length) { return; } for (const source of sources) { const ranges = currentRanges[source.url]; if (ranges) { // If the ranges is an empty then the whole source was blackboxed. await dispatch(toggleBlackBox(source, true, ranges)); } } if (prefs.sourceMapIgnoreListEnabled) { await dispatch(toggleSourceMapIgnoreList(true)); } }; } export function newOriginalSources(originalSourcesInfo) { return async ({ dispatch, getState }) => { const state = getState(); const seen = new Set(); const actors = []; const actorsSources = {}; for (const { id, url, sourceActor } of originalSourcesInfo) { if (seen.has(id) || getSource(state, id)) { continue; } seen.add(id); if (!actorsSources[sourceActor.actor]) { actors.push(sourceActor); actorsSources[sourceActor.actor] = []; } actorsSources[sourceActor.actor].push( createSourceMapOriginalSource(id, url, sourceActor.sourceObject) ); } // Add the original sources per the generated source actors that // they are primarily from. actors.forEach(sourceActor => { dispatch({ type: "ADD_ORIGINAL_SOURCES", originalSources: actorsSources[sourceActor.actor], generatedSourceActor: sourceActor, }); }); // Accumulate the sources back into one list const actorsSourcesValues = Object.values(actorsSources); let sources = []; if (actorsSourcesValues.length) { sources = actorsSourcesValues.reduce((acc, sourceList) => acc.concat(sourceList) ); } await dispatch(checkNewSources(sources)); for (const source of sources) { dispatch(checkPendingBreakpoints(source, null)); } return sources; }; } // Wrapper around newGeneratedSources, only used by tests export function newGeneratedSource(sourceInfo) { return async ({ dispatch }) => { const sources = await dispatch(newGeneratedSources([sourceInfo])); return sources[0]; }; } export function newGeneratedSources(sourceResources) { return async ({ dispatch, getState }) => { if (!sourceResources.length) { return []; } const resultIds = []; const newSourcesObj = {}; const newSourceActors = []; for (const sourceResource of sourceResources) { // By the time we process the sources, the related target // might already have been destroyed. It means that the sources // are also about to be destroyed, so ignore them. // (This is covered by browser_toolbox_backward_forward_navigation.js) if (sourceResource.targetFront.isDestroyed()) { continue; } const id = makeSourceId(sourceResource); if (!getSource(getState(), id) && !newSourcesObj[id]) { newSourcesObj[id] = createGeneratedSource(sourceResource); } const actorId = sourceResource.actor; // We are sometimes notified about a new source multiple times if we // request a new source list and also get a source event from the server. if (!hasSourceActor(getState(), actorId)) { newSourceActors.push( createSourceActor( sourceResource, getSource(getState(), id) || newSourcesObj[id] ) ); } resultIds.push(id); } const newSources = Object.values(newSourcesObj); dispatch({ type: "ADD_SOURCES", sources: newSources }); dispatch(insertSourceActors(newSourceActors)); await dispatch(checkNewSources(newSources)); (async () => { await dispatch(loadSourceMapsForSourceActors(newSourceActors)); // We have to force fetching the breakable lines for any incoming source actor // related to HTML page as we may have the HTML page selected, // and already fetched its breakable lines and won't try to update // the breakable lines for any late coming inline