Skip to content

Commit a084a2c

Browse files
fix: unable to load CommonJS in vite plugin (#3204)
Vite has an internal check that limits CommonJS detection to only bare specifier. This doesn't work well together with us resolving npm modules to absolute file paths. https://github.com/vitejs/vite/blob/main/packages/vite/src/node/ssr/fetchModule.ts#L46 I've tried re-using our own instance of `@rollup/plugin-commonjs` or `esbuild` but couldn't get these to work in that setup. The easiest way seems to be to just write our own CJS -> ESM translation layer. With a little luck since the Deno ecosystem is heavy into ESM we might get away with this. In either way this removes one of the two blockers to using the vite plugin.
1 parent 3d1eb2c commit a084a2c

7 files changed

Lines changed: 492 additions & 79 deletions

File tree

deno.lock

Lines changed: 90 additions & 79 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
exports.value = "ok";
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { value } from "../../fixtures/commonjs_mod.cjs";
2+
3+
export default function Page() {
4+
return <h1>{value}</h1>;
5+
}

packages/plugin-vite/deno.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
"demo:start": "cd demo && deno serve -A _fresh/server/server-entry.mjs"
1717
},
1818
"imports": {
19+
"@babel/core": "npm:@babel/core@^7.28.0",
1920
"@deno/loader": "jsr:@deno/loader@^0.3.2",
2021
"@prefresh/vite": "npm:@prefresh/vite@^2.4.8",
22+
"@types/babel__core": "npm:@types/babel__core@^7.20.5",
2123
"@types/node": "npm:@types/node@^24.1.0",
2224
"preact": "npm:preact@^10.26.9",
2325
"vite": "npm:vite@^7.0.6",

packages/plugin-vite/src/mod.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { devServer } from "./plugins/dev_server.ts";
1212
import { buildIdPlugin } from "./plugins/build_id.ts";
1313
import { clientSnapshot } from "./plugins/client_snapshot.ts";
1414
import { serverSnapshot } from "./plugins/server_snapshot.ts";
15+
import { commonjs } from "./plugins/commonjs.ts";
1516

1617
export function fresh(config?: FreshViteConfig): Plugin[] {
1718
const fConfig: ResolvedFreshViteConfig = {
@@ -108,6 +109,7 @@ export function fresh(config?: FreshViteConfig): Plugin[] {
108109
fConfig.routeDir = pathWithRoot(fConfig.routeDir, config.root);
109110
},
110111
},
112+
commonjs(),
111113
serverEntryPlugin(fConfig),
112114
...serverSnapshot(fConfig),
113115
clientEntryPlugin(),
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import type { Plugin } from "vite";
2+
import * as babel from "@babel/core";
3+
4+
export function commonjs(): Plugin {
5+
return {
6+
name: "cjs",
7+
applyToEnvironment() {
8+
return true;
9+
},
10+
transform(code, id) {
11+
const res = babel.transformSync(code, {
12+
filename: id,
13+
babelrc: false,
14+
plugins: [cjsPlugin],
15+
});
16+
17+
if (res?.code) {
18+
return {
19+
code: res.code,
20+
map: res.map,
21+
};
22+
}
23+
},
24+
};
25+
}
26+
27+
export function cjsPlugin(
28+
{ types: t }: { types: typeof babel.types },
29+
): babel.PluginObj {
30+
const HAS_ES_MODULE = "esModule";
31+
const REQUIRE_CALLS = "requireCalls";
32+
const ROOT_SCOPE = "rootScope";
33+
34+
return {
35+
name: "fresh-cjs-esm",
36+
visitor: {
37+
Program: {
38+
enter(path, state) {
39+
state.set(ROOT_SCOPE, path.scope);
40+
},
41+
exit(path, state) {
42+
const body = path.get("body");
43+
44+
const requires = state.get(REQUIRE_CALLS);
45+
if (requires !== undefined) {
46+
for (let i = 0; i < requires.length; i++) {
47+
const { specifier, id } = requires[i];
48+
path.unshiftContainer(
49+
"body",
50+
t.importDeclaration(
51+
[t.importNamespaceSpecifier(id)],
52+
specifier,
53+
),
54+
);
55+
}
56+
}
57+
58+
if (body.length === 0 && state.get(HAS_ES_MODULE)) {
59+
path.pushContainer("body", t.exportNamedDeclaration(null));
60+
}
61+
},
62+
},
63+
CallExpression(path, state) {
64+
if (isObjEsModuleFlag(t, path.node)) {
65+
state.set(HAS_ES_MODULE, true);
66+
path.remove();
67+
return;
68+
}
69+
70+
if (
71+
t.isIdentifier(path.node.callee) &&
72+
path.node.callee.name === "require"
73+
) {
74+
const root = state.get(ROOT_SCOPE);
75+
const id = root.generateUidIdentifier("mod");
76+
77+
const mods = state.get(REQUIRE_CALLS) ?? [];
78+
state.set(REQUIRE_CALLS, mods);
79+
80+
mods.push({
81+
id,
82+
specifier: t.cloneNode(path.node.arguments[0], true),
83+
});
84+
85+
if (
86+
path.parentPath?.isVariableDeclarator() &&
87+
path.parentPath?.get("id").isIdentifier() ||
88+
path.parentPath?.isCallExpression()
89+
) {
90+
path.replaceWith(
91+
t.logicalExpression(
92+
"??",
93+
t.memberExpression(
94+
t.cloneNode(id, true),
95+
t.identifier("default"),
96+
),
97+
t.cloneNode(id, true),
98+
),
99+
);
100+
return;
101+
}
102+
103+
path.replaceWith(t.cloneNode(id, true));
104+
}
105+
},
106+
ExpressionStatement(path, state) {
107+
const expr = path.get("expression");
108+
if (expr.isAssignmentExpression()) {
109+
const left = expr.get("left");
110+
111+
if (isEsModuleFlag(t, expr.node)) {
112+
state.set(HAS_ES_MODULE, true);
113+
path.remove();
114+
} else if (left.isMemberExpression()) {
115+
if (isModuleExports(t, left.node)) {
116+
const right = t.cloneNode(expr.node.right, true);
117+
118+
path.replaceWith(t.exportDefaultDeclaration(right));
119+
} else {
120+
const named = getExportsAssignName(t, left.node);
121+
if (named === null) return;
122+
123+
const right = t.cloneNode(expr.node.right, true);
124+
125+
if (named === "default") {
126+
path.replaceWith(t.exportDefaultDeclaration(right));
127+
} else {
128+
path.scope.rename(named);
129+
path.replaceWith(
130+
t.exportNamedDeclaration(
131+
t.variableDeclaration("let", [
132+
t.variableDeclarator(t.identifier(named), right),
133+
]),
134+
),
135+
);
136+
}
137+
}
138+
}
139+
}
140+
},
141+
},
142+
};
143+
}
144+
145+
function isModuleExports(
146+
t: typeof babel.types,
147+
node: babel.types.MemberExpression,
148+
): boolean {
149+
return t.isIdentifier(node.object) && node.object.name === "module" &&
150+
t.isIdentifier(node.property) && node.property.name === "exports";
151+
}
152+
153+
function getExportsAssignName(
154+
t: typeof babel.types,
155+
node: babel.types.MemberExpression,
156+
): string | null {
157+
if (
158+
(t.isMemberExpression(node.object) &&
159+
isModuleExports(t, node.object) ||
160+
t.isIdentifier(node.object) && node.object.name === "exports") &&
161+
t.isIdentifier(node.property)
162+
) {
163+
return node.property.name;
164+
}
165+
166+
return null;
167+
}
168+
169+
/**
170+
* Detect `exports.__esModule = true;`
171+
*/
172+
function isEsModuleFlag(
173+
t: typeof babel.types,
174+
node: babel.types.AssignmentExpression,
175+
): boolean {
176+
if (!t.isMemberExpression(node.left)) return false;
177+
178+
const { left, right } = node;
179+
return (t.isMemberExpression(left.object) &&
180+
isModuleExports(t, left.object) ||
181+
t.isIdentifier(left.object) && left.object.name === "exports") &&
182+
t.isIdentifier(left.property) && left.property.name === "__esModule" &&
183+
t.isBooleanLiteral(right);
184+
}
185+
186+
/**
187+
* Check for `Object.defineProperty(exports, '__esModule', { value: true })`
188+
*/
189+
function isObjEsModuleFlag(
190+
t: typeof babel.types,
191+
node: babel.types.CallExpression,
192+
): boolean {
193+
return t.isMemberExpression(node.callee) &&
194+
t.isIdentifier(node.callee.object) &&
195+
node.callee.object.name === "Object" &&
196+
t.isIdentifier(node.callee.property) &&
197+
node.callee.property.name === "defineProperty" &&
198+
node.arguments.length === 3 &&
199+
(t.isMemberExpression(node.arguments[0]) &&
200+
isModuleExports(t, node.arguments[0]) ||
201+
t.isIdentifier(node.arguments[0]) &&
202+
node.arguments[0].name === "exports") &&
203+
t.isStringLiteral(node.arguments[1]) &&
204+
node.arguments[1].value === "__esModule" &&
205+
t.isObjectExpression(node.arguments[2]);
206+
}

0 commit comments

Comments
 (0)