loadSourceText.js (7375B)
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 const { 6 PROMISE, 7 } = require("resource://devtools/client/shared/redux/middleware/promise.js"); 8 import { 9 getSourceTextContentForSource, 10 getSettledSourceTextContent, 11 getSourcesEpoch, 12 getBreakpointsForSource, 13 getSourceActorsForSource, 14 getFirstSourceActorForGeneratedSource, 15 } from "../../selectors/index"; 16 import { addBreakpoint } from "../breakpoints/index"; 17 18 import { prettyPrintSourceTextContent } from "./prettyPrint"; 19 import { isFulfilled, fulfilled } from "../../utils/async-value"; 20 21 import { createLocation } from "../../utils/location"; 22 import { memoizeableAction } from "../../utils/memoizableAction"; 23 24 import { getEditor } from "../../utils/editor/index"; 25 26 async function loadGeneratedSource(sourceActor, { client }) { 27 // If no source actor can be found then the text for the 28 // source cannot be loaded. 29 if (!sourceActor) { 30 throw new Error("Source actor is null or not defined"); 31 } 32 33 let response; 34 try { 35 response = await client.sourceContents(sourceActor); 36 } catch (e) { 37 throw new Error(`sourceContents failed: ${e}`); 38 } 39 40 return { 41 text: response.source, 42 contentType: response.contentType || "text/javascript", 43 }; 44 } 45 46 async function loadOriginalSource( 47 source, 48 { getState, sourceMapLoader, prettyPrintWorker } 49 ) { 50 if (source.isPrettyPrinted) { 51 const { generatedSource } = source; 52 if (!generatedSource) { 53 throw new Error("Unable to find minified original."); 54 } 55 56 const content = getSettledSourceTextContent( 57 getState(), 58 createLocation({ 59 source: generatedSource, 60 }) 61 ); 62 63 return prettyPrintSourceTextContent( 64 sourceMapLoader, 65 prettyPrintWorker, 66 generatedSource, 67 content, 68 getSourceActorsForSource(getState(), generatedSource.id) 69 ); 70 } 71 72 const result = await sourceMapLoader.getOriginalSourceText(source.id); 73 if (!result) { 74 // The way we currently try to load and select a pending 75 // selected location, it is possible that we will try to fetch the 76 // original source text right after the source map has been cleared 77 // after a navigation event. 78 throw new Error("Original source text unavailable"); 79 } 80 return result; 81 } 82 83 async function loadGeneratedSourceTextPromise(sourceActor, thunkArgs) { 84 const { dispatch, getState } = thunkArgs; 85 const epoch = getSourcesEpoch(getState()); 86 87 await dispatch({ 88 type: "LOAD_GENERATED_SOURCE_TEXT", 89 sourceActor, 90 epoch, 91 [PROMISE]: loadGeneratedSource(sourceActor, thunkArgs), 92 }); 93 94 await onSourceTextContentAvailable( 95 sourceActor.sourceObject, 96 sourceActor, 97 thunkArgs 98 ); 99 } 100 101 async function loadOriginalSourceTextPromise(source, thunkArgs) { 102 const { dispatch, getState } = thunkArgs; 103 const epoch = getSourcesEpoch(getState()); 104 await dispatch({ 105 type: "LOAD_ORIGINAL_SOURCE_TEXT", 106 source, 107 epoch, 108 [PROMISE]: loadOriginalSource(source, thunkArgs), 109 }); 110 111 await onSourceTextContentAvailable(source, null, thunkArgs); 112 } 113 114 /** 115 * Function called everytime a new original or generated source gets its text content 116 * fetched from the server and registered in the reducer. 117 * 118 * @param {object} source 119 * @param {object} sourceActor (optional) 120 * If this is a generated source, we expect a precise source actor. 121 * @param {object} thunkArgs 122 */ 123 async function onSourceTextContentAvailable( 124 source, 125 sourceActor, 126 { dispatch, getState, parserWorker } 127 ) { 128 const location = createLocation({ 129 source, 130 sourceActor, 131 }); 132 const content = getSettledSourceTextContent(getState(), location); 133 if (!content) { 134 return; 135 } 136 137 const contentValue = isFulfilled(content) 138 ? content.value 139 : { type: "text", value: "", contentType: undefined }; 140 141 // Lezer parser uses the sources to map the original frame names 142 const editor = getEditor(); 143 if (!editor.isWasm) { 144 editor.addSource(source.id, contentValue.value); 145 // The babel parser needs the sources to parse scopes 146 parserWorker.setSource(source.id, contentValue); 147 } 148 149 // Update the text in any breakpoints for this source by re-adding them. 150 const breakpoints = getBreakpointsForSource(getState(), source); 151 for (const breakpoint of breakpoints) { 152 await dispatch( 153 addBreakpoint( 154 breakpoint.location, 155 breakpoint.options, 156 breakpoint.disabled 157 ) 158 ); 159 } 160 } 161 162 /** 163 * Loads the source text for the generated source based of the source actor 164 * 165 * @param {object} sourceActor 166 * There can be more than one source actor per source 167 * so the source actor needs to be specified. This is 168 * required for generated sources but will be null for 169 * original/pretty printed sources. 170 */ 171 export const loadGeneratedSourceText = memoizeableAction( 172 "loadGeneratedSourceText", 173 { 174 getValue: (sourceActor, { getState }) => { 175 if (!sourceActor) { 176 return null; 177 } 178 179 const sourceTextContent = getSourceTextContentForSource( 180 getState(), 181 sourceActor.sourceObject, 182 sourceActor 183 ); 184 185 if (!sourceTextContent || sourceTextContent.state === "pending") { 186 return sourceTextContent; 187 } 188 189 // This currently swallows source-load-failure since we return fulfilled 190 // here when content.state === "rejected". In an ideal world we should 191 // propagate that error upward. 192 return fulfilled(sourceTextContent); 193 }, 194 createKey: (sourceActor, { getState }) => { 195 const epoch = getSourcesEpoch(getState()); 196 return `${epoch}:${sourceActor.actor}`; 197 }, 198 action: (sourceActor, thunkArgs) => 199 loadGeneratedSourceTextPromise(sourceActor, thunkArgs), 200 } 201 ); 202 203 /** 204 * Loads the source text for an original source and source actor 205 * 206 * @param {object} source 207 * The original source to load the source text 208 */ 209 export const loadOriginalSourceText = memoizeableAction( 210 "loadOriginalSourceText", 211 { 212 getValue: (source, { getState }) => { 213 if (!source) { 214 return null; 215 } 216 217 const sourceTextContent = getSourceTextContentForSource( 218 getState(), 219 source 220 ); 221 if (!sourceTextContent || sourceTextContent.state === "pending") { 222 return sourceTextContent; 223 } 224 225 // This currently swallows source-load-failure since we return fulfilled 226 // here when content.state === "rejected". In an ideal world we should 227 // propagate that error upward. 228 return fulfilled(sourceTextContent); 229 }, 230 createKey: (source, { getState }) => { 231 const epoch = getSourcesEpoch(getState()); 232 return `${epoch}:${source.id}`; 233 }, 234 action: (source, thunkArgs) => 235 loadOriginalSourceTextPromise(source, thunkArgs), 236 } 237 ); 238 239 export function loadSourceText(source, sourceActor) { 240 return async ({ dispatch, getState }) => { 241 if (!source) { 242 return null; 243 } 244 if (source.isOriginal) { 245 return dispatch(loadOriginalSourceText(source)); 246 } 247 if (!sourceActor) { 248 sourceActor = getFirstSourceActorForGeneratedSource( 249 getState(), 250 source.id 251 ); 252 } 253 return dispatch(loadGeneratedSourceText(sourceActor)); 254 }; 255 }