Skip to content

fix(router): per-app cache versions ignore rolldown 1.2.15's module markers; vite 8.3.5 (rango-t228) - #1320

Merged
ivogt merged 2 commits into
mainfrom
fix/t228-vite-cache-versions
Oct 28, 2026
Merged

ivogt merged 2 commits into
mainfrom
fix/t228-vite-cache-versions

Conversation

@ivogt

@ivogt ivogt commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

devcorner: rango-t228 · session rango-s448

On rolldown 1.2.15, unchanged source builds to the same per-app cache versions in any directory again, and one app's new server action no longer changes the other apps' versions. Any fresh install of vite 8.2 or later resolves that rolldown. The repo moves to vite 8.3.5, which replaces Dependabot's #1222. The peer range stays vite: ^8.0.16.

Problem

vite build gives each app a data version and a document version, and the deploy keeps the caches of every app whose versions did not change (docs/design/per-app-cache-version.md). The versions hash the server output after taking out what describes the build rather than the code. That includes the module path the bundler prints above each module.

rolldown 1.2.15 (rolldown/rolldown#11235) changed that comment. It used to wrap each module in //#region <id> … //#endregion; now it prints only // <id> where the module starts. The hashing only knew the region form, so on vite 8.2 and later:

  • Building unchanged code with vite build from another directory gave every app new versions. Building it from a checkout at another path did the same for every app with a server component that imports CSS: plugin-rsc's CSS module id prints the absolute path. Each such deploy threw away caches that were still valid.
  • plugin-rsc's server-reference map was hashed again. Adding a server action to one app changed every app's versions.

#1222 showed it: 11 of 22 tests in cache-versions-build.test.ts failed. Its title says vite 8.1.6, but its lockfile resolved vite 8.3.5 with rolldown 1.2.15. vite 8.1.6 depends on rolldown ~1.1.5, which is not affected.

Before and after, for a consumer

The same two builds of the same commit, one in the app and one from the repository root:

cd apps/web && vite build          # build 1
cd ../.. && vite build apps/web    # build 2: same commit, another directory

Before, on vite 8.2+: build 2 baked other data and document versions into every app, so deploying it cleared every app's cache.
After: both builds bake in the same versions, as they do on vite 8.0 and 8.1.

No consumer code changes.

Semantics

Case rolldown < 1.2.15 (vite 8.0, 8.1) rolldown 1.2.15, before rolldown 1.2.15, after
Same source, another build directory same versions every app changes same versions
Same source, checkout at another path same versions apps with a CSS-importing server component change same versions
New action in app A app A changes every app changes app A changes
Changed code in a module its apps change its apps change its apps change
A // comment or string line that names no module of its chunk hashed hashed hashed
Minified server build (no markers) map kept in every hash same same (unchanged)
First deploy after upgrading n/a n/a every app changes once

Code changes

packages/rangojs-router/src/vite/discovery/build-versions.ts:

  • stripServerReferenceMap finds the map's marker in either form and drops its body up to the next // line or //#endregion:

    const SERVER_REFERENCE_MAP = new RegExp(
      `(^//(?:#region)? ${escapeRegExp(`\\0${SERVER_REFERENCES_MODULE_ID.slice(1)}`)}\n)[\\s\\S]*?(?=^//(?: |#endregion))`,
      "m",
    );
  • stripRegionPaths becomes stripModulePaths(source, chunk.moduleIds). A //#region line is always a marker, as before. A // line is blanked only when it names one of the chunk's own modules as rolldown prints it: relative to its working directory, so some ../ and then a trailing run of the path's segments, or a virtual id with \0 printed as two characters. A kept comment or a line of a multi-line string that happens to start with // stays in the hash, which only clears more.

The fix stays in the hash input, not the output. rolldown's experimental.attachDebugInfo: "none" would remove the markers, but it changes the shipped bytes. It also removes the marker that bounds the server-reference map, which would put the map back into every hash. Hashing the per-module code from generateBundle instead would miss the chunk's import and export lines and anything postprocessBundle writes afterwards.

Other files:

  • pnpm-workspace.yaml: catalog and overrides go to vite: ^8.3.5. The lockfile changes only vite 8.3.5 and what it pulls in: rolldown 1.2.15, lightningcss 1.33.0, postcss 8.5.32, @oxc-project/types 0.155.0, nanoid, picomatch and source-map-js, plus the peer-suffix renames.
  • docs/design/per-app-cache-version.md describes both marker forms; CHANGELOG.md gets an Unreleased Fixed entry.

Test plan

  • Red before green.

    • On main with vite 8.3.5, cache-versions-build.test.ts (real builds): 11 failed, 11 passed.
    • The test commit (f289608e1) against main's code: 3 failed, 76 passed. The failures are:
      • the start-only map;
      • the map strip;
      • the directory digest.
  • With the fix, build-versions.test.ts and cache-versions-build.test.ts pass, 102/102, on vite 8.0.16, 8.1.6 and 8.3.5. After the review cleanup they were re-run on 8.3.5: 102/102. The region-form unit tests pin the older form.

  • New unit tests in build-versions.test.ts. Each pins a contract:

    • A start-only map does not reach any router's hash, and the code after it does.
    • The map's body ends at the next start-only marker. A map that no marker follows is kept.
    • Start-only markers that name the chunk's modules are blanked, and other // lines are kept.
    • A chunk digests the same whatever directory its markers are relative to: a different checkout root, working directory and pnpm peer suffix.
    • A // line that names no module of its chunk is hashed.
  • pnpm run typecheck, pnpm run test:unit:all, pnpm run lint, pnpm run format.

  • Every check:* script, including check:e2e-parity --strict, check:suspense-contract, check:internals and check:route-gen (no .gen.ts changes).

  • check:bundle-guards: pass. The router chunk is 52025 B gzip, down from 52279 B on main, under the 52352 B cap. The change comes from the rolldown bump; this code is build-time only.

  • node --test on the tools tests: 63/63.

  • Targeted e2e on vite 8.3.5, before the review cleanup. The cleanup changed only the hashing code that the unit tests above cover.

    App Suites Dev Production
    Router test-app cache-version, navigation 18 passed 6 passed
    cloudflare-basic cache-version, navigation 6 passed 7 passed

    The cache-version suites have production describes only, by design: versions exist only after vite build.

  • CI's full e2e run on this head.

Notes

  • One-time cache clear. On rolldown 1.2.15, the first deploy with this fix gives every app new versions once, because the hash input changes. Builds on an earlier rolldown keep their versions. The CHANGELOG says so.
  • vite 8.3.5 warning. Its native config loader warns about __dirname at packages/rangojs-router/vitest.config.ts:29 and an import without an extension at tests/cloudflare-basic/vite.config.ts:8. Nothing fails. It is outside this PR.
  • Follow-up. rango-t229: warn when a chunk's markers are blanked but the server-reference map is not found, so a third marker format shows up in the build output rather than only in versions that move.
  • chore(deps): bump vite from 8.0.16 to 8.1.6 #1222 is replaced by this PR and gets closed when this lands.

Practices

  • Lint: pnpm run lint, pnpm run format and every check:* script passed on the head.
  • Dead code: the region-only constants (SERVER_REFERENCES_REGION_START, REGION_END, REGION_LINE) and stripRegionPaths are gone. No references remain.
  • Tests: the new unit tests were red before green. The real-build suite passes on the oldest vite the peer range allows, on vite 8.1.6, and on the vite the repo now pins.
  • Code review:
    • The diff was checked for reuse, simplification, efficiency and depth.
    • Applied:
      • one regex for the server-reference map, in place of two plus index arithmetic;
      • one lookup per marker;
      • a shared one-chunk helper in the new tests;
      • the exact rolldown ranges in the design doc.
    • Kept:
      • every //#region line blanked without a module check, as on main, so builds on vite 8.0 and 8.1 hash exactly as before;
      • the trailing-segment set, the simplest exact form of rolldown's relative path.
  • Docs: docs/design/per-app-cache-version.md, "What is hashed"; CHANGELOG.

ivogt added 2 commits October 28, 2026 20:35
…le markers (rango-t228, red)

rolldown 1.2.15 (rolldown/rolldown#11235, installed by vite 8.2 and later)
prints `// <module id>` where each module starts instead of wrapping it in
`//#region <id>` / `//#endregion`. The version hashing only knows the region
form, so the module paths stay in the hash and the server-reference map is
hashed whole. Three tests fail on this commit:

- a chunk digests differently when its start-only markers are relative to
  another directory;
- the server-reference map's body is not dropped under a start-only marker;
- a router's version moves when another app adds a server action.

Two guards pass already and must keep passing: a `// ` line that names no
module of its chunk is hashed, and a map no marker follows is kept.
…arkers (rango-t228)

rolldown 1.2.15 (rolldown/rolldown#11235), which a fresh install of vite
8.2 or later resolves to, prints `// <id>` before each module of an
unminified chunk instead of wrapping it in `//#region <id>` and
`//#endregion`. digestBundle stripped only the region form, so on the new
rolldown:

- the module paths stayed in the hashed text: a build started from another
  directory gave every router new versions, and a checkout at another path
  did so for each router with a CSS-importing server component (the
  virtual css id prints the absolute path in its query);
- stripServerReferenceMap found no region and kept plugin-rsc's
  server-reference map, so a new action in one app moved every app's
  versions.

stripServerReferenceMap now finds the map's marker in either form and ends
its body at the next marker. stripModulePaths (was stripRegionPaths) blanks
every `//#region` path, and a `// ` line only when it names a module of
the chunk, as rolldown prints it relative to its working directory, so a
kept comment or a line of a template literal stays hashed. A minified
build, or a map no marker follows, keeps the old behavior of hashing more.

The repo moves to vite ^8.3.5 (catalog and overrides; lockfile resolves
vite 8.3.5 and rolldown 1.2.15). The peer range stays ^8.0.16: the real-
build suite passes on vite 8.0.16, 8.1.6 and 8.3.5.
@ivogt
ivogt merged commit 3051098 into main Oct 28, 2026
61 checks passed
@ivogt
ivogt deleted the fix/t228-vite-cache-versions branch October 28, 2026 18:59
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.

1 participant