| const path = require('node:path'); |
| const url = require('node:url'); |
| const debug = require('debug')('mocha:esm-utils'); |
| |
| const forward = x => x; |
| |
| const formattedImport = async (file, esmDecorator = forward) => { |
| if (path.isAbsolute(file)) { |
| try { |
| return await exports.doImport(esmDecorator(url.pathToFileURL(file))); |
| } catch (err) { |
| // This is a hack created because ESM in Node.js (at least in Node v15.5.1) does not emit |
| // the location of the syntax error in the error thrown. |
| // This is problematic because the user can't see what file has the problem, |
| // so we add the file location to the error. |
| // TODO: remove once Node.js fixes the problem. |
| if ( |
| err instanceof SyntaxError && |
| err.message && |
| err.stack && |
| !err.stack.includes(file) |
| ) { |
| const newErrorWithFilename = new SyntaxError(err.message); |
| newErrorWithFilename.stack = err.stack.replace( |
| /^SyntaxError/, |
| `SyntaxError[ @${file} ]` |
| ); |
| throw newErrorWithFilename; |
| } |
| throw err; |
| } |
| } |
| return exports.doImport(esmDecorator(file)); |
| }; |
| |
| exports.doImport = async file => import(file); |
| |
| // When require(esm) is not available, we need to use `import()` to load ESM modules. |
| // In this case, CJS modules are loaded using `import()` as well. When Node.js' builtin |
| // TypeScript support is enabled, `.ts` files are also loaded using `import()`, and |
| // compilers based on `require.extensions` are omitted. |
| const tryImportAndRequire = async (file, esmDecorator) => { |
| if (path.extname(file) === '.mjs') { |
| return formattedImport(file, esmDecorator); |
| } |
| try { |
| return dealWithExports(await formattedImport(file, esmDecorator)); |
| } catch (err) { |
| if ( |
| err.code === 'ERR_MODULE_NOT_FOUND' || |
| err.code === 'ERR_UNKNOWN_FILE_EXTENSION' || |
| err.code === 'ERR_UNSUPPORTED_DIR_IMPORT' |
| ) { |
| try { |
| // Importing a file usually works, but the resolution of `import` is the ESM |
| // resolution algorithm, and not the CJS resolution algorithm. We may have |
| // failed because we tried the ESM resolution, so we try to `require` it. |
| return require(file); |
| } catch (requireErr) { |
| if ( |
| requireErr.code === 'ERR_REQUIRE_ESM' || |
| (requireErr instanceof SyntaxError && |
| requireErr |
| .toString() |
| .includes('Cannot use import statement outside a module')) |
| ) { |
| // ERR_REQUIRE_ESM happens when the test file is a JS file, but via type:module is actually ESM, |
| // AND has an import to a file that doesn't exist. |
| // This throws an `ERR_MODULE_NOT_FOUND` error above, |
| // and when we try to `require` it here, it throws an `ERR_REQUIRE_ESM`. |
| // What we want to do is throw the original error (the `ERR_MODULE_NOT_FOUND`), |
| // and not the `ERR_REQUIRE_ESM` error, which is a red herring. |
| // |
| // SyntaxError happens when in an edge case: when we're using an ESM loader that loads |
| // a `test.ts` file (i.e. unrecognized extension), and that file includes an unknown |
| // import (which throws an ERR_MODULE_NOT_FOUND). `require`-ing it will throw the |
| // syntax error, because we cannot require a file that has `import`-s. |
| throw err; |
| } else { |
| throw requireErr; |
| } |
| } |
| } else { |
| throw err; |
| } |
| } |
| }; |
| |
| // Utilize Node.js' require(esm) feature to load ESM modules |
| // and CJS modules. This keeps the require() features like `require.extensions` |
| // and `require.cache` effective, while allowing us to load ESM modules |
| // and CJS modules in the same way. |
| const requireModule = async (file, esmDecorator) => { |
| if (path.extname(file) === '.mjs') { |
| return formattedImport(file, esmDecorator); |
| } |
| try { |
| return require(file); |
| } catch (requireErr) { |
| debug('requireModule caught err: %O', requireErr.message); |
| try { |
| return dealWithExports(await formattedImport(file, esmDecorator)); |
| } catch (importErr) { |
| // If a --require module throws in a Node.js version that doesn't yet support .ts files, |
| // the fallback import() will throw an uninformative error about the file extension. |
| // What we actually care about is the original require() error. |
| // See: https://github.com/mochajs/mocha/issues/5393 |
| if ( |
| /\.(cts|mts|ts)$/.test(file) && |
| importErr.code === 'ERR_UNKNOWN_FILE_EXTENSION' |
| ) { |
| throw requireErr; |
| } |
| |
| // Similarly, for an exports/imports mismatch such as a missing 'default', |
| // the require() error will be more informative for users. |
| // See: https://github.com/mochajs/mocha/issues/5411 |
| if (importErr.code === 'ERR_INTERNAL_ASSERTION') { |
| throw requireErr; |
| } |
| |
| throw importErr; |
| } |
| } |
| }; |
| |
| // We only assign this `requireOrImport` function once based on Node version |
| // We check for file extensions in `requireModule` and `tryImportAndRequire` |
| debug('assigning requireOrImport, require_module === %O', process.features.require_module); |
| if (process.features.require_module) { |
| exports.requireOrImport = requireModule; |
| } else { |
| exports.requireOrImport = tryImportAndRequire; |
| } |
| |
| function dealWithExports(module) { |
| if (module.default) { |
| return module.default; |
| } else { |
| return {...module, default: undefined}; |
| } |
| } |
| |
| exports.loadFilesAsync = async ( |
| files, |
| preLoadFunc, |
| postLoadFunc, |
| esmDecorator |
| ) => { |
| for (const file of files) { |
| preLoadFunc(file); |
| const result = await exports.requireOrImport( |
| path.resolve(file), |
| esmDecorator |
| ); |
| postLoadFunc(file, result); |
| } |
| }; |