fix: externalize CJS-only npm packages in SSR build#3765
Open
bartlomieju wants to merge 3 commits into
Open
Conversation
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>
Adds a test fixture with a CJS-only npm package and verifies: 1. The build succeeds (no TDZ errors) 2. The CJS module is externalized (not inlined in the bundle) 3. The production server works with the externalized dependency Also fixes the externalization approach to use Rollup's external option (which receives bare specifiers) instead of Vite's resolve.external (which doesn't work with noExternal: true). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This was referenced May 7, 2026
lunadogbot
suggested changes
May 12, 2026
Contributor
lunadogbot
left a comment
There was a problem hiding this comment.
CI is red on every test job because packages/plugin-vite/tests/fixtures/cjs_dependency/routes/index.tsx:3 imports cjs-test-module but the fixture has no deno.json (or package.json) declaring the import, so deno task check:types fails with TS2307: Import "cjs-test-module" not a dependency and not in import map. The integration test in build_test.ts:689 also expects node_modules/cjs-test-module/ to exist for the symlink and prod launch, but no such fixture is committed. Add a deno.json to the fixture and commit (or generate during test setup) a minimal node_modules/cjs-test-module/{package.json,index.js}.
- nit:
isCjsOnly(packages/plugin-vite/src/mod.ts:106) checks for theimportcondition viaJSON.stringify(pkg.exports).includes('"import"')— substring-matches any key or value containing the literal"import"(e.g. a custom"importmap"condition or a path withimportin its name). Walking the exports object and looking for an exactimportkey is more robust.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
CJS dependencies (Sharp, ioredis, MongoDB, etc.) cause fatal TDZ
ReferenceErrorwhen building for production, even though they work fine in dev mode. The root cause is that Fresh's CJS-to-ESM babel transform hoistsrequire()calls toimportdeclarations, and Rollup can reorder theseconstbindings in the bundled output, causing references before initialization.Fix: Instead of transforming CJS modules, externalize them in the SSR build. They're loaded at runtime by Deno's Node compat layer, which handles CJS natively.
How it works
The SSR Rollup config now has an
externalfunction that checks each imported package:package.jsonfromnode_modules"type": "module", a"module"field, or"import"conditions in"exports"→ keep bundled (ESM, no issues)Framework packages (preact, @preact/signals, fresh) are always bundled regardless, to prevent duplicate module instances.
Impact
Should fix the entire class of "CJS dependency breaks production build" issues:
@supabase/postgres-jsbreaks due to commonjs #3449 — @supabase/postgres-jsCloses #3653
Test plan
deno task build && deno task startworks🤖 Generated with Claude Code