Skip to content

Commit 808c3d3

Browse files
bartlomiejuclaude
andcommitted
fix: externalize CJS-only npm packages in SSR build (#3653)
CJS dependencies like Sharp, ioredis, and MongoDB cause TDZ errors when Rollup bundles the SSR output, because the CJS-to-ESM transform hoists require() to import declarations that Rollup can reorder. Instead of transforming CJS modules, externalize them in the SSR build so they're loaded at runtime by Deno's Node compat layer. A package is externalized only if it has no ESM entry point (no "type": "module", no "module" field, no "import" condition in "exports"). Framework packages (preact, fresh) are always bundled to avoid duplicate module instances. This should also fix #3673 (ioredis), #3505 (mongoose), #3478 (mongodb), and #3449 (supabase/postgres-js). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 990aeb0 commit 808c3d3

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

  • packages/plugin-vite/src

packages/plugin-vite/src/mod.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,60 @@ import { checkImports } from "./plugins/verify_imports.ts";
2626
import { isBuiltin } from "node:module";
2727
import { load as stdLoadEnv } from "@std/dotenv";
2828
import path from "node:path";
29+
import * as fs from "node:fs";
30+
31+
// Packages that must always be bundled in the SSR build to avoid
32+
// duplicate module instances (e.g. preact's component registry).
33+
const SSR_BUNDLE_ALLOWLIST = new Set([
34+
"preact",
35+
"preact/hooks",
36+
"preact/compat",
37+
"preact/jsx-runtime",
38+
"preact/jsx-dev-runtime",
39+
"preact/test-utils",
40+
"preact/debug",
41+
"preact/devtools",
42+
"@preact/signals",
43+
"@preact/signals-core",
44+
]);
45+
46+
/**
47+
* Check if a package is CJS-only (no ESM entry point).
48+
* Returns true if the package should be externalized in the SSR build.
49+
*/
50+
function isCjsOnlyPackage(id: string, root: string): boolean {
51+
// Extract bare package name (handle scoped packages)
52+
const parts = id.startsWith("@") ? id.split("/", 2) : id.split("/", 1);
53+
const packageName = parts.join("/");
54+
55+
if (SSR_BUNDLE_ALLOWLIST.has(packageName) || SSR_BUNDLE_ALLOWLIST.has(id)) {
56+
return false;
57+
}
58+
59+
// Look for the package's package.json
60+
const pkgJsonPath = path.join(
61+
root,
62+
"node_modules",
63+
packageName,
64+
"package.json",
65+
);
66+
try {
67+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
68+
// If type is "module", it's ESM — keep bundled
69+
if (pkg.type === "module") return false;
70+
// If it has an ESM entry via "module" or "exports" with import condition,
71+
// keep it bundled so Vite can resolve the ESM version
72+
if (pkg.module) return false;
73+
if (pkg.exports) {
74+
const exportsStr = JSON.stringify(pkg.exports);
75+
if (exportsStr.includes('"import"')) return false;
76+
}
77+
// CJS-only package — externalize it
78+
return true;
79+
} catch {
80+
return false;
81+
}
82+
}
2983

3084
export type { FreshViteConfig };
3185
export type {
@@ -161,6 +215,28 @@ export function fresh(config?: FreshViteConfig): Plugin[] {
161215
: null) ??
162216
"_fresh/server",
163217
rollupOptions: {
218+
// Externalize CJS-only npm packages in the SSR build.
219+
// These will be loaded at runtime by Deno's Node compat
220+
// layer, avoiding the CJS-to-ESM transform that can cause
221+
// TDZ errors when Rollup reorders bundled declarations.
222+
external(id) {
223+
// Never externalize virtual modules, relative paths,
224+
// absolute paths, or Node builtins (Vite handles those)
225+
if (
226+
id.startsWith("\0") || id.startsWith(".") ||
227+
id.startsWith("/") || isBuiltin(id)
228+
) {
229+
return false;
230+
}
231+
// Never externalize fresh internals or jsr: specifiers
232+
if (
233+
id.startsWith("fresh") || id.startsWith("@fresh/") ||
234+
id.startsWith("jsr:")
235+
) {
236+
return false;
237+
}
238+
return isCjsOnlyPackage(id, config.root ?? process.cwd());
239+
},
164240
onwarn(warning, handler) {
165241
// Ignore "use client"; warnings
166242
if (warning.code === "MODULE_LEVEL_DIRECTIVE") {

0 commit comments

Comments
 (0)