diff --git a/lib/nodejs/esm-utils.js b/lib/nodejs/esm-utils.js index 5318099365..f0e1c21375 100644 --- a/lib/nodejs/esm-utils.js +++ b/lib/nodejs/esm-utils.js @@ -39,48 +39,34 @@ exports.requireOrImport = async (file, esmDecorator) => { return formattedImport(file, esmDecorator); } try { - return dealWithExports(await formattedImport(file, esmDecorator)); - } catch (err) { + return require(file); + } catch (requireErr) { if ( - err.code === 'ERR_MODULE_NOT_FOUND' || - err.code === 'ERR_UNKNOWN_FILE_EXTENSION' || - err.code === 'ERR_UNSUPPORTED_DIR_IMPORT' + requireErr.code === 'ERR_REQUIRE_ESM' || requireErr.code === 'ERR_INTERNAL_ASSERTION' || + (requireErr instanceof SyntaxError && + requireErr + .toString() + .includes('Cannot use import statement outside a module')) ) { + // In Node.js environments after version 22.11, the `loadESMFromCJS` function + // is used within the `require` function to handle cases where the target file + // is in ESM (ECMAScript Module) format. If the Node.js version is after 22.11, + // the code will import the module without any issues. For older versions, + // this `if` statement is required to properly handle ESM modules. + // This `if` statement can be removed once all Node.js environments with current + // support include the `loadESMFromCJS` function. 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; - } + return dealWithExports(await formattedImport(file, esmDecorator)); + } catch (err) { + throw err; } } else { - throw err; + throw requireErr; } } }; + function dealWithExports(module) { if (module.default) { return module.default; diff --git a/test/integration/plugins/root-hooks.spec.js b/test/integration/plugins/root-hooks.spec.js index 597575b474..4c9b269de4 100644 --- a/test/integration/plugins/root-hooks.spec.js +++ b/test/integration/plugins/root-hooks.spec.js @@ -136,47 +136,38 @@ describe('root hooks', function () { }); describe('support ESM via .js extension w/o type=module', function () { - describe('should fail due to ambiguous file type', function () { - const filename = - '../fixtures/plugins/root-hooks/root-hook-defs-esm-broken.fixture.js'; - const noDetectModuleRegex = /SyntaxError: Unexpected token/; - const detectModuleRegex = /Cannot require\(\) ES Module/; - - it('with --no-experimental-detect-module', function () { + const filename = + '../fixtures/plugins/root-hooks/root-hook-defs-esm-broken.fixture.js'; + const [major, minor] = process.version.slice(1).split('.').map(Number); + if (major > 22 || (major === 22 && minor >= 7)) { + it('It should not fail since nodejs can recognize the file based on syntax', function () { return expect( - invokeMochaAsync( - [ - '--require=' + require.resolve(filename), // as object - '--no-experimental-detect-module' - ], - 'pipe' - )[1], - 'when fulfilled', - 'to contain output', - noDetectModuleRegex + runMochaForHookOutput([ + '--require=' + require.resolve(filename), + require.resolve( + '../fixtures/plugins/root-hooks/root-hook-test.fixture.js' + ) + ]), + 'to be fulfilled with', + ['mjs afterAll', 'mjs beforeAll'] ); }); - - it('with --experimental-detect-module', function () { - // --experimental-detect-module was introduced in Node 21.1.0 - const expectedRegex = - process.version >= 'v21.1.0' - ? detectModuleRegex - : noDetectModuleRegex; + } else { + const noModuleDetectedRegex = /SyntaxError: Unexpected token/; + it('should fail due to ambiguous file type', function () { return expect( invokeMochaAsync( [ - '--require=' + require.resolve(filename), // as object - '--experimental-detect-module' + '--require=' + require.resolve(filename) // as object ], 'pipe' )[1], 'when fulfilled', 'to contain output', - expectedRegex + noModuleDetectedRegex ); }); - }); + } }); });