base-loader.sys.mjs (19982B)
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 /* exported Loader, resolveURI, Module, Require, unload */ 6 7 const systemPrincipal = Components.Constructor( 8 "@mozilla.org/systemprincipal;1", 9 "nsIPrincipal" 10 )(); 11 12 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 13 14 const lazy = {}; 15 16 XPCOMUtils.defineLazyServiceGetter( 17 lazy, 18 "resProto", 19 "@mozilla.org/network/protocol;1?name=resource", 20 Ci.nsIResProtocolHandler 21 ); 22 23 ChromeUtils.defineESModuleGetters( 24 lazy, 25 { 26 NetUtil: "resource://gre/modules/NetUtil.sys.mjs", 27 }, 28 { global: "contextual" } 29 ); 30 31 const VENDOR_URI = "resource://devtools/client/shared/vendor/"; 32 const REACT_ESM_MODULES = new Set([ 33 VENDOR_URI + "react-dev.js", 34 VENDOR_URI + "react.js", 35 VENDOR_URI + "react-dom-dev.js", 36 VENDOR_URI + "react-dom.js", 37 VENDOR_URI + "react-dom-factories.js", 38 VENDOR_URI + "react-dom-server-dev.js", 39 VENDOR_URI + "react-dom-server.js", 40 VENDOR_URI + "react-prop-types-dev.js", 41 VENDOR_URI + "react-prop-types.js", 42 VENDOR_URI + "react-test-renderer.js", 43 ]); 44 45 // Define some shortcuts. 46 function* getOwnIdentifiers(x) { 47 yield* Object.getOwnPropertyNames(x); 48 yield* Object.getOwnPropertySymbols(x); 49 } 50 51 function isJSONURI(uri) { 52 return uri.endsWith(".json"); 53 } 54 function isESMURI(uri) { 55 return uri.endsWith(".mjs"); 56 } 57 function isJSURI(uri) { 58 return uri.endsWith(".js"); 59 } 60 const AbsoluteRegExp = /^(resource|chrome|file|jar):/; 61 function isAbsoluteURI(uri) { 62 return AbsoluteRegExp.test(uri); 63 } 64 function isRelative(id) { 65 return id.startsWith("."); 66 } 67 68 function readURI(uri) { 69 const nsURI = lazy.NetUtil.newURI(uri); 70 if (nsURI.scheme == "resource") { 71 // Resolve to a real URI, this will catch any obvious bad paths without 72 // logging assertions in debug builds, see bug 1135219 73 uri = lazy.resProto.resolveURI(nsURI); 74 } 75 76 const stream = lazy.NetUtil.newChannel({ 77 uri: lazy.NetUtil.newURI(uri, "UTF-8"), 78 loadUsingSystemPrincipal: true, 79 }).open(); 80 const count = stream.available(); 81 const data = lazy.NetUtil.readInputStreamToString(stream, count, { 82 charset: "UTF-8", 83 }); 84 85 stream.close(); 86 87 return data; 88 } 89 90 // Combines all arguments into a resolved, normalized path 91 function join(base, ...paths) { 92 // If this is an absolute URL, we need to normalize only the path portion, 93 // or we wind up stripping too many slashes and producing invalid URLs. 94 const match = /^((?:resource|file|chrome)\:\/\/[^\/]*|jar:[^!]+!)(.*)/.exec( 95 base 96 ); 97 if (match) { 98 return match[1] + normalize([match[2], ...paths].join("/")); 99 } 100 101 return normalize([base, ...paths].join("/")); 102 } 103 104 // Function takes set of options and returns a JS sandbox. Function may be 105 // passed set of options: 106 // - `name`: A string value which identifies the sandbox in about:memory. Will 107 // throw exception if omitted. 108 // - `prototype`: Ancestor for the sandbox that will be created. Defaults to 109 // `{}`. 110 function Sandbox(options) { 111 // Normalize options and rename to match `Cu.Sandbox` expectations. 112 const sandboxOptions = { 113 // This will allow exposing Components as well as Cu, Ci and Cr. 114 wantComponents: true, 115 116 // By default, Sandbox come with a very limited set of global. 117 // The list of all available symbol names is available over there: 118 // https://searchfox.org/mozilla-central/rev/31368c7795f44b7a15531d6c5e52dc97f82cf2d5/js/xpconnect/src/Sandbox.cpp#905-997 119 // Request to expose all meaningful global here: 120 wantGlobalProperties: [ 121 "AbortController", 122 "atob", 123 "btoa", 124 "Blob", 125 "crypto", 126 "ChromeUtils", 127 "CSS", 128 "CSSPositionTryDescriptors", 129 "CSSRule", 130 "CustomStateSet", 131 "DOMParser", 132 "Element", 133 "Event", 134 "FileReader", 135 "FormData", 136 "Headers", 137 "InspectorCSSParser", 138 "InspectorUtils", 139 "MIDIInputMap", 140 "MIDIOutputMap", 141 "Node", 142 "TextDecoder", 143 "TextEncoder", 144 "TrustedHTML", 145 "TrustedScript", 146 "TrustedScriptURL", 147 "URL", 148 "URLSearchParams", 149 "Window", 150 "XMLHttpRequest", 151 ], 152 153 sandboxName: options.name, 154 sandboxPrototype: "prototype" in options ? options.prototype : {}, 155 freshCompartment: options.freshCompartment || false, 156 }; 157 158 return Cu.Sandbox(systemPrincipal, sandboxOptions); 159 } 160 161 // This allows defining some modules in AMD format while retaining CommonJS 162 // compatibility with this loader by allowing the factory function to have 163 // access to general CommonJS functions, e.g. 164 // 165 // define(function(require, exports, module) { 166 // ... code ... 167 // }); 168 function define(factory) { 169 factory(this.require, this.exports, this.module); 170 } 171 172 // Populates `exports` of the given CommonJS `module` object, in the context 173 // of the given `loader` by evaluating code associated with it. 174 function load(loader, module) { 175 const require = Require(loader, module); 176 177 // We expose set of properties defined by `CommonJS` specification via 178 // prototype of the sandbox. Also globals are deeper in the prototype 179 // chain so that each module has access to them as well. 180 const properties = { 181 require, 182 module, 183 exports: module.exports, 184 }; 185 if (loader.supportAMDModules) { 186 properties.define = define; 187 } 188 189 // Create a new object in the shared global of the loader, that will be used 190 // as the scope object for this particular module. 191 const scopeFromSharedGlobal = new loader.sharedGlobal.Object(); 192 Object.assign(scopeFromSharedGlobal, properties); 193 194 const originalExports = module.exports; 195 try { 196 Services.scriptloader.loadSubScript(module.uri, scopeFromSharedGlobal); 197 } catch (error) { 198 // loadSubScript sometime throws string errors, which includes no stack. 199 // At least provide the current stack by re-throwing a real Error object. 200 if (typeof error == "string") { 201 if ( 202 error.startsWith("Error creating URI") || 203 error.startsWith("Error opening input stream (invalid filename?)") 204 ) { 205 throw new Error( 206 `Module \`${module.id}\` is not found at ${module.uri}` 207 ); 208 } 209 throw new Error( 210 `Error while loading module \`${module.id}\` at ${module.uri}:` + 211 "\n" + 212 error 213 ); 214 } 215 // Otherwise just re-throw everything else which should have a stack 216 throw error; 217 } 218 219 // Only freeze the exports object if we created it ourselves. Modules 220 // which completely replace the exports object and still want it 221 // frozen need to freeze it themselves. 222 if (module.exports === originalExports) { 223 Object.freeze(module.exports); 224 } 225 226 return module; 227 } 228 229 // Utility function to normalize module `uri`s so they have `.js` extension. 230 function normalizeExt(uri) { 231 if (isJSURI(uri) || isJSONURI(uri) || isESMURI(uri)) { 232 return uri; 233 } 234 return uri + ".js"; 235 } 236 237 // Utility function to join paths. In common case `base` is a 238 // `requirer.uri` but in some cases it may be `baseURI`. In order to 239 // avoid complexity we require `baseURI` with a trailing `/`. 240 function resolve(id, base) { 241 if (!isRelative(id)) { 242 return id; 243 } 244 245 const baseDir = dirname(base); 246 247 let resolved; 248 if (baseDir.includes(":")) { 249 resolved = join(baseDir, id); 250 } else { 251 resolved = normalize(`${baseDir}/${id}`); 252 } 253 254 // Joining and normalizing removes the "./" from relative files. 255 // We need to ensure the resolution still has the root 256 if (base.startsWith("./")) { 257 resolved = "./" + resolved; 258 } 259 260 return resolved; 261 } 262 263 function compileMapping(paths) { 264 // Make mapping array that is sorted from longest path to shortest path. 265 const mapping = Object.keys(paths) 266 .sort((a, b) => b.length - a.length) 267 .map(path => [path, paths[path]]); 268 269 const PATTERN = /([.\\?+*(){}[\]^$])/g; 270 const escapeMeta = str => str.replace(PATTERN, "\\$1"); 271 272 const patterns = []; 273 paths = {}; 274 275 for (let [path, uri] of mapping) { 276 // Strip off any trailing slashes to make comparisons simpler 277 if (path.endsWith("/")) { 278 path = path.slice(0, -1); 279 uri = uri.replace(/\/+$/, ""); 280 } 281 282 paths[path] = uri; 283 284 // We only want to match path segments explicitly. Examples: 285 // * "foo/bar" matches for "foo/bar" 286 // * "foo/bar" matches for "foo/bar/baz" 287 // * "foo/bar" does not match for "foo/bar-1" 288 // * "foo/bar/" does not match for "foo/bar" 289 // * "foo/bar/" matches for "foo/bar/baz" 290 // 291 // Check for an empty path, an exact match, or a substring match 292 // with the next character being a forward slash. 293 if (path == "") { 294 patterns.push(""); 295 } else { 296 patterns.push(`${escapeMeta(path)}(?=$|/)`); 297 } 298 } 299 300 const pattern = new RegExp(`^(${patterns.join("|")})`); 301 302 // This will replace the longest matching path mapping at the start of 303 // the ID string with its mapped value. 304 return id => { 305 return id.replace(pattern, (m0, m1) => paths[m1]); 306 }; 307 } 308 309 export function resolveURI(id, mapping) { 310 // Do not resolve if already a resource URI 311 if (isAbsoluteURI(id)) { 312 return normalizeExt(id); 313 } 314 315 return normalizeExt(mapping(id)); 316 } 317 318 // Creates version of `require` that will be exposed to the given `module` 319 // in the context of the given `loader`. Each module gets own limited copy 320 // of `require` that is allowed to load only a modules that are associated 321 // with it during link time. 322 export function Require(loader, requirer) { 323 const { modules, mapping, mappingCache, requireHook } = loader; 324 325 function require(id) { 326 if (!id) { 327 // Throw if `id` is not passed. 328 throw Error( 329 "You must provide a module name when calling require() from " + 330 requirer.id, 331 requirer.uri 332 ); 333 } 334 335 if (requireHook) { 336 return requireHook(id, _require); 337 } 338 339 return _require(id); 340 } 341 342 function _require(id) { 343 let { uri, requirement } = getRequirements(id); 344 345 // Load all react modules as ES Modules, in the Browser Loader global. 346 // For this we have to ensure using ChromeUtils.importESModule with `global:"current"`, 347 // but executed from the Loader global scope. `syncImport` does that. 348 if (REACT_ESM_MODULES.has(uri)) { 349 // All CommonJS modules are still importing the .js/CommonJS version, 350 // but we hack these require() call to load the ESM version. 351 uri = uri.replace(/.js$/, ".mjs"); 352 } 353 354 let module = null; 355 // If module is already cached by loader then just use it. 356 if (uri in modules) { 357 module = modules[uri]; 358 } else if (isESMURI(uri)) { 359 module = modules[uri] = Module(requirement, uri); 360 const rv = ChromeUtils.importESModule(uri, { 361 global: "contextual", 362 }); 363 module.exports = rv.default || rv; 364 } else if (isJSONURI(uri)) { 365 let data; 366 367 // First attempt to load and parse json uri 368 // ex: `test.json` 369 // If that doesn"t exist, check for `test.json.js` 370 // for node parity 371 try { 372 data = JSON.parse(readURI(uri)); 373 module = modules[uri] = Module(requirement, uri); 374 module.exports = data; 375 } catch (err) { 376 // If error thrown from JSON parsing, throw that, do not 377 // attempt to find .json.js file 378 if (err && /JSON\.parse/.test(err.message)) { 379 throw err; 380 } 381 uri = uri + ".js"; 382 } 383 } 384 385 // If not yet cached, load and cache it. 386 // We also freeze module to prevent it from further changes 387 // at runtime. 388 if (!(uri in modules)) { 389 // Many of the loader's functionalities are dependent 390 // on modules[uri] being set before loading, so we set it and 391 // remove it if we have any errors. 392 module = modules[uri] = Module(requirement, uri); 393 try { 394 Object.freeze(load(loader, module)); 395 } catch (e) { 396 // Clear out modules cache so we can throw on a second invalid require 397 delete modules[uri]; 398 throw e; 399 } 400 } 401 402 return module.exports; 403 } 404 405 // Resolution function taking a module name/path and 406 // returning a resourceURI and a `requirement` used by the loader. 407 // Used by both `require` and `require.resolve`. 408 function getRequirements(id) { 409 if (!id) { 410 // Throw if `id` is not passed. 411 throw Error( 412 "you must provide a module name when calling require() from " + 413 requirer.id, 414 requirer.uri 415 ); 416 } 417 418 let requirement, uri; 419 420 if (modules[id]) { 421 uri = requirement = id; 422 } else if (requirer) { 423 // Resolve `id` to its requirer if it's relative. 424 requirement = resolve(id, requirer.id); 425 } else { 426 requirement = id; 427 } 428 429 // Resolves `uri` of module using loaders resolve function. 430 if (!uri) { 431 if (mappingCache.has(requirement)) { 432 uri = mappingCache.get(requirement); 433 } else { 434 uri = resolveURI(requirement, mapping); 435 mappingCache.set(requirement, uri); 436 } 437 } 438 439 // Throw if `uri` can not be resolved. 440 if (!uri) { 441 throw Error( 442 "Module: Can not resolve '" + 443 id + 444 "' module required by " + 445 requirer.id + 446 " located at " + 447 requirer.uri, 448 requirer.uri 449 ); 450 } 451 452 return { uri, requirement }; 453 } 454 455 // Expose the `resolve` function for this `Require` instance 456 require.resolve = _require.resolve = function (id) { 457 const { uri } = getRequirements(id); 458 return uri; 459 }; 460 461 // This is like webpack's require.context. It returns a new require 462 // function that prepends the prefix to any requests. 463 require.context = prefix => { 464 return id => { 465 return require(prefix + id); 466 }; 467 }; 468 469 return require; 470 } 471 472 // Makes module object that is made available to CommonJS modules when they 473 // are evaluated, along with `exports` and `require`. 474 export function Module(id, uri) { 475 return Object.create(null, { 476 id: { enumerable: true, value: id }, 477 exports: { 478 enumerable: true, 479 writable: true, 480 value: Object.create(null), 481 configurable: true, 482 }, 483 uri: { value: uri }, 484 }); 485 } 486 487 // Takes `loader`, and unload `reason` string and notifies all observers that 488 // they should cleanup after them-self. 489 export function unload(loader, reason) { 490 // subject is a unique object created per loader instance. 491 // This allows any code to cleanup on loader unload regardless of how 492 // it was loaded. To handle unload for specific loader subject may be 493 // asserted against loader.destructor or require("@loader/unload") 494 // Note: We don not destroy loader's module cache or sandboxes map as 495 // some modules may do cleanup in subsequent turns of event loop. Destroying 496 // cache may cause module identity problems in such cases. 497 const subject = { wrappedJSObject: loader.destructor }; 498 Services.obs.notifyObservers(subject, "devtools:loader:destroy", reason); 499 } 500 501 // Function makes new loader that can be used to load CommonJS modules. 502 // Loader takes following options: 503 // - `paths`: Mandatory dictionary of require path mapped to absolute URIs. 504 // Object keys are path prefix used in require(), values are URIs where each 505 // prefix should be mapped to. 506 // - `globals`: Optional map of globals, that all module scopes will inherit 507 // from. Map is also exposed under `globals` property of the returned loader 508 // so it can be extended further later. Defaults to `{}`. 509 // - `sandboxName`: String, name of the sandbox displayed in about:memory. 510 // - `sandboxPrototype`: Object used to define globals on all module's 511 // sandboxes. 512 // - `requireHook`: Optional function used to replace native require function 513 // from loader. This function receive the module path as first argument, 514 // and native require method as second argument. 515 export function Loader(options) { 516 let { paths, globals } = options; 517 if (!globals) { 518 globals = {}; 519 } 520 521 // We create an identity object that will be dispatched on an unload 522 // event as subject. This way unload listeners will be able to assert 523 // which loader is unloaded. Please note that we intentionally don"t 524 // use `loader` as subject to prevent a loader access leakage through 525 // observer notifications. 526 const destructor = Object.create(null); 527 528 const mapping = compileMapping(paths); 529 530 // Define pseudo modules. 531 const builtinModuleExports = { 532 "@loader/unload": destructor, 533 "@loader/options": options, 534 }; 535 536 const modules = {}; 537 for (const id of Object.keys(builtinModuleExports)) { 538 // We resolve `uri` from `id` since modules are cached by `uri`. 539 const uri = resolveURI(id, mapping); 540 const module = Module(id, uri); 541 542 // Lazily expose built-in modules in order to 543 // allow them to be loaded lazily. 544 Object.defineProperty(module, "exports", { 545 enumerable: true, 546 get() { 547 return builtinModuleExports[id]; 548 }, 549 }); 550 551 modules[uri] = module; 552 } 553 554 let sharedGlobal; 555 if (options.sharedGlobal) { 556 sharedGlobal = options.sharedGlobal; 557 } else { 558 // Create the unique sandbox we will be using for all modules, 559 // so that we prevent creating a new compartment per module. 560 // The side effect is that all modules will share the same 561 // global objects. 562 sharedGlobal = Sandbox({ 563 name: options.sandboxName || "DevTools", 564 prototype: options.sandboxPrototype || globals, 565 freshCompartment: options.freshCompartment, 566 }); 567 } 568 569 if (options.sharedGlobal || options.sandboxPrototype) { 570 // If we were given a sharedGlobal or a sandboxPrototype, we have to define 571 // the globals on the shared global directly. Note that this will not work 572 // for callers who depend on being able to add globals after the loader was 573 // created. 574 for (const name of getOwnIdentifiers(globals)) { 575 Object.defineProperty( 576 sharedGlobal, 577 name, 578 Object.getOwnPropertyDescriptor(globals, name) 579 ); 580 } 581 } 582 583 // Loader object is just a representation of a environment 584 // state. We mark its properties non-enumerable 585 // as they are pure implementation detail that no one should rely upon. 586 const returnObj = { 587 destructor: { enumerable: false, value: destructor }, 588 globals: { enumerable: false, value: globals }, 589 mapping: { enumerable: false, value: mapping }, 590 mappingCache: { enumerable: false, value: new Map() }, 591 // Map of module objects indexed by module URIs. 592 modules: { enumerable: false, value: modules }, 593 sharedGlobal: { enumerable: false, value: sharedGlobal }, 594 supportAMDModules: { 595 enumerable: false, 596 value: options.supportAMDModules || false, 597 }, 598 requireHook: { 599 enumerable: false, 600 writable: true, 601 value: options.requireHook, 602 }, 603 }; 604 605 return Object.create(null, returnObj); 606 } 607 608 // NB: These methods are from the UNIX implementation of OS.Path. Refactoring 609 // this module to not use path methods on stringly-typed URIs is 610 // non-trivial. 611 function dirname(path) { 612 let index = path.lastIndexOf("/"); 613 if (index == -1) { 614 return "."; 615 } 616 while (index >= 0 && path[index] == "/") { 617 --index; 618 } 619 return path.slice(0, index + 1); 620 } 621 622 function normalize(path) { 623 const stack = []; 624 let absolute; 625 if (path.length >= 0 && path[0] == "/") { 626 absolute = true; 627 } else { 628 absolute = false; 629 } 630 path.split("/").forEach(function (v) { 631 switch (v) { 632 case "": 633 case ".": // fallthrough 634 break; 635 case "..": 636 if (!stack.length) { 637 if (absolute) { 638 throw new Error("Path is ill-formed: attempting to go past root"); 639 } else { 640 stack.push(".."); 641 } 642 } else if (stack[stack.length - 1] == "..") { 643 stack.push(".."); 644 } else { 645 stack.pop(); 646 } 647 break; 648 default: 649 stack.push(v); 650 } 651 }); 652 const string = stack.join("/"); 653 return absolute ? "/" + string : string; 654 }