Skip to content

Commit 41e24a2

Browse files
authored
feat: use require to load esm (#5366)
This allows compilers based on `require.extensions` continue to work.
1 parent e68b9a6 commit 41e24a2

File tree

9 files changed

+151
-2
lines changed

9 files changed

+151
-2
lines changed

eslint.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ module.exports = [
6161
'lib/nodejs/esm-utils.js',
6262
'rollup.config.js',
6363
'scripts/*.mjs',
64-
'scripts/pick-from-package-json.js'
64+
'scripts/pick-from-package-json.js',
65+
'test/compiler-cjs/test.js'
6566
],
6667
languageOptions: {
6768
sourceType: 'module'

lib/nodejs/esm-utils.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ const formattedImport = async (file, esmDecorator = forward) => {
3434

3535
exports.doImport = async file => import(file);
3636

37-
exports.requireOrImport = async (file, esmDecorator) => {
37+
// When require(esm) is not available, we need to use `import()` to load ESM modules.
38+
// In this case, CJS modules are loaded using `import()` as well. When Node.js' builtin
39+
// TypeScript support is enabled, `.ts` files are also loaded using `import()`, and
40+
// compilers based on `require.extensions` are omitted.
41+
const tryImportAndRequire = async (file, esmDecorator) => {
3842
if (path.extname(file) === '.mjs') {
3943
return formattedImport(file, esmDecorator);
4044
}
@@ -81,6 +85,30 @@ exports.requireOrImport = async (file, esmDecorator) => {
8185
}
8286
};
8387

88+
// Utilize Node.js' require(esm) feature to load ESM modules
89+
// and CJS modules. This keeps the require() features like `require.extensions`
90+
// and `require.cache` effective, while allowing us to load ESM modules
91+
// and CJS modules in the same way.
92+
const requireModule = async (file, esmDecorator) => {
93+
try {
94+
return require(file);
95+
} catch (err) {
96+
if (
97+
err.code === 'ERR_REQUIRE_ASYNC_MODULE'
98+
) {
99+
// Import if the module is async.
100+
return formattedImport(file, esmDecorator);
101+
}
102+
throw err;
103+
}
104+
}
105+
106+
if (process.features.require_module) {
107+
exports.requireOrImport = requireModule;
108+
} else {
109+
exports.requireOrImport = tryImportAndRequire;
110+
}
111+
84112
function dealWithExports(module) {
85113
if (module.default) {
86114
return module.default;

test/compiler-cjs/test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const obj = { foo: 'bar' };
2+
3+
describe('cjs written in esm', () => {
4+
it('should work', () => {
5+
expect(obj, 'to equal', { foo: 'bar' });
6+
});
7+
});
8+
9+
export const foo = 'bar';

test/compiler-cjs/test.js.compiled

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const obj = { foo: 'bar' };
2+
3+
describe('cjs written in esm', () => {
4+
it('should work', () => {
5+
expect(obj, 'to equal', { foo: 'bar' });
6+
});
7+
});
8+
9+
module.exports.foo = 'bar';

test/compiler-cjs/test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const obj: unknown = { foo: 'bar' };
2+
3+
describe('cts written in esm', () => {
4+
it('should work', () => {
5+
expect(obj, 'to equal', { foo: 'bar' });
6+
});
7+
});
8+
9+
export const foo = 'bar';

test/compiler-cjs/test.ts.compiled

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const obj = { foo: 'bar' };
2+
3+
describe('cts written in esm', () => {
4+
it('should work', () => {
5+
expect(obj, 'to equal', { foo: 'bar' });
6+
});
7+
});
8+
9+
module.exports.foo = 'bar';

test/compiler-fixtures/js.fixture.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
5+
const original = require.extensions['.js'];
6+
require.extensions['.js'] = function (module, filename) {
7+
if (!filename.includes('compiler-cjs')) {
8+
return original(module, filename);
9+
}
10+
const content = fs.readFileSync(filename + '.compiled', 'utf8');
11+
return module._compile(content, filename);
12+
};

test/compiler-fixtures/ts.fixture.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
5+
require.extensions['.ts'] = function (module, filename) {
6+
const content = fs.readFileSync(filename + '.compiled', 'utf8');
7+
return module._compile(content, filename);
8+
};

test/integration/compiler-cjs.spec.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use strict';
2+
3+
var exec = require('node:child_process').exec;
4+
var path = require('node:path');
5+
6+
describe('support CJS require.extension compilers with esm syntax', function () {
7+
it('should support .js extension', function (done) {
8+
exec(
9+
'"' +
10+
process.execPath +
11+
'" "' +
12+
path.join('bin', 'mocha') +
13+
'" -R json --require test/compiler-fixtures/js.fixture "test/compiler-cjs/*.js"',
14+
{cwd: path.join(__dirname, '..', '..')},
15+
function (error, stdout) {
16+
if (error && !stdout) {
17+
return done(error);
18+
}
19+
var results = JSON.parse(stdout);
20+
expect(results, 'to have property', 'tests');
21+
var titles = [];
22+
for (var index = 0; index < results.tests.length; index += 1) {
23+
expect(results.tests[index], 'to have property', 'fullTitle');
24+
titles.push(results.tests[index].fullTitle);
25+
}
26+
expect(
27+
titles,
28+
'to contain',
29+
'cjs written in esm should work',
30+
).and('to have length', 1);
31+
done();
32+
}
33+
);
34+
});
35+
36+
it('should support .ts extension', function (done) {
37+
exec(
38+
'"' +
39+
process.execPath +
40+
'" "' +
41+
path.join('bin', 'mocha') +
42+
'" -R json --require test/compiler-fixtures/ts.fixture "test/compiler-cjs/*.ts"',
43+
{cwd: path.join(__dirname, '..', '..')},
44+
function (error, stdout) {
45+
if (error && !stdout) {
46+
return done(error);
47+
}
48+
var results = JSON.parse(stdout);
49+
expect(results, 'to have property', 'tests');
50+
var titles = [];
51+
for (var index = 0; index < results.tests.length; index += 1) {
52+
expect(results.tests[index], 'to have property', 'fullTitle');
53+
titles.push(results.tests[index].fullTitle);
54+
}
55+
expect(
56+
titles,
57+
'to contain',
58+
'cts written in esm should work',
59+
).and('to have length', 1);
60+
done();
61+
}
62+
);
63+
});
64+
});

0 commit comments

Comments
 (0)