Skip to content

Conversation

@hi-ogawa
Copy link
Contributor

@hi-ogawa hi-ogawa commented Jan 13, 2026

Summary

  • Add vitePluginResolvedIdProxy() to enable virtual use client modules
  • Add e2e tests for virtual modules (use client, CSS)
  • Document Vite's ?direct query limitation for virtual CSS modules

The problem

Virtual modules in Vite use \0 prefix for resolved IDs (e.g., \0virtual:test-virtual-client). However, when RSC generates client reference imports, these \0-prefixed IDs cannot be used directly in import specifiers.

The fix

The resolved-id proxy encodes \0 so it can be used in import specifiers:

withResolvedIdProxy("\0virtual:test-virtual-client")
  => "virtual:vite-rsc/resolved-id/%00virtual%3Atest-virtual-client"

The vitePluginResolvedIdProxy() then resolves these back to the original \0-prefixed IDs. This enables virtual use client modules to work correctly in RSC.


Side note: Virtual CSS and ?direct limitation

During this work, we investigated why virtual CSS modules don't work with SSR <link> tags in dev mode.

How Vite's ?direct works

When a browser requests <link href="xxx.css">, Vite middleware injects ?direct query. The CSS plugin checks this to return raw CSS instead of a JS wrapper.

Why virtual CSS breaks

Standard virtual module plugins use exact string matching:

resolveId(source) {
  if (source === 'virtual:my.css') return `\0${source}`  // won't match "virtual:my.css?direct"
}

For real files, Vite retries with query stripped. Virtual modules don't benefit from this fallback.

Workaround for users

Make virtual CSS plugins query-aware:

resolveId(source) {
  const clean = source.split('?')[0]
  if (clean === 'virtual:my.css') {
    return `\0${clean}${source.slice(clean.length)}`  // preserve ?direct
  }
}
load(id) {
  const clean = id.split('?')[0]
  if (clean === '\0virtual:my.css') return `.my { color: red; }`
}

E2E test matrix

Query-aware Exact-match
Server CSS (via <link>) ✅ works ❌ dev fails
Client CSS (via JS import) ✅ works ✅ works

This is a Vite limitation. Proper fix would require Vite to provide query-aware virtual module helpers or apply query-stripping fallback to virtual modules.


🤖 Generated with Claude Code

hi-ogawa and others added 6 commits January 13, 2026 17:47
- Add tests for virtual module with 'use client' directive
- Add tests for virtual CSS modules in server components
- Add tests for virtual CSS modules imported in client components
- Add virtual module with 'use client' directive
- Add virtual CSS modules (server and client)
- Add @nojs test to verify no FOUC
…t references

Virtual modules use \0 prefix internally in Vite, but this cannot be used
as an import specifier when generating client reference code. Strip the
prefix to allow virtual modules with 'use client' directive to work correctly.
Introduce vitePluginResolvedIdProxy() to handle virtual modules with \0 prefix
in import specifiers and CSS hrefs. This provides a cleaner alternative to
stripping the \0 prefix directly.

Input/Output:
- toResolvedIdProxy("\0virtual:test.css")
  => "virtual:vite-rsc/resolved-id/__x00__virtual:test.css"
- fromResolvedIdProxy("virtual:vite-rsc/resolved-id/__x00__virtual:test.css")
  => "\0virtual:test.css"

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Virtual CSS modules return JS wrapper instead of raw CSS in dev mode.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@hi-ogawa hi-ogawa changed the title test(plugin-rsc): add e2e tests for virtual modules feat(plugin-rsc): add resolved-id proxy for virtual modules Jan 13, 2026
@hi-ogawa hi-ogawa changed the title feat(plugin-rsc): add resolved-id proxy for virtual modules fix(rsc): add resolved-id proxy for virtual modules Jan 13, 2026
@hi-ogawa hi-ogawa changed the title fix(rsc): add resolved-id proxy for virtual modules fix(rsc): fix virtual "use client" moudle (add resolved-id proxy for virtual modules) Jan 14, 2026
hi-ogawa and others added 6 commits January 14, 2026 09:20
Expand the TODO comment into comprehensive documentation explaining:
- Standard virtual module pattern with \0 prefix
- How Vite's ?direct CSS mechanism works
- Why virtual modules break (exact string matching vs query injection)
- Why regular CSS files work (query-stripping fallback)
- Conclusion: this is a Vite limitation, not RSC plugin issue

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Remove resolved-id-proxy workaround for CSS hrefs and instead show
both working and failing patterns in e2e tests:

- Query-aware virtual CSS: strips ?direct in resolveId, preserves in
  resolved id, strips in load. Works with <link> in dev mode.
- Exact-match virtual CSS: standard pattern with exact string matching.
  Works via JS import but fails via <link> in dev (Vite limitation).

This demonstrates the Vite limitation documented in resolved-id-proxy.ts
and provides a reference implementation for users who need virtual CSS
to work with SSR <link> tags.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
… query/exact)

Demonstrate Vite's ?direct limitation with comprehensive test cases:

Server CSS (loaded via <link>):
- Query-aware: works in both dev and build
- Exact-match: fails in dev (Vite limitation), works in build

Client CSS (loaded via JS import):
- Query-aware: works in both dev and build
- Exact-match: works in both dev and build (no ?direct in JS imports)

In noJS mode, all CSS is loaded via <link>, so both server and client
exact-match fail in dev mode.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@hi-ogawa hi-ogawa changed the title fix(rsc): fix virtual "use client" moudle (add resolved-id proxy for virtual modules) feat(rsc): add resolved-id proxy for virtual modules + document ?direct limitation Jan 14, 2026
@hi-ogawa hi-ogawa merged commit 77c1b1b into main Jan 14, 2026
27 of 31 checks passed
@hi-ogawa hi-ogawa deleted the test/plugin-rsc-virtual-module-e2e branch January 14, 2026 04:41
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.

2 participants