Skip to content

fix(start-plugin-core): restore Rsbuild import-protection mocks from persistent cache - #8798

Open
mattcosta7 wants to merge 6 commits into
TanStack:mainfrom
mattcosta7:fix/rsbuild-import-protection-persistent-cache
Open

mattcosta7 wants to merge 6 commits into
TanStack:mainfrom
mattcosta7:fix/rsbuild-import-protection-persistent-cache

Conversation

@mattcosta7

@mattcosta7 mattcosta7 commented Oct 23, 2026 •

Copy link
Copy Markdown

🎯 Changes

With performance.buildCache enabled, Rspack restores modules from its persistent cache without rerunning their loaders. Rsbuild import protection wrote its mock modules (node_modules/.virtual/import-protection/<env>/mock-*.mjs) through VirtualModulesPlugin as a side effect of the import-protection loader, so importers restored from cache pointed at mocks that were never written. In the import-protection fixture the warm build failed:

× Module not found: Can't resolve '…/node_modules/.virtual/import-protection/client/mock-edge-….mjs'

and client violations dropped from 11 to 8. A warm rsbuild dev restart failed the same way.

Mocks are now ordinary modules with their own loader (implementation by @SyMind):

  • The import-protection post-loader rewrites denied imports to a real, package-owned mock.js entry, with the mock kind (silent/runtime/edge), environment and base64url payload in the query.
  • mock-loader generates each mock's source from its query using the existing shared mock generators. The mock entry is excluded from the post-loader.
  • Rspack's normal module lifecycle owns every mock, so a cached importer and its mocks can each be restored or rebuilt independently. Nothing depends on replaying another loader's side effect.
  • This removes VirtualModulesPlugin, the shared write/ready/pending state, and the earlier beforeResolve restore hook. A JS beforeResolve tap runs on every resolve and cost ~8% of the basic production build.
  • Mock resources keep their full query in the diagnostic graph, so imports that share a specifier keep their own violation metadata and source locations.

Tests

persistent cache restores import-protection mocks and violations (Rsbuild-only, e2e/react-start/import-protection) runs a cold build and then a warm build with a persistent cache. It requires:

  • no errors and identical violations
  • that the warm build reuses (does not rebuild) an importer containing mock requests
  • that two specifier violations sharing @tanstack/react-start/server keep their importer location, trace and snippet

It fails without the fix. Unit test updated for the mock-loader rule (including Windows separators).

Validation

  • @tanstack/start-plugin-core: test:unit (872), test:types, test:eslint, test:build
  • import-protection e2e, all toolchains: Rsbuild 88 passed; Vite SSR and bundled-dev 87 passed + 1 skipped
  • Rsbuild hmr 28/28; server-functions Rsbuild SSR 53/53 and Rsbuild persistent cache 3/3; basic e2e, all variants
  • Production build time vs main (5 alternating runs): import-protection 1,439 → 1,426 ms, basic 2,825 → 2,838 ms (noise)

Written with AI coding assistants.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested code changes locally with the relevant test commands.
  • I fully understand the code in this pull request, including any code generated with AI assistance.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where Rsbuild’s persistent cache could cause import-protection mocks to be missed after a cached rebuild. Client and server builds now retain consistent protection warnings, including source locations and violation details.

…persistent cache

Rspack's persistent cache restores modules without rerunning loaders, so the
import-protection loader never rewrote the virtual mock modules that restored
importers reference, and warm builds and dev restarts failed with Module not
found while dropping their violations. Regenerate a requested mock from its
file name, which fully encodes its contents, before Rspack resolves it.
@changeset-bot

changeset-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 54ebe15

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@tanstack/start-plugin-core Patch
@tanstack/react-start-rsc Patch
@tanstack/react-start Patch
@tanstack/solid-start Patch
@tanstack/vue-start Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between da2adec and 54ebe15.

Files selected for processing: 10
  • .changeset/rsbuild-import-protection-persistent-cache.md
  • e2e/react-start/import-protection/tests/import-protection.spec.ts
  • packages/start-plugin-core/src/rsbuild/INTERNALS-import-protection.md
  • packages/start-plugin-core/src/rsbuild/import-protection-loader.ts
  • packages/start-plugin-core/src/rsbuild/import-protection.ts
  • packages/start-plugin-core/src/rsbuild/mock-loader.ts
  • packages/start-plugin-core/src/rsbuild/mock-module.ts
  • packages/start-plugin-core/src/rsbuild/mock.ts
  • packages/start-plugin-core/tests/rsbuild/import-protection.test.ts
  • packages/start-plugin-core/vite.config.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Walkthrough

Rsbuild import protection now uses query-specific mock modules instead of shared virtual-module state. The change adds mock-module loading and resource normalization, updates loader registration and diagnostics, and adds a persistent-cache regression test.

Changes: Rsbuild import protection

Layer / File(s) Summary
Query-based mock-module transport
packages/start-plugin-core/src/rsbuild/mock-module.ts, packages/start-plugin-core/src/rsbuild/mock-loader.ts, packages/start-plugin-core/src/rsbuild/mock.ts, packages/start-plugin-core/vite.config.ts
Mock IDs now use query parameters and optional encoded payloads. mock-loader selects the silent, runtime, or edge mock source. The build entries include the new loader and mock resource.
Import-protection integration and diagnostics
packages/start-plugin-core/src/rsbuild/import-protection-loader.ts, packages/start-plugin-core/src/rsbuild/import-protection.ts, packages/start-plugin-core/tests/rsbuild/import-protection.test.ts, packages/start-plugin-core/src/rsbuild/INTERNALS-import-protection.md
The import-protection loader emits mock IDs instead of ensuring virtual modules through shared state. Rsbuild registers mock-loader and excludes its resource from the import-protection rule. Diagnostic resource lookup maps mock requests to their embedded resource paths. Tests and internals documentation reflect the updated setup.
Persistent-cache regression coverage
e2e/react-start/import-protection/tests/import-protection.spec.ts, .changeset/rsbuild-import-protection-persistent-cache.md
The E2E test builds twice with a shared cache and checks warnings, importer reuse, violation locations, traces, and getRequest() snippets. A patch changeset records the issue.

Priority: Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant transformImportProtection
  participant Rspack
  participant mock-loader
  participant getModuleResource
  transformImportProtection->>Rspack: Emit query-specific mock module ID
  Rspack->>mock-loader: Load matching mock.js request
  mock-loader->>Rspack: Return selected mock module source
  Rspack->>getModuleResource: Pass mock resource for diagnostic lookup
  getModuleResource->>Rspack: Return normalized embedded resource path
Loading

Possibly related PRs

  • TanStack/router#8801: Both changes affect Rsbuild import-protection graph handling, including mock-module resource mapping and diagnostic trace construction.

Suggested labels: package: start-plugin-core, documentation

Suggested reviewers: schiller-manuel, symind, beaussan

Merge Risk: Minimal · up to 54ebe

No actionable issue was established that should prevent merging after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description check Passed The description follows the required template. It explains the persistent-cache failure, the mock-loader solution, tests, validation results, checklist completion, changeset status, and AI assistance …
Title check Passed The title clearly and concisely describes the primary change: restoring Rsbuild import-protection mocks from persistent cache.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description follows the required template. It explains the persistent-cache failure, the mock-loader solution, tests, validation results, checklist completion, changeset status, and AI assistance disclosure.

Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 6 functions across 8 files. (2 skipped: 2 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Sheraff added a commit that referenced this pull request Oct 25, 2026
… from their request

An importer Rspack restores from its persistent cache (performance.buildCache)
in a new process requests edge modules nothing registered: the warm build
failed with "Can't resolve .../edge.<key>.mjs" for every edge module. The
importer's request now carries the module's content (?tss-ip=<base64url>,
checked against the key that digests it); a normalModuleFactory beforeResolve
tap defines unknown edge modules and the runtime from it before they resolve.
The rewrite of every known edge module at thisCompilation is gone.

Tests: Rsbuild cold+warm builds with buildCache in error and mock mode and with
an onViolation veto; Rsbuild dev edits that add a .server import, add or remove
a server-only marker of an unchanged importer's dependency (upstream #8798,
#8801 scenarios).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sheraff added a commit that referenced this pull request Oct 25, 2026
…build restored modules

A module Rspack restores from its persistent cache is not compiled, so the
edge index that gives trace steps their specifier and position knew nothing
of it: a warm build's violation traces lost those steps' locations. Each
compiled module's Rspack buildInfo record now carries its indexed edges,
replayed at finishMake for a module the index does not know (replayed by
its canonical resource, the compile's id).

Ports upstream #8798's e2e test (cold vs warm Rsbuild build with
performance.buildCache: no errors, identical warnings).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

SyMind commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

I think a dedicated mock-loader is a better fit here. I implemented it on top of this PR's current head: implementation and test changes.

The import-protection post-loader still checks the compiled code and rewrites denied imports, but the requests now point to a real, package-owned mock.js entry with the mock kind, environment, and payload in the query. mock-loader generates that module's source using the existing mock generators.

This makes each mock independently resolvable and rebuildable even when its importer comes from persistent cache. It removes the VirtualModulesPlugin state, pending writes, readiness tracking, and the beforeResolve restoration hook. The full query stays in the diagnostic graph so different imports retain their own violation metadata and source locations. The tradeoff is an additional loader, entry file, and rule, but I prefer having Rspack's normal module lifecycle own the generated source.

The regression test in this PR passes with this approach. I also strengthened it to check that the warm build actually reuses an importer containing mock requests, and that two distinct specifier diagnostics retain their usage locations and traces. All 88 Rsbuild import-protection E2E tests pass locally, along with the core unit/type/lint and package checks. The 18 bundle scenarios show no gzip growth. These checks were run on macOS; I haven't run Windows E2E or measured a build-time speedup.

…ock-loader

fix(start-plugin-core): build Rsbuild import-protection mocks with a dedicated loader
@mattcosta7

Copy link
Copy Markdown
Author

I think a dedicated mock-loader is a better fit here. I implemented it on top of this PR's current head: implementation and test changes.

The import-protection post-loader still checks the compiled code and rewrites denied imports, but the requests now point to a real, package-owned mock.js entry with the mock kind, environment, and payload in the query. mock-loader generates that module's source using the existing mock generators.

This makes each mock independently resolvable and rebuildable even when its importer comes from persistent cache. It removes the VirtualModulesPlugin state, pending writes, readiness tracking, and the beforeResolve restoration hook. The full query stays in the diagnostic graph so different imports retain their own violation metadata and source locations. The tradeoff is an additional loader, entry file, and rule, but I prefer having Rspack's normal module lifecycle own the generated source.

The regression test in this PR passes with this approach. I also strengthened it to check that the warm build actually reuses an importer containing mock requests, and that two distinct specifier diagnostics retain their usage locations and traces. All 88 Rsbuild import-protection E2E tests pass locally, along with the core unit/type/lint and package checks. The 18 bundle scenarios show no gzip growth. These checks were run on macOS; I haven't run Windows E2E or measured a build-time speedup.

Thanks, I pulled those into this branch and i'll update the related prs to avoid the old impl here!

@SyMind

SyMind commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

@mattcosta7 Maybe we can mark this PR as ready for review.

@mattcosta7
mattcosta7 marked this pull request as ready for review October 29, 2026 03:49

This branch has not been deployed

No deployments
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