esm-utils.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. const path = require('node:path');
  2. const url = require('node:url');
  3. const debug = require('debug')('mocha:esm-utils');
  4. const forward = x => x;
  5. const formattedImport = async (file, esmDecorator = forward) => {
  6. if (path.isAbsolute(file)) {
  7. try {
  8. return await exports.doImport(esmDecorator(url.pathToFileURL(file)));
  9. } catch (err) {
  10. // This is a hack created because ESM in Node.js (at least in Node v15.5.1) does not emit
  11. // the location of the syntax error in the error thrown.
  12. // This is problematic because the user can't see what file has the problem,
  13. // so we add the file location to the error.
  14. // TODO: remove once Node.js fixes the problem.
  15. if (
  16. err instanceof SyntaxError &&
  17. err.message &&
  18. err.stack &&
  19. !err.stack.includes(file)
  20. ) {
  21. const newErrorWithFilename = new SyntaxError(err.message);
  22. newErrorWithFilename.stack = err.stack.replace(
  23. /^SyntaxError/,
  24. `SyntaxError[ @${file} ]`
  25. );
  26. throw newErrorWithFilename;
  27. }
  28. throw err;
  29. }
  30. }
  31. return exports.doImport(esmDecorator(file));
  32. };
  33. exports.doImport = async file => import(file);
  34. // When require(esm) is not available, we need to use `import()` to load ESM modules.
  35. // In this case, CJS modules are loaded using `import()` as well. When Node.js' builtin
  36. // TypeScript support is enabled, `.ts` files are also loaded using `import()`, and
  37. // compilers based on `require.extensions` are omitted.
  38. const tryImportAndRequire = async (file, esmDecorator) => {
  39. if (path.extname(file) === '.mjs') {
  40. return formattedImport(file, esmDecorator);
  41. }
  42. try {
  43. return dealWithExports(await formattedImport(file, esmDecorator));
  44. } catch (err) {
  45. if (
  46. err.code === 'ERR_MODULE_NOT_FOUND' ||
  47. err.code === 'ERR_UNKNOWN_FILE_EXTENSION' ||
  48. err.code === 'ERR_UNSUPPORTED_DIR_IMPORT'
  49. ) {
  50. try {
  51. // Importing a file usually works, but the resolution of `import` is the ESM
  52. // resolution algorithm, and not the CJS resolution algorithm. We may have
  53. // failed because we tried the ESM resolution, so we try to `require` it.
  54. return require(file);
  55. } catch (requireErr) {
  56. if (
  57. requireErr.code === 'ERR_REQUIRE_ESM' ||
  58. (requireErr instanceof SyntaxError &&
  59. requireErr
  60. .toString()
  61. .includes('Cannot use import statement outside a module'))
  62. ) {
  63. // ERR_REQUIRE_ESM happens when the test file is a JS file, but via type:module is actually ESM,
  64. // AND has an import to a file that doesn't exist.
  65. // This throws an `ERR_MODULE_NOT_FOUND` error above,
  66. // and when we try to `require` it here, it throws an `ERR_REQUIRE_ESM`.
  67. // What we want to do is throw the original error (the `ERR_MODULE_NOT_FOUND`),
  68. // and not the `ERR_REQUIRE_ESM` error, which is a red herring.
  69. //
  70. // SyntaxError happens when in an edge case: when we're using an ESM loader that loads
  71. // a `test.ts` file (i.e. unrecognized extension), and that file includes an unknown
  72. // import (which throws an ERR_MODULE_NOT_FOUND). `require`-ing it will throw the
  73. // syntax error, because we cannot require a file that has `import`-s.
  74. throw err;
  75. } else {
  76. throw requireErr;
  77. }
  78. }
  79. } else {
  80. throw err;
  81. }
  82. }
  83. };
  84. // Utilize Node.js' require(esm) feature to load ESM modules
  85. // and CJS modules. This keeps the require() features like `require.extensions`
  86. // and `require.cache` effective, while allowing us to load ESM modules
  87. // and CJS modules in the same way.
  88. const requireModule = async (file, esmDecorator) => {
  89. if (path.extname(file) === '.mjs') {
  90. return formattedImport(file, esmDecorator);
  91. }
  92. try {
  93. return require(file);
  94. } catch (requireErr) {
  95. debug('requireModule caught err: %O', requireErr.message);
  96. try {
  97. return dealWithExports(await formattedImport(file, esmDecorator));
  98. } catch (importErr) {
  99. // If a --require module throws in a Node.js version that doesn't yet support .ts files,
  100. // the fallback import() will throw an uninformative error about the file extension.
  101. // What we actually care about is the original require() error.
  102. // See: https://github.com/mochajs/mocha/issues/5393
  103. if (
  104. /\.(cts|mts|ts)$/.test(file) &&
  105. importErr.code === 'ERR_UNKNOWN_FILE_EXTENSION'
  106. ) {
  107. throw requireErr;
  108. }
  109. // Similarly, for an exports/imports mismatch such as a missing 'default',
  110. // the require() error will be more informative for users.
  111. // See: https://github.com/mochajs/mocha/issues/5411
  112. if (importErr.code === 'ERR_INTERNAL_ASSERTION') {
  113. throw requireErr;
  114. }
  115. throw importErr;
  116. }
  117. }
  118. };
  119. // We only assign this `requireOrImport` function once based on Node version
  120. // We check for file extensions in `requireModule` and `tryImportAndRequire`
  121. debug('assigning requireOrImport, require_module === %O', process.features.require_module);
  122. if (process.features.require_module) {
  123. exports.requireOrImport = requireModule;
  124. } else {
  125. exports.requireOrImport = tryImportAndRequire;
  126. }
  127. function dealWithExports(module) {
  128. if (module.default) {
  129. return module.default;
  130. } else {
  131. return {...module, default: undefined};
  132. }
  133. }
  134. exports.loadFilesAsync = async (
  135. files,
  136. preLoadFunc,
  137. postLoadFunc,
  138. esmDecorator
  139. ) => {
  140. for (const file of files) {
  141. preLoadFunc(file);
  142. const result = await exports.requireOrImport(
  143. path.resolve(file),
  144. esmDecorator
  145. );
  146. postLoadFunc(file, result);
  147. }
  148. };