Skip to content

fix(start-plugin-core): keep import-protection virtual ids resolvable in server-fn compiler#7727

Open
rodriguescarson wants to merge 1 commit into
TanStack:mainfrom
rodriguescarson:fix/start-compiler-import-protection-virtual-id
Open

fix(start-plugin-core): keep import-protection virtual ids resolvable in server-fn compiler#7727
rodriguescarson wants to merge 1 commit into
TanStack:mainfrom
rodriguescarson:fix/start-compiler-import-protection-virtual-id

Conversation

@rodriguescarson

@rodriguescarson rodriguescarson commented Jul 2, 2026

Copy link
Copy Markdown

Fixes #7725

Problem

In build mode, when createServerFn's static analysis (the server-fn start-compiler-plugin) recursively loads a module whose dependency chain resolves to import-protection's mock-edge virtual module, the build crashes:

[tanstack-start-core::server-fn:client] Could not load tanstack-start-import-protection:mock-edge:<base64>: ENOENT: no such file or directory

Root cause

In packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts, the resolveId callback ran every resolved id through cleanId():

if (!r.external) return cleanId(r.id)

cleanId() strips the leading \0 that marks a Rollup-internal virtual module. That's correct for filesystem-backed ids, but import-protection's own ids (e.g. \0tanstack-start-import-protection:mock-edge:...) need that \0: the cleaned id is passed to loadModulethis.load({ id }), and import-protection's load handler filters on the \0-prefixed matchers (getResolvedVirtualModuleMatchers()). Without the prefix the filter never matches, so Rollup falls through to vite:load-fallback, which fs.readFile()s the bare string and throws ENOENT.

Fix

Preserve the \0 prefix for import-protection virtual ids; everything else is cleaned as before:

if (r.id.startsWith('\0tanstack-start-import-protection:')) {
  return r.id
}
return cleanId(r.id)

This mirrors the discriminator already used elsewhere in the plugin (import-protection-plugin/plugin.ts special-cases \0tanstack-start-import-protection: too), and it fixes the build without disabling import-protection — the mock module resolves and loads exactly as designed.

Credit to the detailed root-cause analysis in #7725.

Verification

  • nx run @tanstack/start-plugin-core:test:build (package + 12 dependency tasks) green.
  • test:types (tsc) clean.
  • ESLint clean on the changed file.
  • vitest run: 486/486 tests pass, no type errors.

The crash itself only reproduces in a full vite build of a Start app with a denied import in a server-fn's dependency chain (per #7725's repro), so it isn't covered by a unit test here; happy to add an e2e case if you'd like one.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of special internal module references so they continue to resolve correctly instead of falling through to normal file loading.
    • This helps prevent build or import issues in cases involving protected virtual modules.

… in server-fn compiler

In build mode, the server-fn compiler's resolveId ran every resolved id
through cleanId(), which strips the leading \0 that marks a Rollup virtual
module. For import-protection's own virtual ids (e.g. mock-edge), that broke
the subsequent this.load(): its load handler matches on the \0-prefixed
matchers, so the stripped id fell through to vite:load-fallback, which
fs.readFile()d the bare id and threw 'Could not load
tanstack-start-import-protection:mock-edge:...: ENOENT'.

Preserve the \0 prefix for import-protection ids so loadModule routes them to
the correct load handler; other ids are still cleaned as before.

Fixes TanStack#7725
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The resolveId function in the start-compiler-plugin now returns import-protection virtual module IDs (prefixed with \0tanstack-start-import-protection:) unchanged, rather than always stripping the leading \0 via cleanId, which previously caused these IDs to fail Rollup's virtual module load matching in build mode.

Changes

resolveId fix

Layer / File(s) Summary
Skip cleanId for import-protection virtual IDs
packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts
resolveId special-cases virtual IDs starting with \0tanstack-start-import-protection: and returns them raw instead of applying cleanId.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Suggested labels: package: start-plugin-core

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the core fix: preserving import-protection virtual IDs in the server-fn compiler path.
Linked Issues check ✅ Passed The change matches #7725 by keeping \0-prefixed tanstack-start-import-protection virtual modules resolvable and avoiding the ENOENT build crash.
Out of Scope Changes check ✅ Passed The PR is tightly scoped to the resolveId fix and does not introduce unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts (1)

373-373: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider deriving this prefix from a shared constant to avoid drift.

The literal '\0tanstack-start-import-protection:' hardcodes the namespace defined in import-protection/constants.ts (tanstack-start-import-protection:*). If that namespace ever changes, this matcher silently stops preserving the \0 prefix and reintroduces the ENOENT regression. Since constants.ts currently exports only the full ids (MOCK_MODULE_ID, MOCK_EDGE_PREFIX, etc.) and no base prefix, exporting a shared base prefix constant and reusing it here (and in the import-protection plugin's exclude regex) would keep the contract in one place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts` at line
373, The matcher in start-compiler-plugin/plugin.ts hardcodes the
import-protection namespace prefix, which can drift from the shared ids in
import-protection/constants.ts. Add or reuse a shared base prefix constant from
constants.ts and update the prefix check in the result handling around the
startsWith guard to use that symbol instead of the literal, and also align the
import-protection plugin’s exclude regex with the same shared prefix so the
namespace contract stays centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts`:
- Line 373: The matcher in start-compiler-plugin/plugin.ts hardcodes the
import-protection namespace prefix, which can drift from the shared ids in
import-protection/constants.ts. Add or reuse a shared base prefix constant from
constants.ts and update the prefix check in the result handling around the
startsWith guard to use that symbol instead of the literal, and also align the
import-protection plugin’s exclude regex with the same shared prefix so the
namespace contract stays centralized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c190805c-503c-40be-8e0b-2cf370c14d8c

📥 Commits

Reviewing files that changed from the base of the PR and between ecbbd9a and 3228012.

📒 Files selected for processing (1)
  • packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts

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.

Start: mock-edge virtual module ENOENT crash in build mode (cleanId strips \0 before this.load())

1 participant