Source code

Revision control

Copy as Markdown

Other Tools

/* 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/>. */
import { originalToGeneratedId } from "devtools/client/shared/source-map-loader/index";
import {
getSource,
getSourceFromId,
getBreakpointPositionsForSource,
getSourceActorsForSource,
} from "../../selectors/index";
import { makeBreakpointId } from "../../utils/breakpoint/index";
import { memoizeableAction } from "../../utils/memoizableAction";
import { fulfilled } from "../../utils/async-value";
import {
sourceMapToDebuggerLocation,
createLocation,
} from "../../utils/location";
import { validateSource } from "../../utils/context";
/**
* Helper function which consumes breakpoints positions sent by the server
* and map them to location objects.
* During this process, the SourceMapLoader will be queried to map the positions from generated to original locations.
*
* @param {Object} breakpointPositions
* The positions to map related to the generated source:
* {
* 1: [ 2, 6 ], // Line 1 is breakable on column 2 and 6
* 2: [ 2 ], // Line 2 is only breakable on column 2
* }
* @param {Object} generatedSource
* @param {Object} location
* The current location we are computing breakable positions.
* @param {Object} thunk arguments
* @return {Object}
* The mapped breakable locations in the original source:
* {
* 1: [ { source, line: 1, column: 2} , { source, line: 1, column 6 } ], // Line 1 is not mapped as location are same as breakpointPositions.
* 10: [ { source, line: 10, column: 28 } ], // Line 2 is mapped and locations and line key refers to the original source positions.
* }
*/
async function mapToLocations(
breakpointPositions,
generatedSource,
mappedLocation,
{ getState, sourceMapLoader }
) {
// Map breakable positions from generated to original locations.
let mappedBreakpointPositions = await sourceMapLoader.getOriginalLocations(
breakpointPositions,
generatedSource.id
);
// The Source Map Loader will return null when there is no source map for that generated source.
// Consider the map as unrelated to source map and process the source actor positions as-is.
if (!mappedBreakpointPositions) {
mappedBreakpointPositions = breakpointPositions;
}
const positions = {};
// Ensure that we have an entry for the line fetched
if (typeof mappedLocation.line === "number") {
positions[mappedLocation.line] = [];
}
const handledBreakpointIds = new Set();
const isOriginal = mappedLocation.source.isOriginal;
const originalSourceId = mappedLocation.source.id;
for (let line in mappedBreakpointPositions) {
// createLocation expects a number and not a string.
line = parseInt(line, 10);
for (const columnOrSourceMapLocation of mappedBreakpointPositions[line]) {
let location, generatedLocation;
// When processing a source unrelated to source map, `mappedBreakpointPositions` will be equal to `breakpointPositions`.
// and columnOrSourceMapLocation will always be a number.
// But it will also be a number if we process a source mapped file and SourceMapLoader didn't find any valid mapping
// for the current position (line and column).
// When this happen to be a number it means it isn't mapped and columnOrSourceMapLocation refers to the column index.
if (typeof columnOrSourceMapLocation == "number") {
// If columnOrSourceMapLocation is a number, it means that this location doesn't mapped to an original source.
// So if we are currently computation positions for an original source, we can skip this breakable positions.
if (isOriginal) {
continue;
}
location = generatedLocation = createLocation({
line,
column: columnOrSourceMapLocation,
source: generatedSource,
});
} else {
// Otherwise, for sources which are mapped. `columnOrSourceMapLocation` will be a SourceMapLoader location object.
// This location object will refer to the location where the current column (columnOrSourceMapLocation.generatedColumn)
// mapped in the original file.
// When computing positions for an original source, ignore the location if that mapped to another original source.
if (
isOriginal &&
columnOrSourceMapLocation.sourceId != originalSourceId
) {
continue;
}
location = sourceMapToDebuggerLocation(
getState(),
columnOrSourceMapLocation
);
// Merge positions that refer to duplicated positions.
// Some sourcemaped positions might refer to the exact same source/line/column triple.
const breakpointId = makeBreakpointId(location);
if (handledBreakpointIds.has(breakpointId)) {
continue;
}
handledBreakpointIds.add(breakpointId);
generatedLocation = createLocation({
line,
column: columnOrSourceMapLocation.generatedColumn,
source: generatedSource,
});
}
// The positions stored in redux will be keyed by original source's line (if we are
// computing the original source positions), or the generated source line.
// Note that when we compute the bundle positions, location may refer to the original source,
// but we still want to use the generated location as key.
const keyLocation = isOriginal ? location : generatedLocation;
const keyLine = keyLocation.line;
if (!positions[keyLine]) {
positions[keyLine] = [];
}
positions[keyLine].push({ location, generatedLocation });
}
}
return positions;
}
async function _setBreakpointPositions(location, thunkArgs) {
const { client, dispatch, getState, sourceMapLoader } = thunkArgs;
const results = {};
let generatedSource = location.source;
if (location.source.isOriginal) {
const ranges = await sourceMapLoader.getGeneratedRangesForOriginal(
location.source.id,
true
);
const generatedSourceId = originalToGeneratedId(location.source.id);
generatedSource = getSourceFromId(getState(), generatedSourceId);
// Note: While looping here may not look ideal, in the vast majority of
// cases, the number of ranges here should be very small, and is quite
// likely to only be a single range.
for (const range of ranges) {
// Wrap infinite end positions to the next line to keep things simple
// and because we know we don't care about the end-line whitespace
// in this case.
if (range.end.column === Infinity) {
range.end = {
line: range.end.line + 1,
column: 0,
};
}
// Retrieve the positions for all the source actors for the related generated source.
// There might be many if it is loaded many times.
// We limit the retrieval of positions within the given range, so that we don't
// retrieve the whole bundle positions.
const allActorsPositions = await Promise.all(
getSourceActorsForSource(getState(), generatedSourceId).map(actor =>
client.getSourceActorBreakpointPositions(actor, range)
)
);
// `allActorsPositions` looks like this:
// [
// { // Positions for the first source actor
// 1: [ 2, 6 ], // Line 1 is breakable on column 2 and 6
// 2: [ 2 ], // Line 2 is only breakable on column 2
// },
// {...} // Positions for another source actor
// ]
for (const actorPositions of allActorsPositions) {
for (const rangeLine in actorPositions) {
const columns = actorPositions[rangeLine];
// Merge all actors's breakable columns and avoid duplication of columns reported as breakable
const existing = results[rangeLine];
if (existing) {
for (const column of columns) {
if (!existing.includes(column)) {
existing.push(column);
}
}
} else {
results[rangeLine] = columns;
}
}
}
}
} else {
const { line } = location;
if (typeof line !== "number") {
throw new Error("Line is required for generated sources");
}
// We only retrieve the positions for the given requested line, that, for each source actor.
// There might be many source actor, if it is loaded many times.
// Or if this is an html page, with many inline scripts.
const allActorsBreakableColumns = await Promise.all(
getSourceActorsForSource(getState(), location.source.id).map(
async actor => {
const positions = await client.getSourceActorBreakpointPositions(
actor,
{
// Only retrieve positions for the given line
start: { line, column: 0 },
end: { line: line + 1, column: 0 },
}
);
return positions[line] || [];
}
)
);
for (const columns of allActorsBreakableColumns) {
// Merge all actors's breakable columns and avoid duplication of columns reported as breakable
const existing = results[line];
if (existing) {
for (const column of columns) {
if (!existing.includes(column)) {
existing.push(column);
}
}
} else {
results[line] = columns;
}
}
}
const positions = await mapToLocations(
results,
generatedSource,
location,
thunkArgs
);
// `mapToLocations` may compute for a little while asynchronously,
// ensure that the location is still valid before continuing.
validateSource(getState(), location.source);
dispatch({
type: "ADD_BREAKPOINT_POSITIONS",
source: location.source,
positions,
});
}
function generatedSourceActorKey(state, source) {
const generatedSource = getSource(
state,
source.isOriginal ? originalToGeneratedId(source.id) : source.id
);
const actors = generatedSource
? getSourceActorsForSource(state, generatedSource.id).map(
({ actor }) => actor
)
: [];
return [source.id, ...actors].join(":");
}
/**
* This method will force retrieving the breakable positions for a given source, on a given line.
* If this data has already been computed, it will returned the cached data.
*
* For original sources, this will query the SourceMap worker.
* For generated sources, this will query the DevTools server and the related source actors.
*
* @param Object options
* Dictionary object with many arguments:
* @param String options.sourceId
* The source we want to fetch breakable positions
* @param Number options.line
* The line we want to know which columns are breakable.
* (note that this seems to be optional for original sources)
* @return Array<Object>
* The list of all breakable positions, each object of this array will be like this:
* {
* line: Number
* column: Number
* source: Source object
* }
*/
export const setBreakpointPositions = memoizeableAction(
"setBreakpointPositions",
{
getValue: (location, { getState }) => {
const positions = getBreakpointPositionsForSource(
getState(),
location.source.id
);
if (!positions) {
return null;
}
if (
!location.source.isOriginal &&
location.line &&
!positions[location.line]
) {
// We always return the full position dataset, but if a given line is
// not available, we treat the whole set as loading.
return null;
}
return fulfilled(positions);
},
createKey(location, { getState }) {
const key = generatedSourceActorKey(getState(), location.source);
return !location.source.isOriginal && location.line
? `${key}-${location.line}`
: key;
},
action: async (location, thunkArgs) =>
_setBreakpointPositions(location, thunkArgs),
}
);