Skip to content

Commit 983a5eb

Browse files
fix: createRequire leaking into CommonJS (#3)
fixes #103
1 parent 4d5542e commit 983a5eb

8 files changed

+153
-86
lines changed

package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"files": [
2424
"dist"
2525
],
26-
"bin": "./dist/cli.js",
26+
"type": "module",
27+
"bin": "./dist/cli.mjs",
2728
"imports": {
2829
"typescript": "./src/local-typescript-loader.ts"
2930
},
@@ -57,13 +58,15 @@
5758
"@rollup/pluginutils": "^5.1.4",
5859
"esbuild": "^0.24.2",
5960
"magic-string": "^0.30.17",
60-
"rollup": "^4.29.1"
61+
"rollup": "^4.29.1",
62+
"rollup-pluginutils": "^2.8.2"
6163
},
6264
"devDependencies": {
6365
"@types/node": "^22.10.2",
6466
"@types/react": "^18.3.5",
6567
"clean-pkg-json": "^1.2.0",
6668
"cleye": "^1.3.2",
69+
"estree-walker": "^3.0.3",
6770
"execa": "9.3.0",
6871
"fs-fixture": "^2.6.0",
6972
"get-node": "^15.0.1",

pnpm-lock.yaml

+25
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/utils/get-rollup-configs.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ import dynamicImportVars from '@rollup/plugin-dynamic-import-vars';
1111
import type { PackageJson } from 'type-fest';
1212
import type { TsConfigResult } from 'get-tsconfig';
1313
import type { ExportEntry, AliasMap } from '../types.js';
14-
import { isFormatEsm, createRequire } from './rollup-plugins/create-require.js';
1514
import { esbuildTransform, esbuildMinify } from './rollup-plugins/esbuild.js';
1615
import { externalizeNodeBuiltins } from './rollup-plugins/externalize-node-builtins.js';
1716
import { patchBinary } from './rollup-plugins/patch-binary.js';
1817
import { resolveTypescriptMjsCts } from './rollup-plugins/resolve-typescript-mjs-cjs.js';
1918
import { resolveTsconfigPaths } from './rollup-plugins/resolve-tsconfig-paths.js';
2019
import { stripHashbang } from './rollup-plugins/strip-hashbang.js';
20+
import { esmInjectCreateRequire } from './rollup-plugins/esm-inject-create-require.js';
2121
import { getExternalDependencies } from './parse-package-json/get-external-dependencies.js';
2222

2323
type Options = {
@@ -134,10 +134,13 @@ const getConfig = {
134134
: []
135135
),
136136
stripHashbang(),
137-
commonjs(),
138137
json(),
139138
esbuildTransform(esbuildConfig),
140-
createRequire(),
139+
commonjs({
140+
ignoreDynamicRequires: true,
141+
extensions: ['.js', '.ts', '.jsx', '.tsx'],
142+
transformMixedEsModules: true,
143+
}),
141144
dynamicImportVars({
142145
warnOnError: true,
143146
}),
@@ -147,6 +150,7 @@ const getConfig = {
147150
: []
148151
),
149152
patchBinary(executablePaths),
153+
esmInjectCreateRequire(),
150154
],
151155
output: [] as unknown as Output,
152156
external: [] as (string | RegExp)[],
@@ -253,9 +257,6 @@ export const getRollupConfigs = async (
253257
format: exportEntry.type,
254258
chunkFileNames: `[name]-[hash]${extension}`,
255259
sourcemap: flags.sourcemap,
256-
plugins: [
257-
isFormatEsm(exportEntry.type === 'module'),
258-
],
259260

260261
/**
261262
* Preserve source path in dist path

src/utils/rollup-plugins/create-require.ts

-69
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import MagicString from 'magic-string';
2+
import { attachScopes, type AttachedScope } from 'rollup-pluginutils';
3+
import { walk } from 'estree-walker';
4+
import type { Plugin } from 'rollup';
5+
6+
export const esmInjectCreateRequire = (): Plugin => {
7+
const createRequire = 'import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);';
8+
9+
return {
10+
name: 'esmInjectCreateRequire',
11+
renderChunk(code, _chunk, options) {
12+
if (
13+
options.format !== 'es'
14+
|| !/\brequire\b/.test(code)
15+
) {
16+
return null;
17+
}
18+
19+
const ast = this.parse(code);
20+
let currentScope = attachScopes(ast, 'scope');
21+
let injectionNeeded = false;
22+
23+
walk(ast, {
24+
enter(node) {
25+
// Not all nodes have scopes
26+
if (node.scope) {
27+
currentScope = node.scope as AttachedScope;
28+
}
29+
if (
30+
node.type === 'Identifier'
31+
&& node.name === 'require'
32+
// If the current scope (or its parents) does not contain 'require'
33+
&& !currentScope.contains('require')
34+
) {
35+
injectionNeeded = true;
36+
37+
// No need to continue if one instance is found
38+
this.skip();
39+
}
40+
},
41+
leave: (node) => {
42+
if (node.scope) {
43+
currentScope = currentScope.parent!;
44+
}
45+
},
46+
});
47+
48+
if (!injectionNeeded) {
49+
return null;
50+
}
51+
52+
const magicString = new MagicString(code);
53+
magicString.prepend(createRequire);
54+
return {
55+
code: magicString.toString(),
56+
map: magicString.generateMap({ hires: true }),
57+
};
58+
},
59+
};
60+
};

tests/specs/builds/minification.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { pathToFileURL } from 'node:url';
12
import { testSuite, expect } from 'manten';
23
import { createFixture } from 'fs-fixture';
34
import { pkgroll } from '../../utils.js';
@@ -30,10 +31,8 @@ export default testSuite(({ describe }, nodePath: string) => {
3031
expect(content).not.toMatch('exports.foo=foo');
3132

3233
// Minification should preserve name
33-
expect(
34-
// eslint-disable-next-line @typescript-eslint/no-require-imports
35-
require(fixture.getPath('dist/target.js')).functionName,
36-
).toBe('preservesName');
34+
const { functionName } = await import(pathToFileURL(fixture.getPath('dist/target.js')).toString());
35+
expect(functionName).toBe('preservesName');
3736
});
3837
});
3938
});

tests/specs/builds/output-module.ts

+52-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import fs from 'node:fs/promises';
2+
import { pathToFileURL } from 'node:url';
23
import { testSuite, expect } from 'manten';
34
import { createFixture } from 'fs-fixture';
45
import { pkgroll } from '../../utils.js';
@@ -181,7 +182,7 @@ export default testSuite(({ describe }, nodePath: string) => {
181182
expect(content).toMatch('export { sayHello }');
182183
});
183184

184-
test('require() works in esm', async () => {
185+
test('require() gets converted to import in esm', async () => {
185186
await using fixture = await createFixture({
186187
...packageFixture(),
187188
'package.json': createPackageJson({
@@ -202,7 +203,8 @@ export default testSuite(({ describe }, nodePath: string) => {
202203
expect(js).not.toMatch('createRequire');
203204

204205
const mjs = await fixture.readFile('dist/require.mjs', 'utf8');
205-
expect(mjs).toMatch('createRequire');
206+
expect(mjs).not.toMatch('require(');
207+
expect(mjs).toMatch(/import . from"fs"/);
206208
});
207209

208210
test('conditional require() no side-effects', async () => {
@@ -243,9 +245,55 @@ export default testSuite(({ describe }, nodePath: string) => {
243245

244246
const content = await fixture.readFile('dist/conditional-require.mjs', 'utf8');
245247
expect(content).not.toMatch('\tconsole.log(\'side effect\');');
248+
expect(content).not.toMatch('require(');
249+
expect(content).toMatch('"development"');
250+
});
251+
252+
describe('injects createRequire', ({ test }) => {
253+
test('dynamic require should get a createRequire', async () => {
254+
await using fixture = await createFixture({
255+
'src/dynamic-require.ts': 'require((() => \'fs\')());',
256+
'package.json': createPackageJson({
257+
main: './dist/dynamic-require.mjs',
258+
}),
259+
});
260+
261+
const pkgrollProcess = await pkgroll([], {
262+
cwd: fixture.path,
263+
nodePath,
264+
});
265+
266+
expect(pkgrollProcess.exitCode).toBe(0);
267+
expect(pkgrollProcess.stderr).toBe('');
268+
269+
const content = await fixture.readFile('dist/dynamic-require.mjs', 'utf8');
270+
expect(content).toMatch('createRequire');
271+
expect(content).toMatch('(import.meta.url)');
272+
273+
// Shouldn't throw
274+
await import(pathToFileURL(fixture.getPath('dist/dynamic-require.mjs')).toString());
275+
});
246276

247-
const [, createRequireMangledVariable] = content.toString().match(/createRequire as (\w+)/)!;
248-
expect(content).not.toMatch(`${createRequireMangledVariable}(`);
277+
test('defined require should not get a createRequire', async () => {
278+
await using fixture = await createFixture({
279+
'src/dynamic-require.ts': 'const require = ()=>{}; require((() => \'fs\')());',
280+
'package.json': createPackageJson({
281+
main: './dist/dynamic-require.mjs',
282+
}),
283+
});
284+
285+
const pkgrollProcess = await pkgroll([], {
286+
cwd: fixture.path,
287+
nodePath,
288+
});
289+
290+
expect(pkgrollProcess.exitCode).toBe(0);
291+
expect(pkgrollProcess.stderr).toBe('');
292+
293+
const content = await fixture.readFile('dist/dynamic-require.mjs', 'utf8');
294+
expect(content).not.toMatch('createRequire');
295+
expect(content).not.toMatch('(import.meta.url)');
296+
});
249297
});
250298

251299
test('dynamic imports', async () => {

tests/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import path from 'node:path';
22
import { execa, type Options } from 'execa';
33

4-
const pkgrollBinPath = path.resolve('./dist/cli.js');
4+
const pkgrollBinPath = path.resolve('./dist/cli.mjs');
55

66
export const pkgroll = async (
77
cliArguments: string[],

0 commit comments

Comments
 (0)