Skip to content

Commit 9eaec18

Browse files
committed
Revert "refactor: remove CJS and env var Babel transforms, let Vite handle natively (#3767)"
This reverts commit b9a314b.
1 parent 76b9142 commit 9eaec18

15 files changed

Lines changed: 2050 additions & 178 deletions

File tree

deno.lock

Lines changed: 0 additions & 1 deletion
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";

packages/plugin-vite/demo/fixtures/commonjs_mod.js

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"use strict";
2+
// deno-lint-ignore no-var
3+
var __importDefault = (this && this.__importDefault) || function (mod) {
4+
return (mod && mod.__esModule) ? mod : { "default": mod };
5+
};
6+
Object.defineProperty(exports, "__esModule", { value: true });
7+
const assert_1 = __importDefault(require("assert"));
8+
(0, assert_1.default)(true);

packages/plugin-vite/demo/fixtures/maxmind.js

Lines changed: 0 additions & 2 deletions
This file was deleted.

packages/plugin-vite/demo/routes/tests/commonjs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { value } from "../../fixtures/commonjs_mod.js";
1+
import { value } from "../../fixtures/commonjs_mod.cjs";
22

33
export default function Page() {
44
return <h1>{value}</h1>;

packages/plugin-vite/demo/routes/tests/maxmind.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as maxmind from "../../fixtures/maxmind.js";
1+
import * as maxmind from "../../fixtures/maxmind.cjs";
22

33
export default function Page() {
44
// deno-lint-ignore no-console

packages/plugin-vite/src/mod.ts

Lines changed: 26 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -82,43 +82,15 @@ export function fresh(config?: FreshViteConfig): Plugin[] {
8282
});
8383

8484
let isDev = false;
85-
let freshMode = "development";
8685

8786
const plugins: Plugin[] = [
8887
{
8988
name: "fresh",
9089
sharedDuringBuild: true,
91-
async config(config, env) {
90+
config(config, env) {
9291
isDev = env.command === "serve";
93-
freshMode = isDev ? "development" : "production";
94-
95-
// Load env files early so define entries are available
96-
const root = config.root ? path.resolve(config.root) : Deno.cwd();
97-
const envDir = config.envDir ? path.resolve(root, config.envDir) : root;
98-
await loadEnvFile(path.join(envDir, ".env"));
99-
await loadEnvFile(path.join(envDir, ".env.local"));
100-
await loadEnvFile(path.join(envDir, `.env.${freshMode}`));
101-
await loadEnvFile(path.join(envDir, `.env.${freshMode}.local`));
102-
103-
// Build define map for FRESH_PUBLIC_* env vars
104-
// Replaces the Babel inlineEnvVarsPlugin with Vite's native define
105-
const envDefine: Record<string, string> = {};
106-
for (const [key, value] of Object.entries(Deno.env.toObject())) {
107-
if (key.startsWith("FRESH_PUBLIC_")) {
108-
envDefine[`process.env.${key}`] = JSON.stringify(value);
109-
envDefine[`import.meta.env.${key}`] = JSON.stringify(value);
110-
}
111-
}
11292

11393
return {
114-
define: envDefine,
115-
ssr: {
116-
// Bundle all deps in SSR so that resolve.alias
117-
// (react -> preact/compat) is applied consistently.
118-
// CJS packages are handled by the deno plugin's load
119-
// hook which wraps them in an ESM-compatible shim.
120-
noExternal: true,
121-
},
12294
server: {
12395
watch: {
12496
// Ignore temp files, editor swap files, and Vite timestamp
@@ -147,15 +119,14 @@ export function fresh(config?: FreshViteConfig): Plugin[] {
147119
"react-dom": "preact/compat",
148120
react: "preact/compat",
149121
},
122+
// Disallow externals, because it leads to duplicate
123+
// modules with `preact` vs `npm:preact@*` in the server
124+
// environment.
125+
noExternal: true,
150126
},
151-
152127
optimizeDeps: {
153-
// Disable dep optimizer because deno.ts handles all
154-
// module resolution. The optimizer causes duplicate
155-
// module instances when remote (JSR) islands resolve
156-
// deps to /@fs/ paths while the optimizer bundles to
157-
// /.vite/deps/. CJS packages in client-side islands
158-
// are handled by deno.ts's load hook.
128+
// Optimize deps somehow leads to duplicate modules or them
129+
// being placed in the wrong chunks...
159130
noDiscovery: true,
160131
},
161132

@@ -221,6 +192,14 @@ export function fresh(config?: FreshViteConfig): Plugin[] {
221192
return;
222193
}
223194

195+
// Ignore commonjs optional exports
196+
if (
197+
warning.code === "MISSING_EXPORT" &&
198+
warning.message.includes("__require")
199+
) {
200+
return;
201+
}
202+
224203
// Ignore this warnings
225204
if (warning.code === "THIS_IS_UNDEFINED") {
226205
return;
@@ -242,7 +221,7 @@ export function fresh(config?: FreshViteConfig): Plugin[] {
242221
},
243222
};
244223
},
245-
configResolved(vConfig) {
224+
async configResolved(vConfig) {
246225
// Run update check in background
247226
updateCheck(UPDATE_INTERVAL).catch(() => {});
248227

@@ -257,45 +236,19 @@ export function fresh(config?: FreshViteConfig): Plugin[] {
257236
const name = fConfig.namer.getUniqueName(specName);
258237
fConfig.islandSpecifiers.set(spec, name);
259238
});
260-
},
261-
},
262-
// Lightweight replacement for Deno.env.get() calls with FRESH_PUBLIC_*
263-
// and NODE_ENV values. Replaces the Babel inlineEnvVarsPlugin for this
264-
// pattern which can't be handled by Vite's define (it's a call expression).
265-
{
266-
name: "fresh:deno-env",
267-
sharedDuringBuild: true,
268-
applyToEnvironment() {
269-
return true;
270-
},
271-
transform: {
272-
filter: {
273-
id: /\.([tj]sx?|[mc]?[tj]s)(\?.*)?$/,
274-
},
275-
handler(code) {
276-
if (!code.includes("Deno.env.get(")) return;
277239

278-
const allEnv = Deno.env.toObject();
279-
let modified = false;
280-
const result = code.replace(
281-
/Deno\.env\.get\(\s*["']([^"']+)["']\s*\)/g,
282-
(match: string, name: string) => {
283-
if (name === "NODE_ENV") {
284-
modified = true;
285-
return JSON.stringify(freshMode);
286-
}
287-
if (name.startsWith("FRESH_PUBLIC_") && name in allEnv) {
288-
modified = true;
289-
return JSON.stringify(allEnv[name]);
290-
}
291-
return match;
292-
},
293-
);
240+
const envDir = pathWithRoot(
241+
vConfig.envDir || vConfig.root,
242+
vConfig.root,
243+
);
294244

295-
if (modified) return { code: result };
296-
},
245+
await loadEnvFile(path.join(envDir, ".env"));
246+
await loadEnvFile(path.join(envDir, ".env.local"));
247+
const mode = isDev ? "development" : "production";
248+
await loadEnvFile(path.join(envDir, `.env.${mode}`));
249+
await loadEnvFile(path.join(envDir, `.env.${mode}.local`));
297250
},
298-
} satisfies Plugin,
251+
},
299252
serverEntryPlugin(fConfig),
300253
patches(),
301254
...serverSnapshot(fConfig),

packages/plugin-vite/src/plugins/deno.ts

Lines changed: 56 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@ import {
99
import * as path from "@std/path";
1010
import * as babel from "@babel/core";
1111
import { httpAbsolute } from "./patches/http_absolute.ts";
12-
import { JSX_REG } from "../utils.ts";
12+
import { JS_REG, JSX_REG } from "../utils.ts";
1313
import { builtinModules } from "node:module";
1414

1515
// @ts-ignore Workaround for https://github.com/denoland/deno/issues/30850
1616
const { default: babelReact } = await import("@babel/preset-react");
1717

1818
const BUILTINS = new Set(builtinModules);
1919

20+
interface DenoState {
21+
type: RequestedModuleType;
22+
}
23+
2024
export function deno(): Plugin {
2125
let ssrLoader: Loader;
2226
let browserLoader: Loader;
@@ -81,21 +85,6 @@ export function deno(): Plugin {
8185
id = `${url.origin}${id}`;
8286
}
8387

84-
// Apply resolve.alias before Deno resolution so that
85-
// react -> preact/compat works even in externalized packages.
86-
// Vite normalizes alias config to { find, replacement }[] format.
87-
const aliases = this.environment?.config?.resolve?.alias;
88-
if (aliases) {
89-
const list = Array.isArray(aliases) ? aliases : [];
90-
for (const alias of list) {
91-
const find = alias.find;
92-
if (typeof find === "string" ? find === id : find?.test?.(id)) {
93-
id = typeof alias.replacement === "string" ? alias.replacement : id;
94-
break;
95-
}
96-
}
97-
}
98-
9988
// We still want to allow other plugins to participate in
10089
// resolution, with us being in front due to `enforce: "pre"`.
10190
// But we still want to ignore everything `vite:resolve` does
@@ -166,13 +155,14 @@ export function deno(): Plugin {
166155
resolved = path.fromFileUrl(resolved);
167156
}
168157

169-
// For file:// resolved modules (npm packages in node_modules,
170-
// local files), let Vite handle loading natively. This allows
171-
// Vite to externalize CJS packages in SSR mode (Node.js handles
172-
// them with native require()) and avoids needing a custom CJS
173-
// transform. Only \0deno:: virtual modules (jsr:, non-default
174-
// types) need Fresh's custom load hook.
175-
return { id: resolved };
158+
return {
159+
id: resolved,
160+
meta: {
161+
deno: {
162+
type,
163+
},
164+
},
165+
};
176166
} catch {
177167
// ignore
178168
}
@@ -182,80 +172,6 @@ export function deno(): Plugin {
182172
? ssrLoader
183173
: browserLoader;
184174

185-
// In dev mode, CJS files need to be wrapped in an ESM shim:
186-
// - SSR: module runner evaluates as ESM, needs module/exports/require
187-
// - Client: browser evaluates as ESM, needs module/exports
188-
// In build mode, Rollup's @rollup/plugin-commonjs handles CJS.
189-
if (
190-
isDev &&
191-
!id.startsWith("\0") &&
192-
id.includes("node_modules") &&
193-
/\.(c?js|cjs)$/.test(id)
194-
) {
195-
try {
196-
const code = await Deno.readTextFile(id);
197-
// Quick heuristic: if file has CJS patterns and no ESM
198-
if (
199-
!code.includes("export ") &&
200-
!code.includes("import ") &&
201-
(code.includes("module.exports") ||
202-
code.includes("exports.") ||
203-
code.includes("require("))
204-
) {
205-
const isServer = this.environment.config.consumer === "server";
206-
207-
if (isServer) {
208-
// SSR: use Node.js createRequire for full CJS compat
209-
const wrapped = `
210-
import { createRequire as __cjs_createRequire } from "node:module";
211-
import { fileURLToPath as __cjs_fileURLToPath } from "node:url";
212-
import { dirname as __cjs_dirname } from "node:path";
213-
var __filename = __cjs_fileURLToPath(import.meta.url);
214-
var __dirname = __cjs_dirname(__filename);
215-
var require = __cjs_createRequire(import.meta.url);
216-
var module = { exports: {} };
217-
var exports = module.exports;
218-
219-
${code}
220-
221-
export default module.exports;
222-
`;
223-
return { code: wrapped };
224-
}
225-
226-
// Client: convert require() calls to ESM imports so
227-
// browsers can load them. Hoist static require() calls
228-
// to import statements at the top.
229-
const imports: string[] = [];
230-
let idx = 0;
231-
const transformed = code.replace(
232-
/\brequire\(["']([^"']+)["']\)/g,
233-
(_match: string, spec: string) => {
234-
const varName = `__cjs_import_${idx++}`;
235-
imports.push(
236-
`import ${varName} from ${JSON.stringify(spec)};`,
237-
);
238-
return `(${varName}.default ?? ${varName})`;
239-
},
240-
);
241-
242-
const wrapped = `${imports.join("\n")}
243-
var module = { exports: {} };
244-
var exports = module.exports;
245-
var __filename = "";
246-
var __dirname = "";
247-
248-
${transformed}
249-
250-
export default module.exports;
251-
`;
252-
return { code: wrapped };
253-
}
254-
} catch {
255-
// Fall through to default loading
256-
}
257-
}
258-
259175
if (isDenoSpecifier(id)) {
260176
const { type, specifier } = parseDenoSpecifier(id);
261177

@@ -281,6 +197,49 @@ export default module.exports;
281197
code,
282198
};
283199
}
200+
201+
if (id.startsWith("\0")) {
202+
id = id.slice(1);
203+
}
204+
205+
const meta = this.getModuleInfo(id)?.meta.deno as
206+
| DenoState
207+
| undefined
208+
| null;
209+
210+
if (meta === null || meta === undefined) return;
211+
212+
// Skip for non-js files like `.css`
213+
if (
214+
meta.type === RequestedModuleType.Default &&
215+
!JS_REG.test(id)
216+
) {
217+
return;
218+
}
219+
220+
const url = path.toFileUrl(id);
221+
222+
const result = await loader.load(url.href, meta.type);
223+
if (result.kind === "external") {
224+
return null;
225+
}
226+
227+
const code = new TextDecoder().decode(result.code);
228+
229+
const maybeJsx = babelTransform({
230+
ssr: this.environment.config.consumer === "server",
231+
media: result.mediaType,
232+
id,
233+
code,
234+
isDev,
235+
});
236+
if (maybeJsx) {
237+
return maybeJsx;
238+
}
239+
240+
return {
241+
code,
242+
};
284243
},
285244
transform: {
286245
filter: {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { Plugin } from "vite";
22
import * as babel from "@babel/core";
3+
import { cjsPlugin } from "./patches/commonjs.ts";
34
import { jsxComments } from "./patches/jsx_comment.ts";
5+
import { inlineEnvVarsPlugin } from "./patches/inline_env_vars.ts";
46
import { removePolyfills } from "./patches/remove_polyfills.ts";
57
import { JS_REG, JSX_REG } from "../utils.ts";
68
import { codeEvalPlugin } from "./patches/code_eval.ts";
@@ -39,8 +41,10 @@ export function patches(): Plugin {
3941

4042
const plugins: babel.PluginItem[] = [
4143
codeEvalPlugin(this.environment.config.consumer, env),
44+
cjsPlugin,
4245
removePolyfills,
4346
jsxComments,
47+
inlineEnvVarsPlugin(env, Deno.env.toObject()),
4448
];
4549

4650
const res = babel.transformSync(code, {

0 commit comments

Comments
 (0)