| const path = require("node:path"); |
| const url = require("node:url"); |
| const debug = require("debug")("mocha:esm-utils"); |
| const { isMochaError } = require("../errors.js"); |
| |
| 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); |
| if (requireErr.name === "TSError" || isMochaError(requireErr)) { |
| throw requireErr; |
| } |
| 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 }; |
| } |
| } |
| |
| const resolveFile = (file) => { |
| if (URL.canParse(file)) { |
| const parsed = new URL(file); |
| // Single-letter "protocols" (e.g. "d:") are Windows drive letters, not URL schemes |
| if (parsed.protocol.length > 2) { |
| if (parsed.protocol === "file:") { |
| return url.fileURLToPath(parsed); |
| } |
| throw new Error( |
| `Unsupported URL protocol "${parsed.protocol}". Only "file:" URLs are supported.`, |
| ); |
| } |
| } |
| return path.resolve(file); |
| }; |
| |
| exports.loadFilesAsync = async ( |
| files, |
| preLoadFunc, |
| postLoadFunc, |
| esmDecorator, |
| ) => { |
| for (const file of files) { |
| preLoadFunc(file); |
| const result = await exports.requireOrImport( |
| resolveFile(file), |
| esmDecorator, |
| ); |
| postLoadFunc(file, result); |
| } |
| }; |