Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion packages/plugin-vite/demo/fixtures/commonjs_mod.cjs

This file was deleted.

1 change: 1 addition & 0 deletions packages/plugin-vite/demo/fixtures/commonjs_mod.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = "ok";
8 changes: 0 additions & 8 deletions packages/plugin-vite/demo/fixtures/maxmind.cjs

This file was deleted.

2 changes: 2 additions & 0 deletions packages/plugin-vite/demo/fixtures/maxmind.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import assert from "node:assert";
assert(true);
2 changes: 1 addition & 1 deletion packages/plugin-vite/demo/routes/tests/commonjs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { value } from "../../fixtures/commonjs_mod.cjs";
import { value } from "../../fixtures/commonjs_mod.js";

export default function Page() {
return <h1>{value}</h1>;
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-vite/demo/routes/tests/maxmind.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as maxmind from "../../fixtures/maxmind.cjs";
import * as maxmind from "../../fixtures/maxmind.js";

export default function Page() {
// deno-lint-ignore no-console
Expand Down
99 changes: 73 additions & 26 deletions packages/plugin-vite/src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,43 @@ export function fresh(config?: FreshViteConfig): Plugin[] {
});

let isDev = false;
let freshMode = "development";

const plugins: Plugin[] = [
{
name: "fresh",
sharedDuringBuild: true,
config(config, env) {
async config(config, env) {
isDev = env.command === "serve";
freshMode = isDev ? "development" : "production";

// Load env files early so define entries are available
const root = config.root ? path.resolve(config.root) : Deno.cwd();
const envDir = config.envDir ? path.resolve(root, config.envDir) : root;
await loadEnvFile(path.join(envDir, ".env"));
await loadEnvFile(path.join(envDir, ".env.local"));
await loadEnvFile(path.join(envDir, `.env.${freshMode}`));
await loadEnvFile(path.join(envDir, `.env.${freshMode}.local`));

// Build define map for FRESH_PUBLIC_* env vars
// Replaces the Babel inlineEnvVarsPlugin with Vite's native define
const envDefine: Record<string, string> = {};
for (const [key, value] of Object.entries(Deno.env.toObject())) {
if (key.startsWith("FRESH_PUBLIC_")) {
envDefine[`process.env.${key}`] = JSON.stringify(value);
envDefine[`import.meta.env.${key}`] = JSON.stringify(value);
}
}

return {
define: envDefine,
ssr: {
// Bundle all deps in SSR so that resolve.alias
// (react -> preact/compat) is applied consistently.
// CJS packages are handled by the deno plugin's load
// hook which wraps them in an ESM-compatible shim.
noExternal: true,
},
server: {
watch: {
// Ignore temp files, editor swap files, and Vite timestamp
Expand Down Expand Up @@ -119,14 +147,15 @@ export function fresh(config?: FreshViteConfig): Plugin[] {
"react-dom": "preact/compat",
react: "preact/compat",
},
// Disallow externals, because it leads to duplicate
// modules with `preact` vs `npm:preact@*` in the server
// environment.
noExternal: true,
},

optimizeDeps: {
// Optimize deps somehow leads to duplicate modules or them
// being placed in the wrong chunks...
// Disable dep optimizer because deno.ts handles all
// module resolution. The optimizer causes duplicate
// module instances when remote (JSR) islands resolve
// deps to /@fs/ paths while the optimizer bundles to
// /.vite/deps/. CJS packages in client-side islands
// are handled by deno.ts's load hook.
noDiscovery: true,
},

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

// Ignore commonjs optional exports
if (
warning.code === "MISSING_EXPORT" &&
warning.message.includes("__require")
) {
return;
}

// Ignore this warnings
if (warning.code === "THIS_IS_UNDEFINED") {
return;
Expand All @@ -221,7 +242,7 @@ export function fresh(config?: FreshViteConfig): Plugin[] {
},
};
},
async configResolved(vConfig) {
configResolved(vConfig) {
// Run update check in background
updateCheck(UPDATE_INTERVAL).catch(() => {});

Expand All @@ -236,19 +257,45 @@ export function fresh(config?: FreshViteConfig): Plugin[] {
const name = fConfig.namer.getUniqueName(specName);
fConfig.islandSpecifiers.set(spec, name);
});
},
},
// Lightweight replacement for Deno.env.get() calls with FRESH_PUBLIC_*
// and NODE_ENV values. Replaces the Babel inlineEnvVarsPlugin for this
// pattern which can't be handled by Vite's define (it's a call expression).
{
name: "fresh:deno-env",
sharedDuringBuild: true,
applyToEnvironment() {
return true;
},
transform: {
filter: {
id: /\.([tj]sx?|[mc]?[tj]s)(\?.*)?$/,
},
handler(code) {
if (!code.includes("Deno.env.get(")) return;

const envDir = pathWithRoot(
vConfig.envDir || vConfig.root,
vConfig.root,
);
const allEnv = Deno.env.toObject();
let modified = false;
const result = code.replace(
/Deno\.env\.get\(\s*["']([^"']+)["']\s*\)/g,
(match: string, name: string) => {
if (name === "NODE_ENV") {
modified = true;
return JSON.stringify(freshMode);
}
if (name.startsWith("FRESH_PUBLIC_") && name in allEnv) {
modified = true;
return JSON.stringify(allEnv[name]);
}
return match;
},
);

await loadEnvFile(path.join(envDir, ".env"));
await loadEnvFile(path.join(envDir, ".env.local"));
const mode = isDev ? "development" : "production";
await loadEnvFile(path.join(envDir, `.env.${mode}`));
await loadEnvFile(path.join(envDir, `.env.${mode}.local`));
if (modified) return { code: result };
},
},
},
} satisfies Plugin,
serverEntryPlugin(fConfig),
patches(),
...serverSnapshot(fConfig),
Expand Down
153 changes: 97 additions & 56 deletions packages/plugin-vite/src/plugins/deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,14 @@ import {
import * as path from "@std/path";
import * as babel from "@babel/core";
import { httpAbsolute } from "./patches/http_absolute.ts";
import { JS_REG, JSX_REG } from "../utils.ts";
import { JSX_REG } from "../utils.ts";
import { builtinModules } from "node:module";

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

const BUILTINS = new Set(builtinModules);

interface DenoState {
type: RequestedModuleType;
}

export function deno(): Plugin {
let ssrLoader: Loader;
let browserLoader: Loader;
Expand Down Expand Up @@ -85,6 +81,21 @@ export function deno(): Plugin {
id = `${url.origin}${id}`;
}

// Apply resolve.alias before Deno resolution so that
// react -> preact/compat works even in externalized packages.
// Vite normalizes alias config to { find, replacement }[] format.
const aliases = this.environment?.config?.resolve?.alias;
if (aliases) {
const list = Array.isArray(aliases) ? aliases : [];
for (const alias of list) {
const find = alias.find;
if (typeof find === "string" ? find === id : find?.test?.(id)) {
id = typeof alias.replacement === "string" ? alias.replacement : id;
break;
}
}
}

// We still want to allow other plugins to participate in
// resolution, with us being in front due to `enforce: "pre"`.
// But we still want to ignore everything `vite:resolve` does
Expand Down Expand Up @@ -155,14 +166,13 @@ export function deno(): Plugin {
resolved = path.fromFileUrl(resolved);
}

return {
id: resolved,
meta: {
deno: {
type,
},
},
};
// For file:// resolved modules (npm packages in node_modules,
// local files), let Vite handle loading natively. This allows
// Vite to externalize CJS packages in SSR mode (Node.js handles
// them with native require()) and avoids needing a custom CJS
// transform. Only \0deno:: virtual modules (jsr:, non-default
// types) need Fresh's custom load hook.
return { id: resolved };
} catch {
// ignore
}
Expand All @@ -172,6 +182,80 @@ export function deno(): Plugin {
? ssrLoader
: browserLoader;

// In dev mode, CJS files need to be wrapped in an ESM shim:
// - SSR: module runner evaluates as ESM, needs module/exports/require
// - Client: browser evaluates as ESM, needs module/exports
// In build mode, Rollup's @rollup/plugin-commonjs handles CJS.
if (
isDev &&
!id.startsWith("\0") &&
id.includes("node_modules") &&
/\.(c?js|cjs)$/.test(id)
) {
try {
const code = await Deno.readTextFile(id);
// Quick heuristic: if file has CJS patterns and no ESM
if (
!code.includes("export ") &&
!code.includes("import ") &&
(code.includes("module.exports") ||
code.includes("exports.") ||
code.includes("require("))
) {
const isServer = this.environment.config.consumer === "server";

if (isServer) {
// SSR: use Node.js createRequire for full CJS compat
const wrapped = `
import { createRequire as __cjs_createRequire } from "node:module";
import { fileURLToPath as __cjs_fileURLToPath } from "node:url";
import { dirname as __cjs_dirname } from "node:path";
var __filename = __cjs_fileURLToPath(import.meta.url);
var __dirname = __cjs_dirname(__filename);
var require = __cjs_createRequire(import.meta.url);
var module = { exports: {} };
var exports = module.exports;

${code}

export default module.exports;
`;
return { code: wrapped };
}

// Client: convert require() calls to ESM imports so
// browsers can load them. Hoist static require() calls
// to import statements at the top.
const imports: string[] = [];
let idx = 0;
const transformed = code.replace(
/\brequire\(["']([^"']+)["']\)/g,
(_match: string, spec: string) => {
const varName = `__cjs_import_${idx++}`;
imports.push(
`import ${varName} from ${JSON.stringify(spec)};`,
);
return `(${varName}.default ?? ${varName})`;
},
);

const wrapped = `${imports.join("\n")}
var module = { exports: {} };
var exports = module.exports;
var __filename = "";
var __dirname = "";

${transformed}

export default module.exports;
`;
return { code: wrapped };
}
} catch {
// Fall through to default loading
}
}

if (isDenoSpecifier(id)) {
const { type, specifier } = parseDenoSpecifier(id);

Expand All @@ -197,49 +281,6 @@ export function deno(): Plugin {
code,
};
}

if (id.startsWith("\0")) {
id = id.slice(1);
}

const meta = this.getModuleInfo(id)?.meta.deno as
| DenoState
| undefined
| null;

if (meta === null || meta === undefined) return;

// Skip for non-js files like `.css`
if (
meta.type === RequestedModuleType.Default &&
!JS_REG.test(id)
) {
return;
}

const url = path.toFileUrl(id);

const result = await loader.load(url.href, meta.type);
if (result.kind === "external") {
return null;
}

const code = new TextDecoder().decode(result.code);

const maybeJsx = babelTransform({
ssr: this.environment.config.consumer === "server",
media: result.mediaType,
id,
code,
isDev,
});
if (maybeJsx) {
return maybeJsx;
}

return {
code,
};
},
transform: {
filter: {
Expand Down
Loading