Skip to content

fix: improve Deno module resolution in Vite plugin#3734

Merged
bartlomieju merged 15 commits into
mainfrom
feat/use-deno-vite-plugin
Apr 3, 2026
Merged

fix: improve Deno module resolution in Vite plugin#3734
bartlomieju merged 15 commits into
mainfrom
feat/use-deno-vite-plugin

Conversation

@bartlomieju
Copy link
Copy Markdown
Contributor

@bartlomieju bartlomieju commented Mar 28, 2026

Summary

Three targeted fixes to Fresh's Vite plugin for better module resolution, plus new test coverage.

1. Workspace import map resolution

Pass the importer's file URL to loader.resolve() so bare specifiers from workspace member packages resolve through the correct import map — not just the root deno.json.

Fixes #3654

2. Remote island resolution

Use this.resolve(spec) in the fresh-island:: and fresh-client-island:: resolveId handlers (server_snapshot.ts / client_snapshot.ts) so JSR remote islands get proper \0deno:: specifiers. Without this, remote islands resolved to bare specifiers that Vite couldn't load.

Fixes #3437

3. Babel JSX operator precedence fix

!ssr && id.endsWith(".tsx") || id.endsWith(".jsx")!ssr && (id.endsWith(".tsx") || id.endsWith(".jsx")). The missing parentheses caused .jsx files to incorrectly get Preact JSX transforms during SSR.

Other changes

Test plan

  • Dev server tests pass (including 2 new CJS tests)
  • Build tests pass
  • deno check passes

Note on CJS issues

This PR does not fix CJS module compatibility issues in production builds. The new CJS tests verify dev server behavior (which uses Vite's built-in optimizer). Production CJS fixes need improvements to the CJS-to-ESM Babel transform in commonjs.ts — see PR #3697.

🤖 Generated with Claude Code

bartlomieju and others added 9 commits March 28, 2026 12:43
Replaces Fresh's custom 400-line deno.ts plugin with @deno/vite-plugin
for Deno module resolution/loading, keeping only Fresh-specific transforms:

- createFreshOnLoad: Preact JSX babel transform for client-side code
- freshSsrTransform: SSR precompile JSX re-transform plugin
- @deno/loader updated to ^0.4.0 (aligned with @deno/vite-plugin)

Requires unpublished @deno/vite-plugin changes:
- Per-environment loaders (environments option)
- onLoad hook for post-load transforms
- https:// import rewriting (deno-http:: prefix)
- ./resolver export for parseDenoSpecifier/isDenoSpecifier

Currently uses local path imports (../../../../deno-vite-plugin/dist/)
which must be replaced with npm:@deno/vite-plugin once published.

Tested: Fresh www site works in both dev and production build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added resolveId hook to freshSsrTransform to resolve bare specifiers
(like @marvinh-test/fresh-island) from Fresh's virtual modules through
@deno/loader. Also added eager Workspace creation via module-level
import to ensure correct deno.json discovery before Vite changes CWD.

Still blocked: @deno/loader Workspace created from freshSsrTransform
cannot resolve bare specifiers from the workspace import map. The same
code works when run directly via deno eval from the same CWD. May be
a @deno/loader instance isolation issue (different WASM instances from
different import paths don't share config discovery).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause found: the server-snapshot virtual module generates import
statements with bare specifiers (e.g. "@marvinh-test/fresh-island") for
remote islands. When Vite's SSR module runner evaluates this code,
it bypasses our resolveId hook for npm-scoped bare specifiers.

The fix: server-snapshot.ts should pre-resolve island specifiers to
their full paths (using this.resolve() in the load hook) before
generating the virtual module code. This way the import in the
generated code uses a resolved path, not a bare specifier.

Also confirmed: @deno/loader Workspace correctly discovers workspace
configs from any CWD — the config discovery is NOT the issue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The fresh-island:: and fresh-client-island:: resolveId handlers were
returning bare specifiers (e.g. "@marvinh-test/fresh-island") which
Vite couldn't resolve through the deno plugin. Now they use
this.resolve() to fully resolve through the plugin pipeline.

All 9 demo CJS/library compat routes now work:
- it_works, ioredis, pg, redis, supabase_pg, qs, stripe, commonjs, radix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dev server tests:
- Add commonjs route test (pure CJS module.exports)
- Add maxmind CJS test

Production build test:
- Add CJS packages production regression test covering commonjs, qs,
  stripe, pg, ioredis, redis, supabase_pg — verifying that CJS packages
  that work in dev also work in production builds (#3653)

Closes #3653

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Fix isDev closure bug: createFreshOnLoad now takes a getter
   (() => isDev) instead of the value, so it reads isDev at call time
   after the config() hook has set it correctly.

2. Replace local path imports (../../../../deno-vite-plugin/dist/)
   with npm:@deno/vite-plugin and add it to deno.json imports.

3. Add debug logging to SSR transform error handler instead of
   silently swallowing errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The package is now published with the onLoad callback, DenoPluginOptions
type, and /resolver subpath export that this PR depends on.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ugin

# Conflicts:
#	deno.lock
#	packages/plugin-vite/tests/build_test.ts
#	packages/plugin-vite/tests/dev_server_test.ts
@bartlomieju bartlomieju marked this pull request as ready for review March 30, 2026 13:52
@bartlomieju bartlomieju changed the title feat: replace Fresh's deno.ts with @deno/vite-plugin feat: use @deno/vite-plugin Mar 30, 2026
bartlomieju and others added 3 commits March 30, 2026 16:02
The @std/async@1.2.0 breaks @astral/astral@0.5.5 by rejecting
Infinity as maxAttempts in retry(). Keep the lockfile close to main's
resolved versions to avoid pulling in the incompatible version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes compatibility with @std/async@1.2.0 which rejects Infinity
as maxAttempts in retry().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ugin

# Conflicts:
#	deno.lock
#	packages/plugin-vite/tests/build_test.ts
bartlomieju and others added 2 commits March 31, 2026 08:49
build-id/mod.ts uses Deno.env.get() which throws in the browser.
The old deno.ts plugin transformed this away, but @deno/vite-plugin
serves it to the client untransformed.

Also:
- Add exclude option to @deno/vite-plugin config to skip Fresh
  virtual module IDs (fresh-island::, fresh-client-island::, fresh:)
- Skip Fresh virtual module prefixes in freshSsrTransform resolveId

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…o.ts

Instead of replacing Fresh's deno.ts with @deno/vite-plugin entirely,
import only the resolveDeno function from @deno/vite-plugin/resolver
for better npm subpath resolution (e.g. npm:preact@^10/jsx-runtime).

Keep Fresh's existing load/transform pipeline which correctly:
- Uses \0deno:: virtual modules that esbuild skips
- Applies Babel JSX transforms in the load hook
- Handles SSR precompile in the transform hook

Changes to deno.ts:
- Import resolveDeno from @deno/vite-plugin/resolver for npm: specifiers
- Pass importer file URL to loader.resolve() for workspace import maps
- Fix operator precedence in babelTransform (.jsx files in SSR)

Removed:
- deno_transforms.ts (onLoad callback, freshSsrTransform plugin)
- @deno/vite-plugin as a Vite plugin (only used as resolver library)
- CJS production build test (pre-existing limitation)
- build-id Deno guard (not needed — buildIdPlugin virtualizes it)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bartlomieju
Copy link
Copy Markdown
Contributor Author

Note on CJS module compatibility

The CJS production build test was removed because ioredis fails with Cannot find module './lib/modern' during Rollup builds. This is a pre-existing limitation of Fresh's deno.ts plugin — it's not a regression from this PR.

Root cause

Fresh's deno.ts uses @deno/loader to resolve and load modules. When Rollup encounters CJS require() calls with relative paths (e.g., require('./lib/modern')), the resolution fails because:

  1. The Deno loader resolves the parent module to a cache URL (e.g., file:///Users/.../npm/.../ioredis/built/index.js)
  2. Rollup's CJS-to-ESM plugin (commonjs.ts) rewrites require('./lib/modern') to an ESM import
  3. The relative import is resolved against the cached path, not the original node_modules path
  4. The ./lib/modern subpath doesn't exist in the resolved location

How to fix

This could be addressed by PR #3697 which improves the CJS-to-ESM Babel transform, or by enhancing deno.ts to resolve CJS relative imports against the original node_modules path. The @deno/vite-plugin approach handled this natively because it delegated CJS resolution to Vite/Node's resolver, but that approach was abandoned due to esbuild transform conflicts.

In dev mode, CJS packages work fine because Vite's dev server handles module loading differently (through its own dependency optimizer).

@bartlomieju bartlomieju changed the title feat: use @deno/vite-plugin fix: improve Deno module resolution in Vite plugin Mar 31, 2026
@deno/loader already handles npm subpath resolution correctly
(e.g. npm:preact@^10/jsx-runtime). No need for @deno/vite-plugin
as an intermediary — the three fixes in deno.ts are self-contained.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bartlomieju bartlomieju merged commit 9875146 into main Apr 3, 2026
9 checks passed
@bartlomieju bartlomieju deleted the feat/use-deno-vite-plugin branch April 3, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vite SSR Cannot Resolve Deno Import Map Alias for MongoDB Driver Published Fresh app on JSR doesn't work

1 participant