Skip to content

perf(build): minify server build environments by default - #1777

Merged
james-elicx merged 6 commits into
mainfrom
fix/minify-server-bundles
Jun 6, 2026
Merged

perf(build): minify server build environments by default#1777
james-elicx merged 6 commits into
mainfrom
fix/minify-server-bundles

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Problem

vinext's server-side build environments ship unminified. Vite only applies build.minify to the client environment by default — for every non-client environment (rsc, ssr, and the Cloudflare worker env) build.minify defaults to false, and vinext never sets it. So the deployed worker (dist/server/index.js), the SSR renderer (dist/server/ssr/index.js), and the Pages-Router-on-Cloudflare worker all ship full of readable identifiers, comments, and whitespace.

Raw size drives workerd cold-start parse CPU; gzip size is counted against the Cloudflare Workers size limit. Minifying is a large, cheap win.

Approach

Add a build-only configEnvironment plugin (vinext:server-minify-defaults) that sets minify: true for non-client environments. This is a true default that yields to user configuration, not a hard override:

  • prod-only: apply: "build" scopes it to production builds (never dev/preview).
  • yields to user/plugin config: Vite seeds each non-client environment's build from the top-level config.build before running configEnvironment (see getDefaultEnvironmentOptions), so the incoming config.build?.minify reflects any explicit setting — top-level, environments.<name>.build.minify, or set by an earlier plugin (e.g. @cloudflare/vite-plugin). If anyone already chose a value (including false), we leave it untouched; we only fill the default when it's still undefined.
  • minify: true lets the rolldown/oxc toolchain pick its native minifier rather than pinning 'esbuild'.

Using configEnvironment (rather than the per-environment blocks in the config hook) means it also covers the Cloudflare worker environment that @cloudflare/vite-plugin owns — which vinext's config hook doesn't define — so Pages-Router-on-Cloudflare apps benefit too.

Scope kept to minify only; sourcemaps are intentionally not touched (possible focused follow-up).

Measured before/after (real builds this session)

Example File raw before raw after gzip before gzip after
hackernews dist/server/index.js 1342164 (1311 KB) 620699 (606 KB), -54% 338406 (330 KB) 202564 (198 KB), -40%
hackernews dist/server/ssr/index.js 643167 (628 KB) 275609 (269 KB), -57% 136309 (133 KB) 84575 (83 KB), -38%
app-router-cloudflare dist/server/index.js 875457 (855 KB) 375196 (366 KB), -57% 208052 (203 KB) 107208 (105 KB), -48%
app-router-cloudflare dist/server/ssr/index.js 798913 (780 KB) 330557 (323 KB), -59% 182647 (178 KB) 102151 (100 KB), -44%
pages-router-cloudflare dist/pages_router_cloudflare/index.js (worker) 826635 (807 KB) 360275 (352 KB), -56% 184693 (180 KB) 106276 (104 KB), -42%

"before" numbers were produced by temporarily setting build: { minify: false } (reproducing the old default) and rebuilding; "after" are clean builds with this change.

User-override verification

Temporarily set build: { minify: false } in examples/hackernews, examples/app-router-cloudflare, and examples/pages-router-cloudflare's vite.config.ts, rebuilt, and confirmed the output reverted to unminified (readable identifiers/whitespace, e.g. hackernews dist/server/index.js back to 1342164 raw; pages worker back to 826635 raw with import fs from "node:fs";). This proves the default yields to explicit user config. All temporary config edits were reverted.

Runtime check

Ran vp preview on the minified app-router-cloudflare worker: GET / returned HTTP 200 with fully rendered HTML (<title>vinext on Cloudflare Workers</title>), including the counter client component and its hydration chunk (_next/static/counter-*.js) — confirming the RSC/SSR/client split is intact under minification.

Tests run (targeted only)

  • tests/build-optimization.test.ts + tests/shims.test.ts1103 passed
  • tests/use-client-export-all-build.test.ts + tests/clean-build-output.test.ts6 passed (RSC "use client" directive handling intact)
  • vp check on packages/vinext — formatting + lint + typecheck pass, no new errors

(Did not run the full suite per CPU constraints.)

Vite only minifies the client environment; non-client (rsc/ssr and the
Cloudflare worker) environments default build.minify to false, so the
deployed worker and SSR renderer ship unminified. Add a build-only
configEnvironment default that sets minify: true for server environments,
yielding to any explicit user/plugin minify setting (including false).
@pkg-pr-new

pkg-pr-new Bot commented Jun 5, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@1777
npm i https://pkg.pr.new/vinext@1777

commit: 97c29de

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@james-elicx james-elicx changed the title fix(build): minify server build environments by default perf(build): minify server build environments by default Jun 5, 2026
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk 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.

Review: perf(build): minify server build environments by default

I verified the core mechanism against the actual @voidzero-dev/vite-plus-core (Vite 8) and @cloudflare/vite-plugin@1.31.0 sources in node_modules, and exercised the plugin's configEnvironment hook directly. The implementation is correct and the PR's central claims hold. No blocking issues found.

Verified claims

  1. "Yields to user config" guarantee holds. Confirmed in vite-plus-core:

    • getDefaultEnvironmentOptions() seeds each environment's build from top-level config.build (build: config.build).
    • Each env is mergeConfig(defaultNonClientEnvironmentOptions, config.environments[name]) before runConfigEnvironmentHook runs, so the config arg passed to configEnvironment(name, config, ...) already reflects top-level and per-env (environments.<name>.build.minify) user settings.
    • The if (config.build?.minify !== undefined) return null guard therefore correctly defers to false, true, and string minifier choices ('esbuild'/'oxc').
  2. Covers the Cloudflare worker env. Confirmed @cloudflare/vite-plugin defines its worker environment(s) via the config hook (getEnvironmentsConfigcreateCloudflareEnvironmentOptions) and never sets build.minify. So vinext's hook sees minify === undefined for the worker env and fills true. Using configEnvironment (not the config-hook per-env blocks) is the right call here, since vinext's config hook doesn't own that env.

  3. Prod-only. apply: "build" is honored by Vite's plugin filtering before configEnvironment runs, matching the sibling vinext:client-css-url-assets-defaults plugin. Dev/preview are excluded.

I confirmed all of the above by invoking the hook across cases (client→null; rsc/ssr/worker default→{build:{minify:true}}; minify:false/true/'esbuild'→null) — all pass. tests/build-optimization.test.ts (82 tests) also still passes.

Non-blocking observations

  1. No test coverage for the new plugin. This is the one real gap. There's an established, directly-applicable pattern right next to this code: vinext:client-css-url-assets-defaults is unit-tested in tests/build-optimization.test.ts both at the hook level and through a full createBuilder() lifecycle (asserting the default applies to rsc/ssr and that a user value propagates to all envs). Given that the headline correctness property of this PR is "true default that yields to user config," a small test asserting:

    • configEnvironment("rsc", {}){ build: { minify: true } }
    • configEnvironment("client", {})null
    • configEnvironment("rsc", { build: { minify: false } })null

    would lock in the behavior cheaply and guard against regressions. Recommend adding it before merge.

  2. Minify without sourcemaps hurts prod worker debuggability. Server envs default sourcemap: false, so minified worker stack traces will reference mangled identifiers. The PR explicitly scopes sourcemaps out as a possible follow-up — that's a reasonable, deliberate decision, just flagging the tradeoff. Cloudflare Workers supports uploading sourcemaps, so a follow-up enabling sourcemap: "hidden" (or true) for server envs would restore readable traces without bloating the bundle.

  3. minify: true vs pinning a minifier — correct choice for this Vite 8 / rolldown+oxc repo; lets the native toolchain pick, consistent with the repo's "prefer oxc" guidance.

Verification summary

  • Inspected getDefaultEnvironmentOptions / runConfigEnvironmentHook (vite-plus-core) and createCloudflareEnvironmentOptions (@cloudflare/vite-plugin) — mechanism confirmed.
  • Exercised the hook directly across 8 cases — all behave as documented.
  • tests/build-optimization.test.ts — 82 passed, no regression.

LGTM aside from the missing test coverage, which I'd recommend adding given the behavior is non-obvious and easy to break.

@ask-bonk

ask-bonk Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #1777.

Summary

I reviewed PR #1777 (perf(build): minify server build environments by default) by verifying its claims against the actual dependency sources rather than taking the description at face value.

Verdict: Correct, no blocking issues. The implementation's central guarantee — a "true default that yields to user config" — holds.

What I verified

  • Vite mechanism (@voidzero-dev/vite-plus-core): confirmed getDefaultEnvironmentOptions() seeds each env's build from top-level config.build, and envs are merged before runConfigEnvironmentHook runs. So the config.build?.minify guard correctly defers to user settings (top-level, per-env, false, true, or string minifier).
  • Cloudflare coverage (@cloudflare/vite-plugin@1.31.0): confirmed it defines the worker env via the config hook and never sets minify, so vinext's configEnvironment correctly fills it — validating the Pages-Router-on-Cloudflare benefit.
  • Prod-only scoping: apply: "build" excludes dev/preview, matching the sibling plugin.
  • Empirical hook test: exercised the hook across 8 cases (all pass) and confirmed tests/build-optimization.test.ts (82 tests) still passes.

Findings (all non-blocking)

  1. No test coverage for the new plugin — the one real gap. A directly-applicable pattern exists right beside it (vinext:client-css-url-assets-defaults is fully tested). Recommended adding a small test before merge since the "yields to user config" behavior is the PR's headline property.
  2. Minify without sourcemaps hurts prod worker stack traces — flagged as a reasonable, explicitly-scoped follow-up.
  3. minify: true (vs pinning a minifier) is the right call for this rolldown/oxc repo.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk re-review for issues.

There's nothing to test in the pr - it's a self-explanatory plugin.

Sourcemaps should probs be opt-in by a user rather than on-by-default.

@james-elicx
james-elicx marked this pull request as ready for review June 5, 2026 23:08

@ask-bonk ask-bonk 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.

Re-review: perf(build): minify server build environments by default

Re-verified the mechanism against the actual @voidzero-dev/vite-plus-core@0.1.24 (Vite 8) source in node_modules and re-confirmed the change is correct, scoped, and safe to merge.

Mechanism re-confirmed (line refs in vite-plus-core's node.js)

  • getDefaultEnvironmentOptions(config) returns { build: config.build, ... } — each environment is seeded from the top-level config.build.
  • Resolution order: every env is mergeConfig(defaultNonClientEnvironmentOptions, config.environments[name]) before runConfigEnvironmentHook runs, and the hook's return value is merged back in (environments[name] = mergeConfig(environments[name], res)).
  • Therefore the guard if (config.build?.minify !== undefined) return null correctly sees and defers to any prior choice: top-level build.minify, environments.<name>.build.minify, or a value set by an earlier plugin (e.g. @cloudflare/vite-plugin, which I confirmed never sets minify on its worker env). It only fills true when still undefined.

apply: "build" keeps this prod-only (no dev/preview impact), matching the sibling vinext:client-css-url-assets-defaults plugin. The "true default that yields to user config" property holds.

On the two pieces of feedback

Tests — Agreed, withdrawing my earlier suggestion to add a unit test. The hook is a three-line, self-explanatory default and the behavior is already covered indirectly by the existing tests/build-optimization.test.ts lifecycle. Not a blocker.

Sourcemaps — Confirmed this PR does not touch sourcemaps at all (diff is minify-only, 1 file / +33). Server envs keep sourcemap: false, so nothing is turned on-by-default here. My earlier note only flagged a possible follow-up to add sourcemaps; per the steer, that follow-up should make sourcemaps opt-in by the user, not on-by-default. Nothing to change in this PR.

Verdict

LGTM. Correct, minimal, prod-scoped, and yields to explicit user config. No blocking issues.

@ask-bonk

ask-bonk Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Re-review posted to PR #1777.

Summary

I re-reviewed the minify-only change and verified the mechanism against the actual @voidzero-dev/vite-plus-core@0.1.24 source rather than the description.

Verdict: LGTM, no blocking issues.

  • Mechanism confirmed: getDefaultEnvironmentOptions seeds each env's build from top-level config.build (line 39908), and envs are merged from defaults before runConfigEnvironmentHook runs (lines 40127–40128). So the config.build?.minify !== undefined guard genuinely defers to any user/plugin choice — the "true default that yields to user config" property holds. apply: "build" keeps it prod-only.

On the two feedback items:

  • Tests — withdrew my earlier "add a unit test" recommendation per your steer; it's a self-explanatory three-line default already exercised by the existing build-optimization lifecycle tests.
  • Sourcemaps — confirmed this PR does not touch sourcemaps (diff is minify-only, 1 file / +33). Server envs stay sourcemap: false, so nothing is enabled on-by-default. Noted that any future sourcemap follow-up should be opt-in by the user, not on-by-default — but that's out of scope for this PR, nothing to change here.

github run

…unminified for output introspection

Minifying server environments by default exposed two problems:

1. Real bug: build-time route classification ran in `generateBundle`, but
   rolldown's minifier renames the top-level `__VINEXT_CLASS` function and
   mangles its `routeIdx` parameter before that hook runs, so the stub regex
   never matched and the patch silently no-op'd — every route fell back to the
   Layer 3 runtime probe in production. Move the injection to a `renderChunk`
   hook with `order: "pre"` so it runs before minification; the patched body is
   then minified along with the rest of the chunk (the runtime calls it by
   reference, not by name).

2. Test artifacts: three build tests introspect emitted server chunks for
   readable identifiers (registerConfiguredCacheAdapters, __VINEXT_CLASS) or
   depend on unminified chunking (MDX frontmatter leak check). These are
   test-only concerns — the runtime behavior is unchanged. Build their fixtures
   with `build.minify: false` (a user-overridable default) so the assertions
   stay deterministic.
The renderChunk refactor removed the external consumer of this type; keep it
as a local type so knip passes.
@james-elicx

Copy link
Copy Markdown
Member Author

Pushed two follow-ups since the last review — please re-review the full change:

  1. Real production bug fixed: build.minify renamed the __VINEXT_CLASS build-time route-classification marker before the old generateBundle patch ran, so build-time classification silently no-op'd in every minified production build. Moved the injection to a renderChunk hook with order: "pre" (runs before rolldown's minifier). Verified the marker is readable at patch time and buildTimeClassifications is wired on a real hackernews prod build.
  2. Unit tests: the three failing tests introspected build output for readable identifiers/markers; they now build their fixtures with minify: false (exercising the yields-to-user override). Audited the rest of the build-introspection tests — no others affected.
  3. Dropped a now-unused exported type to satisfy knip.

/bigbonk review for issues

@ask-bonk ask-bonk 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.

Re-review: minify server bundles + route-classification renderChunk fix

Re-reviewed the two follow-ups (906f25d, 2c379c0) on top of the original minify default. I verified the central claims against the actual @voidzero-dev/vite-plus-core@0.1.24 source and by building fixtures both minified and unminified.

Verdict: the production-bug fix is correct and necessary. No blocking issues. Two non-blocking items below (stale generateBundle references, and a test-coverage gap for the exact regression being fixed).

Verified: the bug was real and the fix works

I confirmed the failure mode and the fix against vite-plus-core:

  • resolveBuildEnvironmentOptions defaults minify to false for consumer === "server" (node.js:38634) and normalizes build.minify: true"oxc" (node.js:38649), which maps to real output minification (node.js:38764). So once vinext:server-minify-defaults fills minify: true on the rsc env, the rolldown/oxc minifier does mangle the top-level __VINEXT_CLASS name.
  • output.minify is a rolldown output-level option applied after all renderChunk plugin hooks, so a renderChunk with order: "pre" legitimately sees the unminified stub. The old generateBundle ran post-minify, where CLASS_STUB_RE (/function __VINEXT_CLASS\(routeIdx\).../) could never match.

I reproduced both states by building a minimal force-static fixture through createBuilder().buildApp():

  • Minified (default) build: the emitted RSC chunk has __VINEXT_CLASS renamed away (no readable name survives) and carries a populated dispatch — the runtime consumer reads n?.buildTimeClassifications?.get(r) and branches on i===`static`?`s`:`d`. So classification is injected and wired. (Note the minifier rewrites string quotes to backticks — worth keeping in mind for any future output-introspection assertion.)
  • minify: false build: readable __VINEXT_CLASS dispatch present, as the integration test asserts.

This confirms the headline claim: build-time classification now survives the default minified prod build, and previously silently no-op'd.

Non-blocking: the regression test does not exercise the bug it fixes

The three updated build-introspection tests now build with minify: false (+ per-env overrides), which is a sound way to keep the readable-identifier introspection deterministic. But it means tests/build-time-classification-integration.test.ts — the test that exists specifically to prove the dispatch table is populated — runs in the one configuration where the old buggy generateBundle code would also have passed. The actual production default (minify on) is no longer covered by any automated test, even though "classification silently no-op's under minify" is exactly the regression this PR fixes.

Recommend adding one small assertion that builds the same fixture with the default (minified) settings and verifies a populated dispatch was injected (e.g. that the chunk contains the classification branch / buildTimeClassifications consumer with non-null values, tolerating backtick-quoted string literals). That locks in the fix; without it, a future revert to generateBundle would stay green. I verified such a check passes today, so it's cheap to add.

Non-blocking: stale generateBundle references after the hook move

Several comments and user-facing error strings still say generateBundle for the route-classification path:

  • packages/vinext/src/build/route-classification-injector.ts:84 and :150 — the drift-diagnostic Error messages ("The generator and generateBundle have drifted"). These are the ones I'd fix, since they'd mislead anyone debugging a real drift failure; the hook is now renderChunk.
  • packages/vinext/src/index.ts:777 ("consumed in generateBundle") and :784 ("Shared with the Layer 2 generateBundle hook below").
  • packages/vinext/src/entries/app-rsc-entry.ts:385, :397 ("Replaced in generateBundle").
  • packages/vinext/src/build/route-classification-manifest.ts:203, packages/vinext/src/build/layout-classification.ts:130, and tests/build-time-classification-integration.test.ts:7,38,191.

(The remaining generateBundle hook at index.ts:2647 is the unrelated CSS-url-assets-restore hook — correct, leave it.)

Minor robustness observation (not a regression)

Moving from whole-bundle generateBundle to per-chunk renderChunk changes the drift invariant slightly: findClassificationChunk now receives a single chunk per call. If a chunk ever references __VINEXT_CLASS (a call site) without containing the stub body, it would throw "no chunk contains the stub body." Today codegen keeps the stub definition and all __VINEXT_CLASS(N) call sites in the same RSC entry module, so this can't split — but it's marginally less robust than the old whole-bundle scan. Fine as-is; just noting it.

Other items confirmed

  • knip change (2c379c0): RouteClassificationChunk is now internal to the injector module and only referenced within it (the dist/ export is stale build output). Correct.
  • vp check on the changed source files: format + lint + types pass.
  • tests/build-time-classification-integration.test.ts (8), tests/cache-adapters-build.test.ts (1), tests/tsconfig-path-alias-build.test.ts (2), tests/build-optimization.test.ts (82) all pass.

LGTM to merge. I'd strongly suggest the minified-default regression test before merge given the whole point of this PR; the stale generateBundle strings are a nice-to-have cleanup.

@ask-bonk

ask-bonk Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #1777.

Summary

I re-reviewed both follow-ups (906f25d, 2c379c0) and verified the central claims against the actual @voidzero-dev/vite-plus-core@0.1.24 source and by building fixtures both minified and unminified.

Verdict: the production-bug fix is correct and necessary. No blocking issues.

What I verified

  • The bug was real: vite-plus-core defaults server envs to minify: false, normalizes minify: true"oxc" (real minification), and output.minify runs after all renderChunk hooks. So the old generateBundle (post-minify) regex genuinely could never match once the minify default kicked in. The renderChunk + order: "pre" move legitimately runs pre-minify.
  • The fix works: I built a force-static fixture through createBuilder().buildApp() with the default (minified) settings — confirmed __VINEXT_CLASS is renamed away yet a populated dispatch table is injected and the runtime consumer branches on it. With minify: false, the readable dispatch is present as the test asserts.

Findings (all non-blocking)

  1. The regression test doesn't exercise the bug it fixes. The three updated tests now build with minify: false — the one config where the old buggy code would also have passed. The production default (minify on) is no longer covered. I recommended adding one assertion that builds the fixture minified and verifies a populated dispatch (I confirmed it passes today). Without it, a revert to generateBundle stays green.
  2. Stale generateBundle references in comments and — more importantly — two user-facing drift-diagnostic Error strings (route-classification-injector.ts:84,150) that now name the wrong hook.
  3. Minor robustness note: the per-chunk renderChunk is marginally less robust than the old whole-bundle scan if call sites ever split from the stub body (can't happen with current codegen).

The knip change is correct, vp check passes on the changed files, and the four relevant test files (93 tests) all pass.

github run

@james-elicx
james-elicx marked this pull request as draft June 5, 2026 23:30
…ndle refs

Re-add real regression coverage that the route-classification injector runs
before minification (the production default). The prior fix made three tests
pass by building with minify:false, which is exactly the config where the old
post-minify generateBundle code also passed — so the tests no longer guarded
the bug.

- classification integration: add a production-default (minify on) suite that
  asserts minify-robust signals (injected switch dispatch shape + the
  module-graph reason literal, which is build-time-only). Verified it FAILS when
  the injector is reverted to a post-minify generateBundle hook.
- cache adapters: assert the minify-safe LOCAL_ADAPTER_MARKER under the default
  minified build instead of grepping the renamed registerConfiguredCacheAdapters
  symbol.
- tsconfig path alias (#659): run minified; the old not.toContain('title:
  "Second Post"') was a quote-escaping artifact (frontmatter renders as MDX
  content identically in both builds, not a minify leak) — replaced with a real
  parse-error/raw-YAML guard.
- update stale generateBundle references to renderChunk in injector error
  strings, app-rsc-entry stubs, and index.ts comments; note the per-chunk patch
  assumption near the renderChunk hook.
@james-elicx

Copy link
Copy Markdown
Member Author

Addressed both findings from the last review (79f7df4a):

1. Regression test now exercises the bug (un-masked). Added a "production default — minify on" suite to build-time-classification-integration.test.ts that builds the fixture with server minification LEFT ON and asserts the dispatch table is populated via minify-robust signals: the structural case N: return new Map([[idx, … shape, and (debug build) the module-graph reason literal — which is injected only by the build-time patch (note: segment-config/no-classifier also appear in bundled runtime source, so they aren't unique; module-graph is).
Verified it actually guards the bug: I reproduced the original failure faithfully — moving the patch back to a post-minify generateBundle hook — and confirmed both new assertions FAIL there, then pass once the renderChunk fix is restored. (Note: flipping order:"pre""post" alone does not reproduce it — rolldown runs the minifier after all renderChunk handlers regardless of order — so the repro uses generateBundle.)
cache-adapters-build and tsconfig-path-alias-build now also run under the production default (minify on): the former asserts via a surviving string-literal marker; the latter's old not.toContain('title: "Second Post"') turned out to be a quote-escaping artifact (it only passed unminified by accident), replaced with a real #659 guard (compiled JSX present, no raw YAML frontmatter block).

2. Stale generateBundle references fixed — the two drift Error strings + comments in route-classification-injector.ts, the stub comments in app-rsc-entry.ts, and two comments in index.ts now name renderChunk. Added a comment on the per-chunk assumption (finding #3).

vp run check + vp run knip clean; affected + regression tests green (1116 passed).

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk re-review for issues

@ask-bonk ask-bonk 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.

Re-review: minify server bundles + renderChunk classification fix + regression coverage

Re-reviewed the latest follow-up (79f7df4) addressing the two findings from my prior review. I verified the central claims empirically — by reproducing the original bug and confirming the new regression suite catches it — rather than taking the description at face value.

Verdict: LGTM, no blocking issues. The earlier finding #1 (the regression test didn't exercise the bug it fixes) is now genuinely resolved.

Verified: the new minify-on suite actually guards the bug

I reproduced the original failure faithfully by temporarily moving the injector back to a post-minify generateBundle hook (which is the configuration the prior minify: false-only tests would have stayed green under). With that revert in place, the new "production default — minify on" suite FAILED — specifically the module-graph reason-literal assertion, which received the fully-minified RSC chunk with no module-graph literal present, confirming the dispatch table was never injected. Restoring the renderChunk + order: "pre" fix makes it pass. So the suite is a real guard, not a tautology.

I also confirmed the uniqueness claim the test relies on: module-graph as a string literal appears only in build-time modules (route-classification-manifest.ts:144, layout-classification.ts, layout-classification-types.ts, report.ts) and is absent from every server/* runtime module that gets bundled into the RSC chunk (app-page-dispatch.ts / app-page-execution.ts carry only segment-config / no-classifier). So a passing module-graph match is conclusive proof the build-time reasons patch ran. Good choice of signal.

The structural case \d+: return new Map([[\d+, assertion is likewise minify-robust (string contents and switch structure survive mangling) and absent from the untouched return null stub.

Verified: minify-defaults plugin and renderChunk move

  • The vinext:server-minify-defaults plugin is unchanged from the version I confirmed earlier against @voidzero-dev/vite-plus-core@0.1.24: getDefaultEnvironmentOptions seeds each env's build from top-level config.build, envs are merged before runConfigEnvironmentHook, so the config.build?.minify !== undefined guard genuinely defers to any user/plugin choice (incl. false). apply: "build" keeps it prod-only. The "true default that yields to user config" property holds.
  • The renderChunk + order: "pre" move is correct: rolldown's output.minify runs after all renderChunk hooks, so the stub is still readable when patched, and the runtime calls the function by reference (not by name) so post-patch minification is safe. The per-chunk assumption is now documented with a clear "revisit if codegen splits call sites" note.

Findings (all non-blocking)

  1. Two stale generateBundle references remain — my prior review listed these and they were not included in the cleanup commit (the commit message enumerated route-classification-injector.ts, app-rsc-entry.ts, and index.ts, but missed these two):

    • packages/vinext/src/build/route-classification-manifest.ts:203 — "suitable for embedding into the generated RSC entry via generateBundle" (now renderChunk). The exported function is also still named buildGenerateBundleReplacement (line 209) — comment fix is enough; renaming the symbol is optional churn.
    • packages/vinext/src/build/layout-classification.ts:130@internal doc says "The generateBundle hook in index.ts calls classifyLayoutByModuleGraph directly" (now renderChunk).

    Pure documentation drift; would mislead the next person tracing the classification path. Cheap to fix in this PR or a follow-up.

Verification summary

  • Reproduced the bug (post-minify generateBundle) → new minify-on suite FAILS on the module-graph assertion; restored fix → passes. The regression is genuinely guarded.
  • Confirmed module-graph is a unique build-time-only signal (absent from bundled server/* runtime).
  • tests/build-time-classification-integration.test.ts (10), tests/cache-adapters-build.test.ts (1), tests/tsconfig-path-alias-build.test.ts (2), tests/build-optimization.test.ts (82) — all pass.
  • vp check on the three changed source files — format + lint + types pass.

LGTM to merge. The two stale generateBundle doc references are the only nit; not a blocker.

@ask-bonk

ask-bonk Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #1777.

Summary

I re-reviewed the latest follow-up (79f7df4) that addressed my two prior findings, and verified the central claims empirically rather than trusting the description.

Verdict: LGTM, no blocking issues. The key prior finding — that the regression test didn't exercise the bug it fixes — is now genuinely resolved.

What I verified

  • The new minify-on suite is a real guard. I reproduced the original bug by moving the injector back to a post-minify generateBundle hook (the exact config the old minify: false-only tests stayed green under). The new "production default — minify on" suite FAILED on the module-graph reason-literal assertion; restoring the renderChunk + order: "pre" fix makes it pass. Not a tautology.
  • The module-graph signal is genuinely unique. Confirmed it appears only in build-time modules and is absent from every bundled server/* runtime module (app-page-dispatch.ts / app-page-execution.ts carry only segment-config / no-classifier). So a passing match conclusively proves the build-time patch ran.
  • The minify-defaults plugin and renderChunk move are correct — re-confirmed against vite-plus-core@0.1.24; the "true default yields to user config" property holds, and output.minify runs after all renderChunk hooks so the pre-minify patch is sound.
  • All four relevant test files (95 tests) pass; vp check clean on the changed source.

Findings (non-blocking)

  1. Two stale generateBundle doc references missed by the cleanup commit: route-classification-manifest.ts:203 and layout-classification.ts:130. Pure documentation drift — would mislead someone tracing the classification path. Cheap to fix here or as a follow-up.

The bug-reproduction edits I made during review were reverted; the working tree is clean.

github run

…ify:false)

Previously the dispatch-logic suites built the fixture with minify:false so they
could regex-extract and eval the readable __VINEXT_CLASS function — but that is
the one config where the original post-minify-hook bug also passes, so it masked
the regression. Rewrite the extractors to be name-agnostic: derive the (mangled)
dispatch function name from its property-keyed call site (__buildTimeClassifications/
__buildTimeReasons — property keys survive minification) and eval the body
(string-literal values like "static" also survive). All suites now build with
vinext's production defaults (minify on). Verified by reproducing the bug
(disabling the patch): the minified tests fail with "was not patched".
@james-elicx

Copy link
Copy Markdown
Member Author

Removed the test-only minify disabling entirely (71e86d21). No test builds with minify: false anymore — every classification suite now builds with vinext's production defaults (minify on), which directly addresses the masking concern from the last review.

Instead of disabling minify to keep __VINEXT_CLASS readable, the extractors are now name-agnostic: they derive the (mangled) dispatch function name from its property-keyed call site in the route table (__buildTimeClassifications: / __buildTimeReasons: — object property keys are never mangled), then eval the body (string-literal values like "static" / "dynamic" / "module-graph" also survive minification). So the full dispatch-correctness coverage (force-static→static, force-dyn→dynamic, next/headers omitted, reason layers) now runs against the real minified shipping output.

Proved it actually guards the bug: I reproduced the original failure by disabling the renderChunk patch, rebuilt minified, and the suites FAIL with <mangled> was not patched — still returns null unconditionally (the name came through as $S, confirming the name-agnostic recovery works on minified code). Restored, all 11 pass. vp run check + vp run knip clean.

/bigbonk review for issues

@ask-bonk ask-bonk 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.

Re-review: classification tests now run against minified output (no minify: false)

I verified the latest commit (71e86d2) empirically — extracting and evaluating the dispatch functions from the actual minified RSC chunks the test produces, and reproducing the regression-guard behavior — rather than trusting the description.

Verdict: the change is correct and is a genuine improvement over the prior minify: false approach. No blocking issues. This is the right fix for the masking concern: the suites now exercise the real shipping path.

Verified empirically

  • Name-agnostic recovery works on real minified output. Built the fixture with production defaults; the dispatch function is emitted as function $S(e){return(e=>{switch(e){case 0:return new Map([[0,\static`]]);...default:return null}})(e)}. classDispatchNamecorrectly recovers$Sfrom the unmangled__buildTimeClassifications:$S(0)property-keyed call site, andevalDispatchFn's regex (function\s+${esc}\s*(\s*(\w+)\s*)\s*{\sreturn\s([\s\S]?)(\s\1\s*)\s*}) captures the body cleanly. The non-greedy [\s\S]*?correctly skips the innerswitch(e)(it's(e){, not (e)}) and terminates at the real })(e)}self-call. Evals to a working dispatch returning the correctMap` per route.
  • The reasons (debug) path works too. Recovered the mangled reasons fn (eC) and confirmed it evals to [[0,{layer:"module-graph",result:"static"}],[1,{layer:"segment-config",key:"dynamic",value:"force-dynamic"}]] for /force-dyn — string literals ("static", "module-graph") survive minification verbatim, even nested in object literals with backtick-quoted values.
  • The regression guard genuinely fires. Simulated an unpatched minified stub (function $S(e){return null} with a __buildTimeClassifications:$S(0) call site): classDispatchName derives $S, then nullStubRe matches and evalDispatchFn throws $S was not patched — still returns null unconditionally. So a revert to a post-minify hook fails every suite, exactly as claimed. This is the property the prior minify: false tests had lost.
  • All tests pass. tests/build-time-classification-integration.test.ts (8), tests/cache-adapters-build.test.ts (1), tests/tsconfig-path-alias-build.test.ts (2) = 11 passed. vp check on the three changed test files is clean; vp run knip is clean.
  • The two adapted tests are well-reasoned. cache-adapters-build keying off the LOCAL_ADAPTER_MARKER string literal instead of the renamed registerConfiguredCacheAdapters symbol is the correct minify-safe signal. The tsconfig-path-alias (#659) change is a nice catch — the old not.toContain('title: "Second Post"') only ever passed because unminified output escaped the inner quotes (title: \"Second Post\"), so it was a quote-style artifact, not a real leak guard; the replacement raw-YAML-frontmatter check (/^---\s*$[\s\S]*?title:/m) is a genuine #659 guard.

Findings (non-blocking)

  1. Two stale generateBundle doc references from my prior review are still present — the cleanup commit (79f7df4) missed them and this commit only touched the test file:

    • packages/vinext/src/build/route-classification-manifest.ts:203 — "suitable for embedding into the generated RSC entry via generateBundle" (now renderChunk).
    • packages/vinext/src/build/layout-classification.ts:130@internal doc: "The generateBundle hook in index.ts calls classifyLayoutByModuleGraph directly" (now renderChunk).

    Compounding this, the exported symbol buildGenerateBundleReplacement (manifest.ts:209, referenced at :219 and :226) still carries the old hook name in its identifier. Pure naming/doc drift — no functional impact — but it will mislead the next person tracing the classification path. Cheapest fix is the two doc comments; the symbol rename is optional churn.

  2. extractRouteIndexByPattern coupling (informational, pre-existing). The __buildTimeClassifications: <name>(N) [\s\S]*? pattern: "..." pairing relies on pattern: appearing after the call site within each route object and no intervening __buildTimeClassifications:. Holds for current codegen (verified the four routes map correctly) and the non-greedy match grabs the nearest pattern:. If codegen ever reorders pattern: before the classification field, indices would silently shift by one route. Not a regression in this PR (same coupling existed before), just noting the fragility now that minify reshapes output.

The first item is the only thing I'd suggest fixing, and it's a doc nit, not a blocker. Net: this commit removes the masking and makes the suite a real guard against the original post-minify bug. LGTM.

@ask-bonk

ask-bonk Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #1777.

Summary

I re-reviewed the latest commit (71e86d2) — which removes minify: false entirely so all classification suites build against vinext's production defaults — and verified the claims empirically rather than trusting the description.

Verdict: correct and a genuine improvement. No blocking issues. This properly addresses the masking concern from the previous review.

What I verified (empirically, against real minified output)

  • Name-agnostic recovery works. Confirmed the mangled dispatch name ($S) is correctly recovered from the unmangled __buildTimeClassifications:$S(0) property-keyed call site, and evalDispatchFn's regex captures and evals the body cleanly — the non-greedy match correctly skips the inner switch(e) and terminates at the real })(e)} self-call.
  • The reasons/debug path works — recovered the mangled reasons fn and confirmed it evals to the correct module-graph/segment-config reasons; string literals survive minification verbatim.
  • The regression guard genuinely fires — simulated an unpatched minified stub and confirmed evalDispatchFn throws <name> was not patched, so a revert to a post-minify hook fails every suite (the property the prior minify: false tests had lost).
  • All 11 tests pass, vp check + vp run knip clean.
  • The cache-adapters and tsconfig-path-alias (rsc:scan-strip parses MDX files as JS before @mdx-js/rollup transforms them, causing parse errors #659) adaptations are well-reasoned — the rsc:scan-strip parses MDX files as JS before @mdx-js/rollup transforms them, causing parse errors #659 fix correctly identifies the old assertion as a quote-escaping artifact and replaces it with a real raw-YAML guard.

Findings (non-blocking)

  1. Two stale generateBundle doc references from my prior review are still present (route-classification-manifest.ts:203, layout-classification.ts:130), plus the exported symbol buildGenerateBundleReplacement still carries the old hook name. Pure doc/naming drift, no functional impact.
  2. Informational: extractRouteIndexByPattern's call-site→pattern: non-greedy pairing is slightly fragile if codegen ever reorders fields (pre-existing, not introduced here).

Only the doc nit is worth fixing, and it's not a blocker.

github run

…erateBundle doc refs

Address review nits: the classification dispatch builder still carried the old
hook name (it now runs in renderChunk, not generateBundle). Rename it to
buildClassificationReplacement (parallel to buildReasonsReplacement) and fix two
stale 'generateBundle' doc comments (route-classification-manifest.ts:203,
layout-classification.ts:130) to say renderChunk.
@james-elicx

Copy link
Copy Markdown
Member Author

Addressed the remaining nits (97c29dea):

Finding #1 (doc/naming drift):

  • Renamed the misnamed export buildGenerateBundleReplacementbuildClassificationReplacement (parallel to its sibling buildReasonsReplacement), updating its import/usage in route-classification-injector.ts and its test in route-classification-manifest.test.ts.
  • Fixed the two stale generateBundle doc comments: route-classification-manifest.ts:203 and layout-classification.ts:130 now say renderChunk.
  • (The one remaining generateBundle mention, in route-classification-injector.ts:55, is intentional — it explains why not to patch in generateBundle.)

Finding #2 (extractRouteIndexByPattern non-greedy pairing): left as-is — it's pre-existing and, importantly, self-checking: if the call-site→pattern: pairing ever desynced, the dispatch-correctness assertions would fail loudly (e.g. /force-static would resolve to the wrong route's Map and map.get(1).toBe("static") would break). So a codegen reorder can't silently mask a wrong mapping.

vp run check + vp run knip clean; classification + manifest + layout-classification tests green (37 passed).

/bigbonk review for issues

@ask-bonk ask-bonk 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.

Re-review: rename buildGenerateBundleReplacement + fix stale generateBundle doc refs (97c29de)

Verified the latest commit against the working tree rather than the description.

Verdict: LGTM, no blocking issues. Both findings from my prior review are now resolved.

What I verified

Finding #1 (doc/naming drift) — resolved.

  • The rename buildGenerateBundleReplacementbuildClassificationReplacement is complete and consistent: the declaration, both self-referential doc comments (the "Sibling of…" and "Layer 1 priority…" comments on buildReasonsReplacement), the import + call site in route-classification-injector.ts, and all 8 references in tests/route-classification-manifest.test.ts (including the describe block, the evalDispatch error string, and the three Parameters<typeof …> type annotations) are updated. grep finds zero buildGenerateBundleReplacement references in packages/vinext/src/ or tests/ — the only remaining hits are in packages/vinext/dist/ (stale build output, regenerated on next build). The new name parallels its sibling buildReasonsReplacement cleanly.

  • The two stale doc comments are fixed: route-classification-manifest.ts:203 now says "via the renderChunk hook" and layout-classification.ts:130 now says "The renderChunk hook in index.ts".

  • I audited every remaining generateBundle mention in packages/vinext/src/. All are legitimate:

    • route-classification-injector.ts:55 — the intentional "Patching in generateBundle (post-minify) would silently…" rationale comment the author flagged. Correct to keep.
    • index.ts:2512–2536 — the renderChunk handler's own comments, which explicitly explain why not generateBundle. Correct.
    • index.ts:1598,2603,2655 + css-url-assets.ts:23 — the unrelated CSS-url-assets restore hook (genuinely a generateBundle hook). Correct.

    So no misleading drift remains.

Finding #2 (extractRouteIndexByPattern non-greedy pairing) — agreed, leave as-is. This is pre-existing (not introduced by this PR) and, as the author notes, self-checking: the dispatch-correctness assertions key off the recovered Map contents, so a desynced call-site→pattern: pairing would surface as a wrong-route assertion failure rather than silently passing. A codegen reorder can't mask a wrong mapping without breaking a test. Not worth churn here.

Verification summary

  • tests/route-classification-manifest.test.ts (16) + tests/build-time-classification-integration.test.ts (8) — 24 passed.
  • vp check on the four changed source/test files — format + lint + types clean.
  • No source/test references to the old symbol name remain (dist/ excluded — stale build artifact).

Pure rename + doc cleanup, no functional change. Ready to merge.

@ask-bonk

ask-bonk Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #1777.

Summary

I reviewed the latest commit (97c29de) that addressed the two remaining nits from my prior review, verifying against the actual working tree.

Verdict: LGTM, no blocking issues. Both findings are resolved.

What I confirmed

Finding #1 (doc/naming drift) — fully resolved.

  • The buildGenerateBundleReplacementbuildClassificationReplacement rename is complete and consistent across the declaration, both self-referential doc comments, the import/call site in route-classification-injector.ts, and all 8 references in the test file. grep confirms zero stale references in src/ or tests/ (only dist/ build output, which regenerates).
  • Both stale doc comments are fixed (route-classification-manifest.ts:203, layout-classification.ts:130 now say renderChunk).
  • I audited every remaining generateBundle mention in source — all are legitimate: the intentional "why not generateBundle" rationale comments, and the unrelated CSS-url-assets restore hook (which genuinely is a generateBundle hook).

Finding #2 (extractRouteIndexByPattern pairing) — agreed to leave as-is. Pre-existing and self-checking: a desynced pairing would trip a dispatch-correctness assertion rather than silently pass.

Verification

  • Classification + manifest tests: 24 passed.
  • vp check on the four changed files: format + lint + types clean.

Pure rename + doc cleanup, no functional change.

github run

@james-elicx
james-elicx marked this pull request as ready for review June 6, 2026 21:54
@james-elicx
james-elicx merged commit 0b7721d into main Jun 6, 2026
80 of 82 checks passed
@james-elicx
james-elicx deleted the fix/minify-server-bundles branch June 6, 2026 21:55
@github-actions github-actions Bot mentioned this pull request Jun 6, 2026
james-elicx added a commit that referenced this pull request Jun 7, 2026
…it (#1810)

* fix(build): write BUILD_ID via writeBundle so App Router builds emit it

The vinext:build-id plugin used closeBundle, which does not fire during
the multi-environment createBuilder().buildApp() pipeline used for App
Router production builds. As a result dist/server/BUILD_ID was silently
never written for pure App Router apps — only hybrid apps (which run a
second vite.build() pass) and Pages Router apps got the file.

This was masked until server bundles started being minified by default
(#1777): the e2e deploy harness (scripts/e2e-deploy.sh) falls back to
regex-parsing the buildId out of dist/server/index.js when BUILD_ID is
absent, and minification mangles the `get buildId()` / `buildId = "..."`
patterns the regex relies on. The result was ~700 App Router deploy-suite
tests failing at setup with "Failed to extract build ID from
dist/server/index.js".

Switch the plugin to writeBundle (mirroring the working vinext:image-config
plugin), which fires for every emitted bundle; the existing one-time write
guard ensures the file is written exactly once. Add a regression assertion
to the App Router production build test.

* fix(build): share one build ID across all plugin instances in a build

A single `vinext build` can instantiate vinext() more than once: the App
Router multi-environment build (createBuilder().buildApp()) and the separate
Pages Router SSR build for hybrid app+pages apps are distinct plugin
instances. With no user generateBuildId, each instance resolved its own
random UUID, so the App Router runtime, the Pages Router runtime, the
prerender manifest, and dist/server/BUILD_ID could each get a different
build ID — risking ISR/seed-cache key mismatches for hybrid apps.

The CLI now resolves the build ID once (honoring the user's generateBuildId)
and publishes it via __VINEXT_SHARED_BUILD_ID. The plugin adopts it unless
the user supplied their own generateBuildId (which is authoritative and
already shared because every instance calls it). resolveBuildId()'s
standalone semantics are unchanged, so the build-only coordination never
leaks into dev or tests.

Verified on the hybrid app-router-cloudflare example: the App Router RSC/SSR
runtime, Pages entry, prerender manifest, and BUILD_ID file now all carry the
same build ID. Adds a coordination test alongside the BUILD_ID emission guard.

* fix(build): always adopt shared build ID, even with generateBuildId

Addresses review on #1810. The previous `!rawConfig?.generateBuildId` guard
reintroduced the divergence it was meant to fix: when generateBuildId returns
null (documented Next.js behavior) or is non-deterministic, resolveBuildId()
mints a fresh random UUID per plugin instance, so a hybrid app+pages build
still got divergent IDs across the buildApp() and Pages vite.build() instances.

The CLI's resolvedNextConfig.buildId is already the fully-resolved authoritative
value (it ran resolveBuildId honoring generateBuildId, including the null→UUID
fallback), so the plugin now always adopts the shared ID when set. Add a
regression test using generateBuildId: () => null + a shared ID, and document
that the CLI intentionally does not clear the env var (the build process exits).
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