From 5cde7f50eabb4d9af0fff1d15998362824294896 Mon Sep 17 00:00:00 2001 From: Chengzhong Wu Date: Tue, 13 May 2025 14:23:34 +0100 Subject: [PATCH] feat: use require to load esm This allows compilers based on `require.extensions` continue to work. --- eslint.config.js | 3 +- lib/nodejs/esm-utils.js | 30 ++++++++++++- test/compiler-cjs/test.js | 9 ++++ test/compiler-cjs/test.js.compiled | 9 ++++ test/compiler-cjs/test.ts | 9 ++++ test/compiler-cjs/test.ts.compiled | 9 ++++ test/compiler-fixtures/js.fixture.js | 12 +++++ test/compiler-fixtures/ts.fixture.js | 8 ++++ test/integration/compiler-cjs.spec.js | 64 +++++++++++++++++++++++++++ 9 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 test/compiler-cjs/test.js create mode 100644 test/compiler-cjs/test.js.compiled create mode 100644 test/compiler-cjs/test.ts create mode 100644 test/compiler-cjs/test.ts.compiled create mode 100644 test/compiler-fixtures/js.fixture.js create mode 100644 test/compiler-fixtures/ts.fixture.js create mode 100644 test/integration/compiler-cjs.spec.js diff --git a/eslint.config.js b/eslint.config.js index 92685168d7..026dffc5df 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -61,7 +61,8 @@ module.exports = [ 'lib/nodejs/esm-utils.js', 'rollup.config.js', 'scripts/*.mjs', - 'scripts/pick-from-package-json.js' + 'scripts/pick-from-package-json.js', + 'test/compiler-cjs/test.js' ], languageOptions: { sourceType: 'module' diff --git a/lib/nodejs/esm-utils.js b/lib/nodejs/esm-utils.js index 539d4aa124..6c43d4ccfc 100644 --- a/lib/nodejs/esm-utils.js +++ b/lib/nodejs/esm-utils.js @@ -34,7 +34,11 @@ const formattedImport = async (file, esmDecorator = forward) => { exports.doImport = async file => import(file); -exports.requireOrImport = async (file, esmDecorator) => { +// 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); } @@ -81,6 +85,30 @@ exports.requireOrImport = async (file, esmDecorator) => { } }; +// 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) => { + try { + return require(file); + } catch (err) { + if ( + err.code === 'ERR_REQUIRE_ASYNC_MODULE' + ) { + // Import if the module is async. + return formattedImport(file, esmDecorator); + } + throw err; + } +} + +if (process.features.require_module) { + exports.requireOrImport = requireModule; +} else { + exports.requireOrImport = tryImportAndRequire; +} + function dealWithExports(module) { if (module.default) { return module.default; diff --git a/test/compiler-cjs/test.js b/test/compiler-cjs/test.js new file mode 100644 index 0000000000..f0032f2628 --- /dev/null +++ b/test/compiler-cjs/test.js @@ -0,0 +1,9 @@ +const obj = { foo: 'bar' }; + +describe('cjs written in esm', () => { + it('should work', () => { + expect(obj, 'to equal', { foo: 'bar' }); + }); +}); + +export const foo = 'bar'; diff --git a/test/compiler-cjs/test.js.compiled b/test/compiler-cjs/test.js.compiled new file mode 100644 index 0000000000..dedf78126d --- /dev/null +++ b/test/compiler-cjs/test.js.compiled @@ -0,0 +1,9 @@ +const obj = { foo: 'bar' }; + +describe('cjs written in esm', () => { + it('should work', () => { + expect(obj, 'to equal', { foo: 'bar' }); + }); +}); + +module.exports.foo = 'bar'; diff --git a/test/compiler-cjs/test.ts b/test/compiler-cjs/test.ts new file mode 100644 index 0000000000..eac5d13082 --- /dev/null +++ b/test/compiler-cjs/test.ts @@ -0,0 +1,9 @@ +const obj: unknown = { foo: 'bar' }; + +describe('cts written in esm', () => { + it('should work', () => { + expect(obj, 'to equal', { foo: 'bar' }); + }); +}); + +export const foo = 'bar'; diff --git a/test/compiler-cjs/test.ts.compiled b/test/compiler-cjs/test.ts.compiled new file mode 100644 index 0000000000..1ceeb77192 --- /dev/null +++ b/test/compiler-cjs/test.ts.compiled @@ -0,0 +1,9 @@ +const obj = { foo: 'bar' }; + +describe('cts written in esm', () => { + it('should work', () => { + expect(obj, 'to equal', { foo: 'bar' }); + }); +}); + +module.exports.foo = 'bar'; diff --git a/test/compiler-fixtures/js.fixture.js b/test/compiler-fixtures/js.fixture.js new file mode 100644 index 0000000000..556edb7e2e --- /dev/null +++ b/test/compiler-fixtures/js.fixture.js @@ -0,0 +1,12 @@ +'use strict'; + +const fs = require('fs'); + +const original = require.extensions['.js']; +require.extensions['.js'] = function (module, filename) { + if (!filename.includes('compiler-cjs')) { + return original(module, filename); + } + const content = fs.readFileSync(filename + '.compiled', 'utf8'); + return module._compile(content, filename); +}; diff --git a/test/compiler-fixtures/ts.fixture.js b/test/compiler-fixtures/ts.fixture.js new file mode 100644 index 0000000000..7ea256bb35 --- /dev/null +++ b/test/compiler-fixtures/ts.fixture.js @@ -0,0 +1,8 @@ +'use strict'; + +const fs = require('fs'); + +require.extensions['.ts'] = function (module, filename) { + const content = fs.readFileSync(filename + '.compiled', 'utf8'); + return module._compile(content, filename); +}; diff --git a/test/integration/compiler-cjs.spec.js b/test/integration/compiler-cjs.spec.js new file mode 100644 index 0000000000..50e4e68c0d --- /dev/null +++ b/test/integration/compiler-cjs.spec.js @@ -0,0 +1,64 @@ +'use strict'; + +var exec = require('node:child_process').exec; +var path = require('node:path'); + +describe('support CJS require.extension compilers with esm syntax', function () { + it('should support .js extension', function (done) { + exec( + '"' + + process.execPath + + '" "' + + path.join('bin', 'mocha') + + '" -R json --require test/compiler-fixtures/js.fixture "test/compiler-cjs/*.js"', + {cwd: path.join(__dirname, '..', '..')}, + function (error, stdout) { + if (error && !stdout) { + return done(error); + } + var results = JSON.parse(stdout); + expect(results, 'to have property', 'tests'); + var titles = []; + for (var index = 0; index < results.tests.length; index += 1) { + expect(results.tests[index], 'to have property', 'fullTitle'); + titles.push(results.tests[index].fullTitle); + } + expect( + titles, + 'to contain', + 'cjs written in esm should work', + ).and('to have length', 1); + done(); + } + ); + }); + + it('should support .ts extension', function (done) { + exec( + '"' + + process.execPath + + '" "' + + path.join('bin', 'mocha') + + '" -R json --require test/compiler-fixtures/ts.fixture "test/compiler-cjs/*.ts"', + {cwd: path.join(__dirname, '..', '..')}, + function (error, stdout) { + if (error && !stdout) { + return done(error); + } + var results = JSON.parse(stdout); + expect(results, 'to have property', 'tests'); + var titles = []; + for (var index = 0; index < results.tests.length; index += 1) { + expect(results.tests[index], 'to have property', 'fullTitle'); + titles.push(results.tests[index].fullTitle); + } + expect( + titles, + 'to contain', + 'cts written in esm should work', + ).and('to have length', 1); + done(); + } + ); + }); +});