Skip to content

Commit d6b0a16

Browse files
bartlomiejuclaude
andauthored
fix: improve CJS-to-ESM transform for better npm compatibility (#3697)
## Summary Fixes several issues in the Fresh Vite plugin's CommonJS-to-ESM Babel transform (`commonjs.ts`) that caused npm packages to break during builds while working fine in dev mode. - **Fix `.mts`/`.cts` extension detection** — `.cts` was incorrectly in the ESM branch (should be `.mts`), causing TypeScript CJS files to skip the transform - **Handle `require.resolve()`** — injects `createRequire(import.meta.url)` polyfill so `require.resolve()` calls work in ESM output (#3619) - **Polyfill `__dirname`/`__filename`** — injects `fileURLToPath`/`dirname` based polyfill when these CJS globals are used - **Use `var` instead of `let` for `_default`** — avoids TDZ errors when Rollup reorders declarations in bundled output (#3653) - **Track `module.exports.X` as named exports** — the MemberExpression visitor now handles both `exports.X` and `module.exports.X` patterns Addresses: #3619, #3653 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9875146 commit d6b0a16

2 files changed

Lines changed: 206 additions & 11 deletions

File tree

packages/plugin-vite/src/plugins/patches/commonjs.ts

Lines changed: 105 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ export function cjsPlugin(
1414
const ALIASED = "aliased";
1515
const REEXPORT = "re-export";
1616
const NEEDS_REQUIRE_IMPORT = "needsRequireImport";
17+
const NEEDS_DIRNAME_IMPORT = "needsDirnameImport";
1718
const IS_ESM = "isESM";
1819

1920
return {
2021
name: "fresh-cjs-esm",
2122
pre(file) {
2223
const filename = file.opts.filename;
2324
if (filename) {
24-
if (filename.endsWith(".mjs") || filename.endsWith(".cts")) {
25+
if (filename.endsWith(".mjs") || filename.endsWith(".mts")) {
2526
this.set(IS_ESM, true);
2627
} else if (filename.endsWith(".cjs") || filename.endsWith(".cts")) {
2728
this.set(IS_ESM, false);
@@ -112,6 +113,64 @@ export function cjsPlugin(
112113
);
113114
}
114115

116+
const needsDirnameImport = state.get(NEEDS_DIRNAME_IMPORT);
117+
if (needsDirnameImport) {
118+
// Inject:
119+
// ```ts
120+
// import { fileURLToPath as __cjs_fileURLToPath } from "node:url";
121+
// import { dirname as __cjs_dirname } from "node:path";
122+
// const __filename = __cjs_fileURLToPath(import.meta.url);
123+
// const __dirname = __cjs_dirname(__filename);
124+
// ```
125+
const fileURLToPathId = t.identifier("__cjs_fileURLToPath");
126+
const dirnameId = t.identifier("__cjs_dirname");
127+
const importMetaUrl = t.memberExpression(
128+
t.metaProperty(
129+
t.identifier("import"),
130+
t.identifier("meta"),
131+
),
132+
t.identifier("url"),
133+
);
134+
135+
path.unshiftContainer(
136+
"body",
137+
t.variableDeclaration("var", [
138+
t.variableDeclarator(
139+
t.identifier("__dirname"),
140+
t.callExpression(dirnameId, [t.identifier("__filename")]),
141+
),
142+
]),
143+
);
144+
path.unshiftContainer(
145+
"body",
146+
t.variableDeclaration("var", [
147+
t.variableDeclarator(
148+
t.identifier("__filename"),
149+
t.callExpression(fileURLToPathId, [importMetaUrl]),
150+
),
151+
]),
152+
);
153+
path.unshiftContainer(
154+
"body",
155+
t.importDeclaration(
156+
[t.importSpecifier(dirnameId, t.identifier("dirname"))],
157+
t.stringLiteral("node:path"),
158+
),
159+
);
160+
path.unshiftContainer(
161+
"body",
162+
t.importDeclaration(
163+
[
164+
t.importSpecifier(
165+
fileURLToPathId,
166+
t.identifier("fileURLToPath"),
167+
),
168+
],
169+
t.stringLiteral("node:url"),
170+
),
171+
);
172+
}
173+
115174
if (reexport !== null) {
116175
path.unshiftContainer(
117176
"body",
@@ -246,9 +305,11 @@ export function cjsPlugin(
246305
if (exported.size > 0 || exportedNs.size > 0 || hasEsModule) {
247306
const id = path.scope.generateUidIdentifier("__default");
248307

308+
// Use `var` instead of `const` to avoid TDZ errors when
309+
// Rollup reorders declarations in the bundled output.
249310
path.pushContainer(
250311
"body",
251-
t.variableDeclaration("let", [
312+
t.variableDeclaration("var", [
252313
t.variableDeclarator(
253314
id,
254315
),
@@ -303,6 +364,8 @@ export function cjsPlugin(
303364
const mapped = mappedNs[i];
304365

305366
const key = path.scope.generateUid("k");
367+
// Only spread namespace properties when the module has no
368+
// explicit default export (i.e. "default" not in exports).
306369
path.pushContainer(
307370
"body",
308371
t.ifStatement(
@@ -430,6 +493,18 @@ export function cjsPlugin(
430493
return;
431494
}
432495

496+
// Handle require.resolve() by injecting createRequire
497+
if (
498+
t.isMemberExpression(path.node.callee) &&
499+
t.isIdentifier(path.node.callee.object) &&
500+
path.node.callee.object.name === "require" &&
501+
t.isIdentifier(path.node.callee.property) &&
502+
path.node.callee.property.name === "resolve"
503+
) {
504+
state.set(NEEDS_REQUIRE_IMPORT, true);
505+
return;
506+
}
507+
433508
if (
434509
t.isIdentifier(path.node.callee) &&
435510
path.node.callee.name === "require"
@@ -557,15 +632,21 @@ export function cjsPlugin(
557632
exit(path, state) {
558633
if (state.get(IS_ESM)) return;
559634
if (
560-
t.isIdentifier(path.node.object) &&
561-
path.node.object.name === "exports" &&
562-
t.isIdentifier(path.node.property)
635+
t.isIdentifier(path.node.property) &&
636+
path.node.property.name !== "__esModule"
563637
) {
564-
const name = t.cloneNode(path.node.property);
565-
566-
if (name.name === "__esModule") return;
567-
568-
state.get(EXPORTED).add(name.name);
638+
// Track both `exports.X` and `module.exports.X`
639+
if (
640+
t.isIdentifier(path.node.object) &&
641+
path.node.object.name === "exports"
642+
) {
643+
state.get(EXPORTED).add(path.node.property.name);
644+
} else if (
645+
t.isMemberExpression(path.node.object) &&
646+
isModuleExports(t, path.node.object)
647+
) {
648+
state.get(EXPORTED).add(path.node.property.name);
649+
}
569650
}
570651
},
571652
},
@@ -780,6 +861,20 @@ export function cjsPlugin(
780861
path.replaceWith(t.cloneNode(path.node.alternate, true));
781862
}
782863
},
864+
Identifier(path, state) {
865+
if (state.get(IS_ESM)) return;
866+
867+
const name = path.node.name;
868+
if (name !== "__dirname" && name !== "__filename") return;
869+
870+
// Skip if this is already a declaration (e.g. our own polyfill)
871+
if (
872+
path.parentPath?.isVariableDeclarator() &&
873+
path.parentPath.get("id") === path
874+
) return;
875+
876+
state.set(NEEDS_DIRNAME_IMPORT, true);
877+
},
783878
AssignmentExpression(path, state) {
784879
if (state.get(IS_ESM)) return;
785880

packages/plugin-vite/src/plugins/patches/commonjs_test.ts

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Object.defineProperty(exports, "__esModule", {
2929
value: true
3030
});`;
3131

32-
const DEFAULT_EXPORT = `let _default;
32+
const DEFAULT_EXPORT = `var _default;
3333
if (typeof exports === "object" && exports !== null && "default" in exports) {
3434
_default = exports.default;
3535
} else {
@@ -715,6 +715,92 @@ const node_events_1 = __importDefault({
715715
});
716716
});
717717

718+
// --- New tests for CJS transform fixes ---
719+
720+
Deno.test("commonjs - require.resolve injects createRequire", () => {
721+
runTest({
722+
input: `var resolved = require.resolve("some-package");`,
723+
expected: `${IMPORT_REQUIRE}
724+
var resolved = require.resolve("some-package");`,
725+
});
726+
});
727+
728+
Deno.test("commonjs - require.resolve with require() both inject createRequire once", () => {
729+
runTest({
730+
input: `var resolved = require.resolve("some-package");
731+
if (true) {
732+
var mod = require("other");
733+
}`,
734+
expected: `${IMPORT_REQUIRE}
735+
var resolved = require.resolve("some-package");
736+
if (true) {
737+
var mod = require("other");
738+
}`,
739+
});
740+
});
741+
742+
Deno.test("commonjs - .mts file treated as ESM", () => {
743+
runTest({
744+
filename: "foo.mts",
745+
input: `export const x = 1;`,
746+
expected: `export const x = 1;`,
747+
});
748+
});
749+
750+
Deno.test("commonjs - .cts file treated as CJS", () => {
751+
runTest({
752+
filename: "foo.cts",
753+
input: `module.exports = 42;`,
754+
expected: `${INIT}
755+
module.exports = 42;
756+
${DEFAULT_EXPORT}
757+
${DEFAULT_EXPORT_END}`,
758+
});
759+
});
760+
761+
Deno.test("commonjs - __dirname and __filename polyfill", () => {
762+
const DIRNAME_IMPORT =
763+
`import { fileURLToPath as __cjs_fileURLToPath } from "node:url";
764+
import { dirname as __cjs_dirname } from "node:path";
765+
var __filename = __cjs_fileURLToPath(import.meta.url);
766+
var __dirname = __cjs_dirname(__filename);`;
767+
runTest({
768+
input: `var dir = __dirname;
769+
var file = __filename;
770+
module.exports = { dir: dir, file: file };`,
771+
expected: `${INIT}
772+
${DIRNAME_IMPORT}
773+
var dir = __dirname;
774+
var file = __filename;
775+
module.exports = {
776+
dir: dir,
777+
file: file
778+
};
779+
var _dir = exports.dir;
780+
var _file = exports.file;
781+
export { _dir as dir, _file as file };
782+
${DEFAULT_EXPORT}
783+
${DEFAULT_EXPORT_END}`,
784+
});
785+
});
786+
787+
Deno.test("commonjs - module.exports.X tracked as named export", () => {
788+
runTest({
789+
input: `module.exports = function parse() {};
790+
module.exports.parse = module.exports;
791+
module.exports.stringify = function stringify() {};`,
792+
expected: `${INIT}
793+
module.exports = function parse() {};
794+
module.exports.parse = module.exports;
795+
module.exports.stringify = function stringify() {};
796+
var _parse = exports.parse;
797+
var _stringify = exports.stringify;
798+
export { _parse as parse, _stringify as stringify };
799+
${DEFAULT_EXPORT}
800+
${DEFAULT_EXPORT_END}`,
801+
});
802+
});
803+
718804
Deno.test("commonjs imitating esm - default export exists", () => {
719805
runTest({
720806
input: `module.exports = {
@@ -733,3 +819,17 @@ ${DEFAULT_EXPORT}
733819
${DEFAULT_EXPORT_END}`,
734820
});
735821
});
822+
823+
Deno.test("commonjs - primitive module.exports with namespace re-export guards assignment", () => {
824+
runTest({
825+
input: `__exportStar(require("./utils"), exports);
826+
module.exports = "RFC3986";`,
827+
expected: `${INIT}
828+
import * as _ns from "./utils";
829+
export * from "./utils";
830+
module.exports = "RFC3986";
831+
${DEFAULT_EXPORT}
832+
if (typeof exports === "object" && exports !== null && !("default" in exports)) for (var _k in _ns) if (_k !== "default" && _k !== "__esModule" && Object.prototype.hasOwnProperty.call(_ns, _k)) _default[_k] = _ns[_k];
833+
${DEFAULT_EXPORT_END}`,
834+
});
835+
});

0 commit comments

Comments
 (0)