Frame.js (12398B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 Component, 9 } = require("resource://devtools/client/shared/vendor/react.mjs"); 10 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 12 const { 13 getUnicodeUrl, 14 getUnicodeUrlPath, 15 getUnicodeHostname, 16 } = require("resource://devtools/client/shared/unicode-url.js"); 17 const { 18 getSourceNames, 19 parseURL, 20 getSourceMappedFile, 21 } = require("resource://devtools/client/shared/source-utils.js"); 22 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); 23 const { 24 MESSAGE_SOURCE, 25 } = require("resource://devtools/client/webconsole/constants.js"); 26 27 const l10n = new LocalizationHelper( 28 "devtools/client/locales/components.properties" 29 ); 30 const webl10n = new LocalizationHelper( 31 "devtools/client/locales/webconsole.properties" 32 ); 33 34 function savedFrameToDebuggerLocation(frame) { 35 const { source: url, line, column, sourceId } = frame; 36 return { 37 url, 38 39 // Line is 1-based everywhere. 40 line, 41 42 // The column received from spidermonkey Frame objects are 1-based, 43 // and RDP's console message as well as page errors are providing 1-based columns, 44 // but most of DevTools frontend consider it to be 0-based, especially the debugger. 45 // 46 // Column set to 0 is unknown column location. 47 column: column >= 1 ? column - 1 : null, 48 49 // The sourceId will be a string if it's a source actor ID, otherwise 50 // it is either a Spidermonkey-internal ID from a SavedFrame or missing, 51 // and in either case we can't use the ID for anything useful. 52 id: typeof sourceId === "string" ? sourceId : null, 53 }; 54 } 55 56 /** 57 * Get the tooltip message. 58 * 59 * @param {string|undefined} messageSource 60 * @param {string} url 61 * @returns {string} 62 */ 63 function getTooltipMessage(messageSource, url) { 64 if (messageSource && messageSource === MESSAGE_SOURCE.CSS) { 65 return l10n.getFormatStr("frame.viewsourceinstyleeditor", url); 66 } 67 return l10n.getFormatStr("frame.viewsourceindebugger", url); 68 } 69 70 class Frame extends Component { 71 static get propTypes() { 72 return { 73 // Optional className that will be put into the element. 74 className: PropTypes.string, 75 // SavedFrame, or an object containing all the required properties. 76 frame: PropTypes.shape({ 77 functionDisplayName: PropTypes.string, 78 // This could be a SavedFrame with a numeric sourceId, or it could 79 // be a SavedFrame-like client-side object, in which case the 80 // "sourceId" will be a source actor ID. 81 sourceId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 82 source: PropTypes.string.isRequired, 83 line: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 84 column: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 85 }).isRequired, 86 // Clicking on the frame link -- probably should link to the debugger. 87 onClick: PropTypes.func, 88 // Option to display a function name before the source link. 89 showFunctionName: PropTypes.bool, 90 // Option to display a function name even if it's anonymous. 91 showAnonymousFunctionName: PropTypes.bool, 92 // Option to display a host name after the source link. 93 showHost: PropTypes.bool, 94 // Option to display a host name if the filename is empty or just '/' 95 showEmptyPathAsHost: PropTypes.bool, 96 // Option to display a full source instead of just the filename. 97 showFullSourceUrl: PropTypes.bool, 98 // Service to enable the source map feature for console. 99 sourceMapURLService: PropTypes.object, 100 // The source of the message 101 messageSource: PropTypes.string, 102 }; 103 } 104 105 static get defaultProps() { 106 return { 107 showFunctionName: false, 108 showAnonymousFunctionName: false, 109 showHost: false, 110 showEmptyPathAsHost: false, 111 showFullSourceUrl: false, 112 }; 113 } 114 115 constructor(props) { 116 super(props); 117 this.state = { 118 originalLocation: null, 119 }; 120 this._locationChanged = this._locationChanged.bind(this); 121 } 122 123 componentDidMount() { 124 if (this.props.sourceMapURLService) { 125 const location = savedFrameToDebuggerLocation(this.props.frame); 126 // Many things that make use of this component either: 127 // a) Pass in no sourceId because they have no way to know. 128 // b) Pass in no sourceId because the actor wasn't created when the 129 // server sent its response. 130 // 131 // and due to that, we need to use subscribeByLocation in order to 132 // handle both cases with an without an ID. 133 this.unsubscribeSourceMapURLService = 134 this.props.sourceMapURLService.subscribeByLocation( 135 location, 136 this._locationChanged 137 ); 138 } 139 } 140 141 componentWillUnmount() { 142 if (this.unsubscribeSourceMapURLService) { 143 this.unsubscribeSourceMapURLService(); 144 } 145 } 146 147 _locationChanged(originalLocation) { 148 this.setState({ originalLocation }); 149 } 150 151 /** 152 * Get current location's source, line, and column. 153 * 154 * @returns {{sourceURL: string, line: number|null, column: number|null}} 155 */ 156 #getCurrentLocationInfo = () => { 157 const { frame } = this.props; 158 const { originalLocation } = this.state; 159 160 const generatedLocation = savedFrameToDebuggerLocation(frame); 161 const currentLocation = originalLocation || generatedLocation; 162 163 const column = Number.parseInt(currentLocation.column, 10); 164 165 return { 166 sourceURL: currentLocation.url || "", 167 // line is 1-based 168 line: Number(currentLocation.line) || null, 169 // column is 0-based while we display 1-based numbers 170 column: typeof column == "number" ? column + 1 : null, 171 }; 172 }; 173 174 /** 175 * Get unicode hostname of the source link. 176 * 177 * @returns {string} 178 */ 179 #getCurrentLocationUnicodeHostName = () => { 180 const { sourceURL } = this.#getCurrentLocationInfo(); 181 182 const { host } = getSourceNames(sourceURL); 183 return host ? getUnicodeHostname(host) : ""; 184 }; 185 186 /** 187 * Check if the current location is linkable. 188 * 189 * @returns {boolean} 190 */ 191 #isCurrentLocationLinkable = () => { 192 const { frame } = this.props; 193 const { originalLocation } = this.state; 194 195 const generatedLocation = savedFrameToDebuggerLocation(frame); 196 197 // Reparse the URL to determine if we should link this; `getSourceNames` 198 // has already cached this indirectly. We don't want to attempt to 199 // link to "self-hosted" and "(unknown)". 200 // Source mapped sources might not necessary linkable, but they 201 // are still valid in the debugger. 202 // If we have a source ID then we can show the source in the debugger. 203 return !!( 204 originalLocation || 205 generatedLocation.id || 206 !!parseURL(generatedLocation.url) 207 ); 208 }; 209 210 /** 211 * Get the props of the top element. 212 */ 213 #getTopElementProps = () => { 214 const { className } = this.props; 215 216 const { sourceURL, line, column } = this.#getCurrentLocationInfo(); 217 const { long } = getSourceNames(sourceURL); 218 const props = { 219 "data-url": long, 220 className: "frame-link" + (className ? ` ${className}` : ""), 221 }; 222 223 // If we have a line number > 0. 224 if (line) { 225 // Add `data-line` attribute for testing 226 props["data-line"] = line; 227 228 // Intentionally exclude 0 229 if (column) { 230 // Add `data-column` attribute for testing 231 props["data-column"] = column; 232 } 233 } 234 return props; 235 }; 236 237 /** 238 * Get the props of the source element. 239 */ 240 #getSourceElementsProps = () => { 241 const { frame, onClick, messageSource } = this.props; 242 243 const generatedLocation = savedFrameToDebuggerLocation(frame); 244 const { sourceURL, line, column } = this.#getCurrentLocationInfo(); 245 const { long } = getSourceNames(sourceURL); 246 let url = getUnicodeUrl(long); 247 248 // Exclude all falsy values, including `0`, as line numbers start with 1. 249 if (line) { 250 url += `:${line}`; 251 // Intentionally exclude 0 252 if (column) { 253 url += `:${column}`; 254 } 255 } 256 257 const isLinkable = this.#isCurrentLocationLinkable(); 258 259 // Inner el is useful for achieving ellipsis on the left and correct LTR/RTL 260 // ordering. See CSS styles for frame-link-source-[inner] and bug 1290056. 261 const tooltipMessage = getTooltipMessage(messageSource, url); 262 263 const sourceElConfig = { 264 key: "source", 265 className: "frame-link-source", 266 title: isLinkable ? tooltipMessage : url, 267 }; 268 269 if (isLinkable) { 270 return { 271 ...sourceElConfig, 272 onClick: e => { 273 // We always need to prevent the default behavior of <a> link 274 e.preventDefault(); 275 if (onClick) { 276 e.stopPropagation(); 277 278 onClick(generatedLocation); 279 } 280 }, 281 href: sourceURL, 282 draggable: false, 283 }; 284 } 285 286 return sourceElConfig; 287 }; 288 289 /** 290 * Render the source elements. 291 * 292 * @returns {React.ReactNode} 293 */ 294 #renderSourceElements = () => { 295 const { line, column } = this.#getCurrentLocationInfo(); 296 297 const sourceElements = [this.#renderDisplaySource()]; 298 299 if (line) { 300 let lineInfo = `:${line}`; 301 302 // Intentionally exclude 0 303 if (column) { 304 lineInfo += `:${column}`; 305 } 306 307 sourceElements.push( 308 dom.span( 309 { 310 key: "line", 311 className: "frame-link-line", 312 }, 313 lineInfo 314 ) 315 ); 316 } 317 318 if (this.#isCurrentLocationLinkable()) { 319 return dom.a(this.#getSourceElementsProps(), sourceElements); 320 } 321 // If source is not a URL (self-hosted, eval, etc.), don't make 322 // it an anchor link, as we can't link to it. 323 return dom.span(this.#getSourceElementsProps(), sourceElements); 324 }; 325 326 /** 327 * Render the display source. 328 * 329 * @returns {React.ReactNode} 330 */ 331 #renderDisplaySource = () => { 332 const { showEmptyPathAsHost, showFullSourceUrl } = this.props; 333 const { originalLocation } = this.state; 334 335 const { sourceURL } = this.#getCurrentLocationInfo(); 336 const { short, long, host } = getSourceNames(sourceURL); 337 const unicodeShort = getUnicodeUrlPath(short); 338 const unicodeLong = getUnicodeUrl(long); 339 let displaySource = showFullSourceUrl ? unicodeLong : unicodeShort; 340 if (originalLocation) { 341 displaySource = getSourceMappedFile(displaySource); 342 343 // In case of pretty-printed HTML file, we would only get the formatted suffix; replace 344 // it with the full URL instead 345 if (showEmptyPathAsHost && displaySource == ":formatted") { 346 displaySource = host + displaySource; 347 } 348 } else if ( 349 showEmptyPathAsHost && 350 (displaySource === "" || displaySource === "/") 351 ) { 352 displaySource = host; 353 } 354 355 return dom.span( 356 { 357 key: "filename", 358 className: "frame-link-filename", 359 }, 360 displaySource 361 ); 362 }; 363 364 /** 365 * Render the function display name. 366 * 367 * @returns {React.ReactNode} 368 */ 369 #renderFunctionDisplayName = () => { 370 const { frame, showFunctionName, showAnonymousFunctionName } = this.props; 371 if (!showFunctionName) { 372 return null; 373 } 374 const functionDisplayName = frame.functionDisplayName; 375 if (functionDisplayName || showAnonymousFunctionName) { 376 return [ 377 dom.span( 378 { 379 key: "function-display-name", 380 className: "frame-link-function-display-name", 381 }, 382 functionDisplayName || webl10n.getStr("stacktrace.anonymousFunction") 383 ), 384 " ", 385 ]; 386 } 387 return null; 388 }; 389 390 render() { 391 const { showHost } = this.props; 392 393 const elements = [ 394 this.#renderFunctionDisplayName(), 395 this.#renderSourceElements(), 396 ]; 397 398 const unicodeHost = showHost 399 ? this.#getCurrentLocationUnicodeHostName() 400 : null; 401 if (unicodeHost) { 402 elements.push(" "); 403 elements.push( 404 dom.span( 405 { 406 key: "host", 407 className: "frame-link-host", 408 }, 409 unicodeHost 410 ) 411 ); 412 } 413 414 return dom.span(this.#getTopElementProps(), ...elements); 415 } 416 } 417 418 module.exports = Frame;