loader.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. const path = require('path');
  2. class Loader {
  3. constructor(options) {
  4. options = options || {};
  5. this.require_ = options.requireShim || requireShim;
  6. this.import_ = options.importShim || importShim;
  7. this.resolvePath_ = options.resolvePath || path.resolve.bind(path);
  8. this.alwaysImport = true;
  9. }
  10. load(modulePath) {
  11. if ((this.alwaysImport && !modulePath.endsWith('.json')) || modulePath.endsWith('.mjs')) {
  12. let importSpecifier;
  13. if (modulePath.indexOf(path.sep) === -1 && modulePath.indexOf('/') === -1) {
  14. importSpecifier = modulePath;
  15. } else {
  16. // The ES module spec requires import paths to be valid URLs. As of v14,
  17. // Node enforces this on Windows but not on other OSes. On OS X, import
  18. // paths that are URLs must not contain parent directory references.
  19. importSpecifier = `file://${this.resolvePath_(modulePath)}`;
  20. }
  21. return this.import_(importSpecifier)
  22. .then(
  23. mod => mod.default,
  24. e => {
  25. if (e.code === 'ERR_UNKNOWN_FILE_EXTENSION') {
  26. // Extension isn't supported by import, e.g. .jsx. Fall back to
  27. // require(). This could lead to confusing error messages if someone
  28. // tries to use ES module syntax without transpiling in a file with
  29. // an unsupported extension, but it shouldn't break anything and it
  30. // should work well in the normal case where the file is loadable
  31. // as a CommonJS module, either directly or with the help of a
  32. // loader like `@babel/register`.
  33. return this.require_(modulePath);
  34. } else {
  35. return Promise.reject(fixupImportException(e, modulePath));
  36. }
  37. }
  38. );
  39. } else {
  40. return new Promise(resolve => {
  41. const result = this.require_(modulePath);
  42. resolve(result);
  43. });
  44. }
  45. }
  46. }
  47. function requireShim(modulePath) {
  48. return require(modulePath);
  49. }
  50. function importShim(modulePath) {
  51. return import(modulePath);
  52. }
  53. function fixupImportException(e, importedPath) {
  54. // When an ES module has a syntax error, the resulting exception does not
  55. // include the filename, which the user will need to debug the problem. We
  56. // need to fix those up to include the filename. However, other kinds of load-
  57. // time errors *do* include the filename and usually the line number. We need
  58. // to leave those alone.
  59. //
  60. // Some examples of load-time errors that we need to deal with:
  61. // 1. Syntax error in an ESM spec:
  62. // SyntaxError: missing ) after argument list
  63. // at Loader.moduleStrategy (node:internal/modules/esm/translators:147:18)
  64. // at async link (node:internal/modules/esm/module_job:64:21)
  65. //
  66. // 2. Syntax error in an ES module imported from an ESM spec. This is exactly
  67. // the same as #1: there is no way to tell which file actually has the syntax
  68. // error.
  69. //
  70. // 3. Syntax error in a CommonJS module imported by an ES module:
  71. // /path/to/commonjs_with_syntax_error.js:2
  72. //
  73. //
  74. //
  75. // SyntaxError: Unexpected end of input
  76. // at Object.compileFunction (node:vm:355:18)
  77. // at wrapSafe (node:internal/modules/cjs/loader:1038:15)
  78. // at Module._compile (node:internal/modules/cjs/loader:1072:27)
  79. // at Object.Module._extensions..js (node:internal/modules/cjs/loader:1137:10)
  80. // at Module.load (node:internal/modules/cjs/loader:988:32)
  81. // at Function.Module._load (node:internal/modules/cjs/loader:828:14)
  82. // at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:201:29)
  83. // at ModuleJob.run (node:internal/modules/esm/module_job:175:25)
  84. // at async Loader.import (node:internal/modules/esm/loader:178:24)
  85. // at async file:///path/to/esm_that_imported_cjs.mjs:2:11
  86. //
  87. // Note: For Jasmine's purposes, case 3 only occurs in Node >= 14.8. Older
  88. // versions don't support top-level await, without which it's not possible to
  89. // load a CommonJS module from an ES module at load-time. The entire content
  90. // above, including the file path and the three blank lines, is part of the
  91. // error's `stack` property. There may or may not be any stack trace after the
  92. // SyntaxError line, and if there's a stack trace it may or may not contain
  93. // any useful information.
  94. //
  95. // 4. Any other kind of exception thrown at load time
  96. //
  97. // Error: nope
  98. // at Object.<anonymous> (/path/to/file_throwing_error.js:1:7)
  99. // at Module._compile (node:internal/modules/cjs/loader:1108:14)
  100. // at Object.Module._extensions..js (node:internal/modules/cjs/loader:1137:10)
  101. // at Module.load (node:internal/modules/cjs/loader:988:32)
  102. // at Function.Module._load (node:internal/modules/cjs/loader:828:14)
  103. // at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:201:29)
  104. // at ModuleJob.run (node:internal/modules/esm/module_job:175:25)
  105. // at async Loader.import (node:internal/modules/esm/loader:178:24)
  106. // at async file:///path_to_file_importing_broken_file.mjs:1:1
  107. //
  108. // We need to replace the error with a useful one in cases 1 and 2, but not in
  109. // cases 3 and 4. Distinguishing among them can be tricky. Simple heuristics
  110. // like checking the stack trace for the name of the file we imported fail
  111. // because it often shows up even when the error was elsewhere, e.g. at the
  112. // bottom of the stack traces in the examples for cases 3 and 4 above. To add
  113. // to the fun, file paths in errors on Windows can be either Windows style
  114. // paths (c:\path\to\file.js) or URLs (file:///c:/path/to/file.js).
  115. if (!(e instanceof SyntaxError)) {
  116. return e;
  117. }
  118. const escapedWin = escapeStringForRegexp(importedPath.replace(/\//g, '\\'));
  119. const windowsPathRegex = new RegExp('[a-zA-z]:\\\\([^\\s]+\\\\|)' + escapedWin);
  120. const windowsUrlRegex = new RegExp('file:///[a-zA-z]:\\\\([^\\s]+\\\\|)' + escapedWin);
  121. const anyUnixPathFirstLineRegex = /^\/[^\s:]+:\d/;
  122. const anyWindowsPathFirstLineRegex = /^[a-zA-Z]:(\\[^\s\\:]+)+:/;
  123. if (e.message.indexOf(importedPath) !== -1
  124. || e.stack.indexOf(importedPath) !== -1
  125. || e.stack.match(windowsPathRegex) || e.stack.match(windowsUrlRegex)
  126. || e.stack.match(anyUnixPathFirstLineRegex)
  127. || e.stack.match(anyWindowsPathFirstLineRegex)) {
  128. return e;
  129. } else {
  130. return new Error(`While loading ${importedPath}: ${e.constructor.name}: ${e.message}`);
  131. }
  132. }
  133. // Adapted from Sindre Sorhus's escape-string-regexp (MIT license)
  134. function escapeStringForRegexp(string) {
  135. // Escape characters with special meaning either inside or outside character sets.
  136. // Use a simple backslash escape when it’s always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns’ stricter grammar.
  137. return string
  138. .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
  139. .replace(/-/g, '\\x2d');
  140. }
  141. module.exports = Loader;