fix: preserve subpath when resolving npm: specifiers - #98
Open
bartlomieju wants to merge 15 commits into
Open
bartlomieju wants to merge 15 commits into
bartlomieju wants to merge 15 commits into
Conversation
The npm specifier parser in resolveDeno() was extracting only the package name and dropping any subpath. For example, `npm:preact@^10.22.0/jsx-runtime` resolved to `preact` instead of `preact/jsx-runtime`, causing Rollup to import from the wrong entry point. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When resolveViteSpecifier has an importer, use loader.resolveSync with the importer URL first. This allows workspace member import maps to be consulted correctly. Previously, only import.meta.resolve was used for bare specifiers, which only sees the root deno.json import map and silently fails for mappings defined in workspace member deno.json files. The import.meta.resolve fallback is kept for cases without an importer or when the loader cannot resolve the specifier. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Vite 7+ per-environment module graphs cause /@id/ URLs to return empty responses when a module was only resolved in the SSR environment. This happens because Vite's /@id/ handler looks up the client module graph for browser requests, but virtual modules discovered during SSR (e.g. island components) are only in the SSR graph. Add a configureServer middleware that calls transformRequest on the client environment before Vite's built-in handler, ensuring the module is resolved and loaded in the client graph. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a configurable `exclude` option that accepts string prefixes, RegExp patterns, or an array of either. Matching IDs are skipped by both resolveId hooks, letting other plugins (e.g. Fresh) handle their own virtual modules like fresh-island:: and fresh: without the Deno loader erroring on unsupported schemes. Also removes debug console.log statements from configureServer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous pre-middleware approach had a race condition: connect doesn't support async middleware natively, so next() fired before transformRequest completed, and Vite's built-in handler would serve an empty response. Switch to a post-middleware (returned from configureServer) that runs after Vite's built-in /@id/ handler. When Vite fails to find the module in the client graph, the request falls through to our handler which resolves it via transformRequest and writes the response directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When resolveViteSpecifier calls loader.resolveSync with a bare specifier like "preact/jsx-dev-runtime", the Deno loader may resolve it to the package main entry (preact/dist/preact.mjs) instead of the correct subpath export (preact/jsx-dev-runtime/dist/jsxRuntime.mjs). Skip file:// results that point into node_modules and return null for npm: results so Vite's native resolver handles them. Vite correctly reads package.json exports maps for subpath resolution. This fixes dev mode resolution of npm subpath imports from onLoad callback output (e.g. JSX transforms producing "npm:preact@^10/jsx-dev-runtime"). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When loader.resolveSync returns a file:// URL for a cached JSR module (outside node_modules), the previous code returned the plain file path immediately. This bypassed the deno specifier creation at the end of resolveViteSpecifier, so the load hook never fired and the onLoad callback (e.g. Babel JSX transform) was skipped. Vite's built-in esbuild transform would then handle the JSX, resolving preact/jsx-dev-runtime to the wrong entry. Now non-node_modules file:// results set id = resolvedUrl and continue through resolveDeno, which creates a deno specifier with the correct media type. The load hook fires, onLoad applies the JSX transform, and the npm: imports in the transformed code go through the prefix plugin for correct subpath resolution. Also removes leftover debug logging from prefixPlugin. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…form When the onLoad callback returns already-transformed code (e.g. Babel JSX output), Vite's esbuild transform would run on it again, adding duplicate JSX runtime imports that resolve to the wrong entry point. Adding loader: "js" to the load result tells Vite the code is already plain JavaScript, so esbuild skips its JSX transformation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the onLoad callback isn't configured or returns null, the load hook returns the raw transpiled source. If it contains @jsxImportSource npm:preact@^10.22.0, Vite's esbuild transform picks it up and resolves preact/jsx-dev-runtime to the wrong entry point. Strip @jsxRuntime, @jsxImportSource, and @jsxImportSourceTypes pragma comments before returning, matching the same stripping already done for onLoad results. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JSR modules cached in node_modules/.deno/ are inside the project root, so isInsideRoot was true and resolveViteSpecifier returned a plain file path. The load hook gates on isDenoSpecifier(id), so it never fired — no onLoad callback, no JSX pragma stripping. Exclude node_modules paths from the isInsideRoot check so these files get deno specifiers and go through the load hook. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the browser requests a deno:: virtual module via /@id/, Vite
calls resolveId again with the deno specifier. The hook was returning
undefined ("I don't handle this"), so Vite never proceeded to the
load step.
Return the ID itself to confirm the module is valid, allowing Vite to
proceed to the load hook where onLoad and pragma stripping happen.
Also removes debug logging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The @deno/loader already transpiles TSX/JSX to plain JavaScript. Without loader: "js" in the load result, Vite infers the type from the virtual module's original extension (.tsx/.jsx) and runs esbuild's JSX transform on already-transpiled code, overriding any JSX transformation done by the onLoad callback. Return loader: "js" for both onLoad results and the default code path so esbuild treats the output as plain JS. Also removes debug logging. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the loader:"js" approach with a dedicated deno:skip-esbuild
transform plugin that runs before esbuild. When it sees a deno::
specifier, it returns { code } early, preventing esbuild from
re-processing already-transpiled JSX based on the .tsx extension.
This is more reliable than loader:"js" since transform hooks with
enforce:"pre" run before Vite's default esbuild transform pipeline.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of checking isDenoSpecifier(id) in the transform hook (which depends on the ID format being preserved between hooks), store IDs processed by the load hook in a shared Set. The deno:skip-esbuild transform plugin checks the Set to determine if esbuild should be skipped, making it resilient to any ID transformations Vite applies between the load and transform phases. Also removes debug logging. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The enforce:"pre" transform hook doesn't skip esbuild — Vite runs all
transform hooks in sequence, so returning { code } just passes it to
the next transform including esbuild.
Instead, use the config hook to add an esbuild.exclude pattern matching
deno:: virtual module IDs (\x00deno::). This tells Vite's esbuild
plugin to skip these modules entirely, preventing it from re-processing
JSX that @deno/loader or the onLoad callback already transformed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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
npm:preact@^10.22.0/jsx-runtimenow correctly resolves topreact/jsx-runtimeinstead of justpreact. The parser inresolveDeno()was dropping everything after the version.loader.resolveSyncwith the importer URL before falling back toimport.meta.resolve. This lets bare specifiers resolve through workspace memberdeno.jsonimport maps, not just the root one.configureServermiddleware that pre-warms the client environment's module graph for/@id/requests. Vite 7+ per-environment module graphs cause virtual modules discovered during SSR (e.g. island components) to be missing from the client graph.excludeoption: Configurable list of string prefixes or RegExp patterns for module IDs that should be skipped by the plugin'sresolveIdhooks. This lets other plugins (e.g. Fresh) handle their own virtual modules (fresh-island::,fresh:) without the Deno loader erroring on unsupported schemes.excludeusageTest plan
resolves npm: subpathtest — importsuseStatefromnpm:preact@^10.24.0/hooks@jsxImportSource npm:preact@^10/jsx-runtimedeno.json/@id/return content)excludeskips virtual module IDs from Fresh🤖 Generated with Claude Code