breakpointPositions.js (12773B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 import { 6 getBreakpointPositionsForSource, 7 getSourceActorsForSource, 8 } from "../../selectors/index"; 9 10 import { makeBreakpointId } from "../../utils/breakpoint/index"; 11 import { memoizeableAction } from "../../utils/memoizableAction"; 12 import { fulfilled } from "../../utils/async-value"; 13 import { 14 sourceMapToDebuggerLocation, 15 createLocation, 16 } from "../../utils/location"; 17 import { validateSource } from "../../utils/context"; 18 19 /** 20 * Helper function which consumes breakpoints positions sent by the server 21 * and map them to location objects. 22 * During this process, the SourceMapLoader will be queried to map the positions from generated to original locations. 23 * 24 * @param {object} breakpointPositions 25 * The positions to map related to the generated source: 26 * { 27 * 1: [ 2, 6 ], // Line 1 is breakable on column 2 and 6 28 * 2: [ 2 ], // Line 2 is only breakable on column 2 29 * } 30 * @param {object} generatedSource 31 * @param {object} location 32 * The current location we are computing breakable positions. 33 * @param {object} thunk arguments 34 * @return {object} 35 * The mapped breakable locations in the original source: 36 * { 37 * 1: [ { source, line: 1, column: 2} , { source, line: 1, column 6 } ], // Line 1 is not mapped as location are same as breakpointPositions. 38 * 10: [ { source, line: 10, column: 28 } ], // Line 2 is mapped and locations and line key refers to the original source positions. 39 * } 40 */ 41 async function mapToLocations( 42 breakpointPositions, 43 generatedSource, 44 mappedLocation, 45 { getState, sourceMapLoader } 46 ) { 47 // Map breakable positions from generated to original locations. 48 let mappedBreakpointPositions = await sourceMapLoader.getOriginalLocations( 49 breakpointPositions, 50 generatedSource.id 51 ); 52 // The Source Map Loader will return null when there is no source map for that generated source. 53 // Consider the map as unrelated to source map and process the source actor positions as-is. 54 if (!mappedBreakpointPositions) { 55 mappedBreakpointPositions = breakpointPositions; 56 } 57 58 const positions = {}; 59 60 // Ensure that we have an entry for the line fetched 61 if (typeof mappedLocation.line === "number") { 62 positions[mappedLocation.line] = []; 63 } 64 65 const handledBreakpointIds = new Set(); 66 const isOriginal = mappedLocation.source.isOriginal; 67 const originalSourceId = mappedLocation.source.id; 68 69 for (let line in mappedBreakpointPositions) { 70 // createLocation expects a number and not a string. 71 line = parseInt(line, 10); 72 for (const columnOrSourceMapLocation of mappedBreakpointPositions[line]) { 73 let location, generatedLocation; 74 75 // When processing a source unrelated to source map, `mappedBreakpointPositions` will be equal to `breakpointPositions`. 76 // and columnOrSourceMapLocation will always be a number. 77 // But it will also be a number if we process a source mapped file and SourceMapLoader didn't find any valid mapping 78 // for the current position (line and column). 79 // When this happen to be a number it means it isn't mapped and columnOrSourceMapLocation refers to the column index. 80 if (typeof columnOrSourceMapLocation == "number") { 81 // If columnOrSourceMapLocation is a number, it means that this location doesn't mapped to an original source. 82 // So if we are currently computation positions for an original source, we can skip this breakable positions. 83 if (isOriginal) { 84 continue; 85 } 86 location = generatedLocation = createLocation({ 87 line, 88 column: columnOrSourceMapLocation, 89 source: generatedSource, 90 }); 91 } else { 92 // Otherwise, for sources which are mapped. `columnOrSourceMapLocation` will be a SourceMapLoader location object. 93 // This location object will refer to the location where the current column (columnOrSourceMapLocation.generatedColumn) 94 // mapped in the original file. 95 96 // When computing positions for an original source, ignore the location if that mapped to another original source. 97 if ( 98 isOriginal && 99 columnOrSourceMapLocation.sourceId != originalSourceId 100 ) { 101 continue; 102 } 103 104 location = sourceMapToDebuggerLocation( 105 getState(), 106 columnOrSourceMapLocation 107 ); 108 109 // Merge positions that refer to duplicated positions. 110 // Some sourcemaped positions might refer to the exact same source/line/column triple. 111 const breakpointId = makeBreakpointId(location); 112 if (handledBreakpointIds.has(breakpointId)) { 113 continue; 114 } 115 handledBreakpointIds.add(breakpointId); 116 117 generatedLocation = createLocation({ 118 line, 119 column: columnOrSourceMapLocation.generatedColumn, 120 source: generatedSource, 121 }); 122 } 123 124 // The positions stored in redux will be keyed by original source's line (if we are 125 // computing the original source positions), or the generated source line. 126 // Note that when we compute the bundle positions, location may refer to the original source, 127 // but we still want to use the generated location as key. 128 const keyLocation = isOriginal ? location : generatedLocation; 129 const keyLine = keyLocation.line; 130 if (!positions[keyLine]) { 131 positions[keyLine] = []; 132 } 133 positions[keyLine].push({ location, generatedLocation }); 134 } 135 } 136 137 return positions; 138 } 139 140 async function _setBreakpointPositions(location, thunkArgs) { 141 const { client, dispatch, getState, sourceMapLoader } = thunkArgs; 142 const results = {}; 143 let generatedSource = location.source; 144 if (location.source.isOriginal) { 145 const ranges = await sourceMapLoader.getGeneratedRangesForOriginal( 146 location.source.id, 147 true 148 ); 149 generatedSource = location.source.generatedSource; 150 151 // Note: While looping here may not look ideal, in the vast majority of 152 // cases, the number of ranges here should be very small, and is quite 153 // likely to only be a single range. 154 for (const range of ranges) { 155 // Wrap infinite end positions to the next line to keep things simple 156 // and because we know we don't care about the end-line whitespace 157 // in this case. 158 if (range.end.column === Infinity) { 159 range.end = { 160 line: range.end.line + 1, 161 column: 0, 162 }; 163 } 164 165 // Retrieve the positions for all the source actors for the related generated source. 166 // There might be many if it is loaded many times. 167 // We limit the retrieval of positions within the given range, so that we don't 168 // retrieve the whole bundle positions. 169 const allActorsPositions = await Promise.all( 170 getSourceActorsForSource(getState(), generatedSource.id).map(actor => 171 client.getSourceActorBreakpointPositions(actor, range) 172 ) 173 ); 174 175 // `allActorsPositions` looks like this: 176 // [ 177 // { // Positions for the first source actor 178 // 1: [ 2, 6 ], // Line 1 is breakable on column 2 and 6 179 // 2: [ 2 ], // Line 2 is only breakable on column 2 180 // }, 181 // {...} // Positions for another source actor 182 // ] 183 for (const actorPositions of allActorsPositions) { 184 for (const rangeLine in actorPositions) { 185 const columns = actorPositions[rangeLine]; 186 187 // Merge all actors's breakable columns and avoid duplication of columns reported as breakable 188 const existing = results[rangeLine]; 189 if (existing) { 190 for (const column of columns) { 191 if (!existing.includes(column)) { 192 existing.push(column); 193 } 194 } 195 } else { 196 results[rangeLine] = columns; 197 } 198 } 199 } 200 } 201 } else { 202 const { line } = location; 203 if (typeof line !== "number") { 204 throw new Error("Line is required for generated sources"); 205 } 206 207 // We only retrieve the positions for the given requested line, that, for each source actor. 208 // There might be many source actor, if it is loaded many times. 209 // Or if this is an html page, with many inline scripts. 210 const allActorsBreakableColumns = await Promise.all( 211 getSourceActorsForSource(getState(), location.source.id).map( 212 async actor => { 213 const positions = await client.getSourceActorBreakpointPositions( 214 actor, 215 { 216 // Only retrieve positions for the given line 217 start: { line, column: 0 }, 218 end: { line: line + 1, column: 0 }, 219 } 220 ); 221 return positions[line] || []; 222 } 223 ) 224 ); 225 226 for (const columns of allActorsBreakableColumns) { 227 // Merge all actors's breakable columns and avoid duplication of columns reported as breakable 228 const existing = results[line]; 229 if (existing) { 230 for (const column of columns) { 231 if (!existing.includes(column)) { 232 existing.push(column); 233 } 234 } 235 } else { 236 results[line] = columns; 237 } 238 } 239 } 240 241 const positions = await mapToLocations( 242 results, 243 generatedSource, 244 location, 245 thunkArgs 246 ); 247 // `mapToLocations` may compute for a little while asynchronously, 248 // ensure that the location is still valid before continuing. 249 validateSource(getState(), location.source); 250 251 dispatch({ 252 type: "ADD_BREAKPOINT_POSITIONS", 253 source: location.source, 254 positions, 255 }); 256 } 257 258 function generatedSourceActorKey(state, source) { 259 const generatedSource = source.isOriginal ? source.generatedSource : source; 260 const actors = generatedSource 261 ? getSourceActorsForSource(state, generatedSource.id).map( 262 ({ actor }) => actor 263 ) 264 : []; 265 return [source.id, ...actors].join(":"); 266 } 267 268 /** 269 * This method will force retrieving the breakable positions for a given source, on a given line. 270 * If this data has already been computed, it will returned the cached data. 271 * 272 * For original sources, this will query the SourceMap worker. 273 * For generated sources, this will query the DevTools server and the related source actors. 274 * 275 * @param Object options 276 * Dictionary object with many arguments: 277 * @param String options.sourceId 278 * The source we want to fetch breakable positions 279 * @param Number options.line 280 * The line we want to know which columns are breakable. 281 * (note that this seems to be optional for original sources) 282 * @return Array<Object> 283 * The list of all breakable positions, each object of this array will be like this: 284 * { 285 * line: Number 286 * column: Number 287 * source: Source object 288 * } 289 */ 290 export const setBreakpointPositions = memoizeableAction( 291 "setBreakpointPositions", 292 { 293 getValue: (location, { getState }) => { 294 const positions = getBreakpointPositionsForSource( 295 getState(), 296 location.source.id 297 ); 298 if (!positions) { 299 return null; 300 } 301 302 if ( 303 !location.source.isOriginal && 304 location.line && 305 !positions[location.line] 306 ) { 307 // We always return the full position dataset, but if a given line is 308 // not available, we treat the whole set as loading. 309 return null; 310 } 311 312 return fulfilled(positions); 313 }, 314 createKey(location, { getState }) { 315 const key = generatedSourceActorKey(getState(), location.source); 316 return !location.source.isOriginal && location.line 317 ? `${key}-${location.line}` 318 : key; 319 }, 320 action: async (location, thunkArgs) => 321 _setBreakpointPositions(location, thunkArgs), 322 } 323 ); 324 325 export function updateBreakpointPositionsForNewPrettyPrintedSource( 326 minifiedSource 327 ) { 328 return async ({ dispatch, getState }) => { 329 const oldPositions = getBreakpointPositionsForSource( 330 getState(), 331 minifiedSource.id 332 ); 333 if (!oldPositions) { 334 return; 335 } 336 337 // gather the lines for which we have breakpointPositions 338 const lines = [...Object.keys(oldPositions)].map(lineString => 339 Number(lineString) 340 ); 341 342 dispatch({ type: "CLEAR_BREAKPOINT_POSITIONS", source: minifiedSource }); 343 344 // recompute the breakpoint positions for all lines for which we had breakpointPositions before 345 await Promise.all( 346 lines.map(line => 347 dispatch(setBreakpointPositions({ source: minifiedSource, line })) 348 ) 349 ); 350 }; 351 }