Skip to content

fix(rendering): compile client and module output for the render mode - #3845

Merged
kojiwakayama merged 1 commit into
mainfrom
fix/issue-555-production-compile-mode
Aug 18, 2026
Merged

kojiwakayama merged 1 commit into
mainfrom
fix/issue-555-production-compile-mode

Conversation

@kojiwakayama

Copy link
Copy Markdown
Contributor

Summary

This is a production behaviour change. Two transform call sites still hardcoded dev: true after #3841, so the hosted production render path shipped development-compiled output on every request. Per src/transforms/pipeline/stages/compile.ts, dev: true means minify: false, treeShaking: false and an inline sourcemap, so production paid a payload-size cost and disclosed the project source. Both sites are fixed here, and each one's cache identity is updated in the same commit.

Refs veryfront/veryfront-issue-inbox#555

Site A: the client hydration bundle

bundleComponentForClient (src/rendering/component-handling.ts) hardcoded dev: true and had no mode parameter. src/rendering/page-renderer.ts reaches it on every TSX page render that does not supply a cached client module.

  • bundleComponentForClient takes the compile mode, defaulting to production.
  • handleComponentPage derives it from the render mode with the same options?.mode === "development" line the SSR half of the file already used.

Cache identity, updated alongside the flag. buildComponentHydrationCacheHash did not include the compile mode. Fixing the flag alone would let a development bundle be served to a production render, or the reverse, because both modes resolve to one hydration cache entry. The hash now carries the compile mode.

Site B: the MDX ESM module fetcher

transformResolvedModuleSource (src/transforms/mdx/esm-module-loader/module-fetcher/source-transform.ts) hardcoded dev: true, and createModuleFetcherContext had no way to express the render mode. The chain is ssr-module-loader/loader.ts to vf-module-resolver.ts to createModuleFetcherContext to fetchAndCacheModule, and it fires for every /_vf_modules/* import of every SSR module.

  • The module fetcher context takes a compile mode, defaulting to production.
  • resolveVfModuleImports requires one, so a caller cannot silently omit it.
  • The SSR module loader passes this.options.dev.

Cache identity, mandatory in the same commit. getTransformCacheKey had no compile-mode segment, and it feeds the distributed transform cache, whose entries are shared across requests and across instances with a TTL. Fixing the transform without the key would have been worse than the original bug: one instance could serve a development-compiled module to a hosted production render, and the reverse.

The compile mode is now part of getMdxModuleCacheVariant, the single segment builder behind the distributed transform key (getTransformCacheKey to buildMdxEsmTransformCacheKey), the module path-cache key (getVersionedPathCacheKey and cacheModule), and the SSR loader and render orchestrator lookups. All of these share one key space, so every writer and reader of it had to agree; a partial fix would have left the same cross-mode reuse through the path cache, which short-circuits before any transform runs.

Development artifacts take an on:compile-dev segment and production keeps the unsegmented key. The MDX-ESM cache namespace is rolled in the same change (the compile-mode split is named in the namespace schema sample), so the legacy entries written under the unsegmented key, all of them development-compiled, cannot be read back by a production render.

Artifact filenames already hash the transformed code, so the two modes cannot collide on disk and the artifact directory layout is unchanged.

Scope

The compiled-MDX entry path (esm-module-loader/loader-helpers.ts) carries no render mode yet, so it keeps the development compile mode it has always used. That is now explicit rather than implied by a hardcoded literal, and the compile mode in the cache identity keeps its artifacts isolated from the production-compiled ones. Threading a render mode into MDXRenderer.loadModuleESM is a separate change.

No RenderContext.mode value ("development" | "production") is forwarded into a "preview" | "production" field. Only the compile-mode boolean is threaded.

Tests

  • src/rendering/component-handling.test.ts: a production-mode handleComponentPage render emits a hydration bundle with no inline sourcemap, minified and tree-shaken; a development-mode render still emits the debuggable bundle. Both assert on the emitted code. A third case pins that the two modes take separate hydration cache entries.
  • src/modules/react-loader/ssr-module-loader/vf-module-resolver.test.ts: a development resolve runs first, then a production resolve of the same module for the same project and content source. The production render gets a different artifact with no inline sourcemap and no dead code, which fails if either the flag or the cache identity is missing.
  • src/modules/react-loader/ssr-module-loader/loader.test.ts: an end-to-end SSRModuleLoader production load of a module with a /_vf_modules/ import, asserting the cached artifact.
  • src/transforms/mdx/esm-module-loader/module-fetcher/cache-keys.test.ts: the compile mode changes the distributed transform key and the path-cache key, an unset mode reads as production, and the compile segment stays separate from the pin and server-external segments.
  • src/transforms/mdx/esm-module-loader/module-fetcher/source-transform.test.ts: the requested compile mode reaches the transform.

Each new behaviour test was confirmed to fail against the unfixed code.

Verification

deno test --preload=src/testing/preload.ts --no-check --allow-all src/rendering/    111 passed, 0 failed
deno test ... src/transforms/                                                       159 passed, 0 failed
deno test ... src/modules/react-loader/                                              26 passed, 0 failed
full src unit suite                                                                3639 passed, 0 failed
tests/ integration suite                                                            302 passed, 0 failed
deno fmt --check src/ cli/ react/ templates/                                        clean
deno lint on touched files, deno task typecheck                                     clean
deno task lint:sanitizer-baseline, lint:test-typecheck, lint:module-boundaries       clean

@coderabbitai

coderabbitai Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@kojiwakayama, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 59 minutes

Limit details: You’ve used all 1 included review currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 52ec232c-903d-4aaa-81e0-15c404c08041

📥 Commits

Reviewing files that changed from the base of the PR and between d6b2f8b and 6c82e90.

📒 Files selected for processing (20)
  • src/modules/react-loader/ssr-module-loader/loader.test.ts
  • src/modules/react-loader/ssr-module-loader/loader.ts
  • src/modules/react-loader/ssr-module-loader/vf-module-resolver.test.ts
  • src/modules/react-loader/ssr-module-loader/vf-module-resolver.ts
  • src/rendering/component-handling.test.ts
  • src/rendering/component-handling.ts
  • src/rendering/orchestrator/module-loader/index.ts
  • src/rendering/orchestrator/module-loader/module-cache-lookup.ts
  • src/rendering/orchestrator/module-loader/module-persistence.ts
  • src/transforms/mdx/esm-module-loader/cache-format.ts
  • src/transforms/mdx/esm-module-loader/loader-helpers.ts
  • src/transforms/mdx/esm-module-loader/module-fetcher/cache-keys.test.ts
  • src/transforms/mdx/esm-module-loader/module-fetcher/cache-keys.ts
  • src/transforms/mdx/esm-module-loader/module-fetcher/http-fallback.ts
  • src/transforms/mdx/esm-module-loader/module-fetcher/index.ts
  • src/transforms/mdx/esm-module-loader/module-fetcher/module-cache.ts
  • src/transforms/mdx/esm-module-loader/module-fetcher/persistence.ts
  • src/transforms/mdx/esm-module-loader/module-fetcher/source-transform.test.ts
  • src/transforms/mdx/esm-module-loader/module-fetcher/source-transform.ts
  • src/transforms/mdx/esm-module-loader/types.ts

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.

@github-actions

Copy link
Copy Markdown

📦 Client bundle boundary

Entrypoint Modules Source size Server leaks
src/index.client.ts 325 1939 KiB ✅ 0

A server module in a client graph aborts hydration in the browser. New leaks fail CI; known leaks are tracked in scripts/lint/client-bundle-baseline.json to burn down.

Two transform call sites still hardcoded `dev: true`, so the hosted
production path shipped development-compiled output: unminified, not
tree-shaken, and carrying an inline sourcemap of the project source
(`stages/compile.ts` derives `minify`, `treeShaking` and `sourcemap`
from `dev`). Both are reached on every hosted production render.

Site A, the client hydration bundle. `bundleComponentForClient`
(`rendering/component-handling.ts`) had no mode parameter at all, and
`page-renderer.ts` reaches it for every TSX page render that does not
supply a cached client module. It now takes the compile mode, and
`handleComponentPage` derives it from the render mode with the same
`options?.mode === "development"` line the SSR half of the file already
used. `buildComponentHydrationCacheHash` omitted the compile mode, so
fixing the flag alone would let a development bundle be served to a
production render; the hash now carries it.

Site B, the MDX ESM module fetcher. `transformResolvedModuleSource`
hardcoded `dev: true` and `createModuleFetcherContext` had no way to
express the render mode, so every `/_vf_modules/*` import of every SSR
module was compiled for development. The fetcher context now takes a
compile mode, `resolveVfModuleImports` requires one, and the SSR module
loader passes `this.options.dev`.

The Site B cache identity had to change in the same commit. The compile
mode was absent from `getTransformCacheKey`, which feeds the distributed
transform cache, so entries are shared across requests and across
instances with a TTL: fixing the transform without the key would have
let one instance serve development-compiled modules to a hosted
production render, which is worse than the current bug. The compile mode
is now part of `getMdxModuleCacheVariant`, the single segment builder
behind the distributed transform key, the module path-cache key, and the
SSR loader and orchestrator lookups, so every writer and reader of those
key spaces agrees. Development artifacts take an `on:compile-dev`
segment and production keeps the unsegmented key; the MDX-ESM cache
namespace is rolled in the same change, so the legacy entries written
under the unsegmented key (all of them development-compiled) cannot be
read back by a production render.

Artifact filenames already hash the transformed code, so the on-disk
layout is unchanged.

The compiled-MDX entry path (`loader-helpers.ts`) carries no render mode
yet and keeps the development compile mode it has always used. It is now
explicit, and the compile mode in the cache identity keeps its artifacts
isolated from the production-compiled ones.

Refs veryfront/veryfront-issue-inbox#555
@kwakayama
kwakayama force-pushed the fix/issue-555-production-compile-mode branch from af861c2 to 6c82e90 Compare August 18, 2026 06:14
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review exact head 6c82e90. Please focus on compile-mode propagation, every reader and writer of the hydration and MDX ESM cache identities, legacy cache invalidation, hosted preview semantics, and positional-call compatibility.

@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 18, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 6c82e90b5a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kojiwakayama
kojiwakayama added this pull request to the merge queue Aug 18, 2026
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 18, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Merged via the queue into main with commit cf8882f Aug 18, 2026
34 checks passed
@kojiwakayama
kojiwakayama deleted the fix/issue-555-production-compile-mode branch August 18, 2026 07:11
kwakayama pushed a commit that referenced this pull request Aug 18, 2026
Follow-up to #3841, #3844 and #3845. Three items from the #3845 review are
live on main.

Site C, hosted production MDX pages. `loader-helpers.ts` passed an explicit
`dev: true` for every `/_vf_modules/*` import of a compiled-MDX entry, and
both `rendering/page-rendering.ts` and the RSC `render-handler.ts` reach that
path on a hosted production render. `ESMLoaderContext` had no mode field, so
the render mode could not be threaded. It has one now, both call sites pass
it, and a context that names no mode compiles for production. Measured on a
probe module: development emits 1031 bytes with an inline sourcemap, retained
dead code and unminified identifiers; production emits 220 bytes with none of
those, and the two land on different cache keys.

The render cache identity. `buildRenderCachePrefix` took the project, the
"preview" | "production" environment and the release key, none of which imply
the compile mode. `isLocalProject` decides the compile mode and never reached
the prefix, so a local development server and a hosted preview server for the
same project and branch both produced `<project>:preview:main:<version>`. A
cached render carries its hydration bundle (`cachedClientModule` in
`handleComponentPage`, fed from `cachedResult.pageModule`), so a
development-compiled bundle could be served straight out of the render cache
to a production-mode render, around the hydration-cache fix in #3845. The
prefix now carries a required compile-mode segment, `parseRenderCacheKey`
reads it back, and both prefix builders derive it from the same value they
already use for `mode`. Both modes gain the segment, so entries written under
the old prefix shape cannot be misread after deploy; they age out like any
`VERSION` roll.

The cache-namespace invariant. `buildMdxEsmCacheSchemaSample` names the
compile-mode variant only to roll `MDX_ESM_CACHE_NAMESPACE`, and that roll is
the only thing keeping a production render off a legacy, always
development-compiled entry. Every test referenced the namespace symbolically,
so deleting the line reopened the hole silently. The sample builder is now
exported and a test rebuilds the pre-roll namespace from it, then asserts no
current production key can reach that namespace. It asserts the isolation
property, not the hash.

Tests now pin the compile mode through the five #3845 source files that
nothing detected a regression in. Reverting each one to its pre-#3845 state
fails at least one test.

Refs veryfront/veryfront-issue-inbox#555
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