Skip to content

fix(pages): preserve fast refresh state - #2544

Merged
james-elicx merged 2 commits into
mainfrom
codex/fix-pages-fast-refresh
Jul 6, 2026
Merged

fix(pages): preserve fast refresh state#2544
james-elicx merged 2 commits into
mainfrom
codex/fix-pages-fast-refresh

Conversation

@james-elicx

@james-elicx james-elicx commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary

  • preserve Pages Router Fast Refresh state instead of forcing full document reloads for ordinary script edits
  • consume Pages SSR dead-end updates after downstream HMR plugins run, while retaining full reloads for route additions and deletions
  • preload CSS introduced by client transforms, including virtual CSS modules, so dev HTML has blocking first-paint styles
  • compile MDX before the development React transform and extend the default dev-only React include so MDX routes also receive Fast Refresh without changing production parsing
  • add unit and browser regression coverage for state preservation, syntax-error recovery, transformed CSS, and MDX route updates

Motivation

DigitecGalaxus/next-yak#569 added vinext to its bundler compatibility suite and exposed two dev-only gaps:

  • seven HMR cases failed because vinext reloaded the document for every Pages Router script edit, clearing browser state
  • pseudo-element styles raced the first computed-style read because next-yak's transformed virtual CSS was only injected during hydration

The HMR behavior now matches Next.js Pages Router Fast Refresh. Dev stylesheet discovery now follows Vite's transformed client module graph and emits blocking stylesheet links for generated CSS, matching production's first-paint behavior.

The automatic React integration now uses command-scoped plugin instances: production keeps the existing configured include behavior, while development adds .mdx only when the user did not explicitly configure react.include. The MDX delegate is split into an early transform proxy and a later config proxy so compiled JSX reaches React Fast Refresh while the delegate is still initialized during vinext:config.

Validation

  • vp check packages/vinext/src/index.ts packages/vinext/src/server/dev-server.ts tests/pages-router.test.ts tests/fixtures/pages-basic/pages/hmr-state.tsx tests/e2e/pages-router/hmr.spec.ts
  • targeted Pages Router transformed-CSS tests — pass
  • Pages Router HMR Playwright regression — pass
  • MDX Pages Router Fast Refresh regression — pass
  • failed CI Vitest shard (integration 4/10) — 398/398 pass locally
  • next-yak vinext-pages dev matrix — 27/27 pass, including all seven HMR cases and pseudo-elements

Related: DigitecGalaxus/next-yak#569

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@pkg-pr-new

pkg-pr-new Bot commented Jul 6, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2544
npm i https://pkg.pr.new/create-vinext-app@2544
npm i https://pkg.pr.new/vinext@2544

commit: 40e9096

@github-actions

github-actions Bot commented Jul 6, 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

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 40e9096 against base 7e1ff08 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 126.3 KB 126.3 KB ⚫ +0.0%
Client entry size (gzip) vinext 120.3 KB 120.3 KB ⚫ +0.0%
Dev server cold start vinext 2.62 s 2.66 s ⚫ +1.4%
Production build time vinext 3.11 s 3.13 s ⚫ +0.6%
RSC entry closure size (gzip) vinext 97.5 KB 97.5 KB ⚫ -0.0%
Server bundle size (gzip) vinext 164.0 KB 164.0 KB ⚫ +0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@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: fix(pages): preserve fast refresh state

Reviewed the full diff against main. The core mechanism is sound and the change matches Next.js Pages Router Fast Refresh behavior. Summary of what I verified and a few minor notes.

What the change does (verified correct)

The old hotUpdate hook force-sent full-reload for _app/page/script edits, wiping browser state. The new approach:

  1. Converts hotUpdate to the object form with order: "post" so it runs after @vitejs/plugin-react, letting client React Fast Refresh handle the client update.
  2. For the SSR environment only, it manually invalidates the changed modules and return [], which empties the SSR env's module list. This is the key trick: without it, Vite's updateModules would hit a dead-end boundary on the SSR page module and fall back to a full-reload (see vite-plus-core updateModules -> needFullReload). By consuming the SSR modules post-hook, the hmr() loop early-returns with "no modules matched" and no reload is sent. The manual invalidateModule(mod, new Set(), timestamp, true) mirrors Vite's own invalidation call, so the SSR entry re-renders fresh HTML on the next request.
  3. Route add/delete still triggers full reloads for all three modes: pages-only (server.ws.send at the add/unlink watcher handlers), hybrid (invalidateHybridClientEntries), and app-only (invalidateRscEntryModule). Only content edits are now state-preserving. This matches Next.js.

Parity check

  • _app edits: previously forced full-reload; now preserved. isPotentialPagesAssetGraphScript returns true for _app.tsx (it's a script under root, outside appDir), so the SSR-invalidation branch fires for it. Correct.
  • Stylesheet edits: pagesAssetGraphChanged invalidates client assets, but pagesAssetGraphScriptChanged is false, so the SSR branch is skipped and Vite's CSS HMR handles it. Correct.
  • pagesRunner.clearCache(): the runner cache is still cleared for every relevant script edit via the watcher change handler's invalidatePagesClientAssetsModule() (the pagesAssetGraphScriptChanged condition matches). No stale-SSR risk introduced. Correct.
  • Dev-only concern; prod-server / worker-entry are untouched, so dev/prod parity holds.

Test results

  • vp check packages/vinext/src/index.ts tests/pages-router.test.ts — clean (format, lint, types).
  • New unit test does not force full reload for Pages Router Fast Refresh updates passes and asserts the order: "post", no full-reload on script/app/shared edits, the SSR return [] + invalidateModule(..., true), and the route add/unlink reloads. Good coverage.
  • The one failing test in the full tests/pages-router.test.ts run is a pre-existing ECONNREFUSED 127.0.0.1:5173 connection error unrelated to this change (sandbox networking), not a regression from this PR.

Minor notes (non-blocking)

  1. E2E test mutates a committed fixture (tests/fixtures/pages-basic/pages/hmr-state.tsx). It's restored in finally, but if the process is hard-killed mid-run the fixture is left dirty. This is an accepted pattern for HMR tests, but worth being aware of when running the suite locally.
  2. page.waitForTimeout(1_000) in the syntax-error recovery step is a fixed sleep and could be flaky on slow CI. Next.js's own error-recovery util uses a similar wait, so this mirrors upstream, but consider replacing with a poll on the error overlay disappearing if it flakes.
  3. New static fixture page hmr-state.tsx is consistent with the many existing static pages in pages-basic and does not appear in any exhaustive route/snapshot assertion, so it should not break other suites.

Overall this is a clean, well-scoped fix with good test coverage. LGTM pending CI (full Vitest + Playwright).

@ask-bonk

ask-bonk Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2544.

Summary

I reviewed PR #2544 (fix(pages): preserve fast refresh state) end-to-end. The change is well-designed and correct.

Core mechanism (verified correct):

  • Converts the Pages Router hotUpdate hook to object form with order: "post" so it runs after @vitejs/plugin-react, letting client React Fast Refresh handle updates.
  • For the SSR environment, it manually invalidates the changed modules and returns [], emptying the SSR module list so Vite's updateModules doesn't hit a dead-end boundary and fall back to a full-reload. The manual invalidateModule(mod, new Set(), timestamp, true) mirrors Vite's own invalidation, so SSR re-renders fresh HTML on the next request.
  • Route add/delete still triggers full reloads for all modes (pages-only, hybrid, app-only). Only content edits become state-preserving — matching Next.js.

Parity verified: _app edits, stylesheet edits, pagesRunner cache clearing (still handled via watcher change), and dev/prod parity (dev-only change) all check out.

Tests: vp check is clean; the new unit test passes and has solid coverage. The one failing test in the full run is a pre-existing ECONNREFUSED:5173 sandbox networking issue, not a regression.

Minor non-blocking notes flagged in the review: the e2e test mutates a committed fixture (restored in finally), a fixed waitForTimeout(1000) that could flake, and the new static fixture page is safe against existing assertions.

Overall: LGTM pending CI.

github run

@james-elicx
james-elicx marked this pull request as ready for review July 6, 2026 09:22
@james-elicx
james-elicx marked this pull request as draft July 6, 2026 11:00
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx
james-elicx force-pushed the codex/fix-pages-fast-refresh branch from 8e2b5e6 to 6cfbb21 Compare July 6, 2026 11:03
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx
james-elicx force-pushed the codex/fix-pages-fast-refresh branch from 6cfbb21 to 0e81996 Compare July 6, 2026 11:06
@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: fix(pages): preserve fast refresh state

Reviewed the full diff (both commits 1627925 and 8e2b5e6) against main, traced the Vite HMR internals, and ran the targeted tests. The core Fast Refresh mechanism is correct. Findings below, ordered by importance.

Core mechanism — verified correct

The hotUpdate hook is now object form with order: "post", and the SSR branch returns [] after manually invalidating changed modules. I traced this against vite-plus-core's hmr()/updateModules():

  • Non-client environments: the returned [] replaces hot.options.modules, so hmr(ssrEnv) hits !options.modules.length and early-returns with [no modules matched]no full-reload, because the reload in that branch is gated on environment.name === "client" (node.js:32470-32481). ✅
  • Client environment: vinext's hotUpdate returns undefined (SSR branch only fires when this.environment?.name === "ssr"), so client modules are untouched and @vitejs/plugin-react (which runs before, since vinext is order: "post") handles React Fast Refresh. ✅
  • this.environment binding: confirmed each per-environment call uses environment.pluginContainer.minimalContext, whose .environment is that environment (node.js:36042-36047). So this.environment?.name resolves correctly per call. ✅
  • Route add/delete full reload moved from the old hotUpdate branch to the add/unlink watcher handlers (index.ts:4285, 4321), gated on !hasAppDir. The new unit test asserts both fire { type: "full-reload" }. ✅

vp check is clean on all changed files; the new unit test (does not force full reload...) and the new CSS test both pass. All existing dev Pages CSS tests (including CSS-module cases) still pass.

Scope: second commit is undocumented

Commit 8e2b5e6 "fix(pages): preload transformed dev styles" adds collectTransformedStylesheetAssets (+117 lines incl. test) and is not mentioned in the PR summary/motivation/validation. It's a distinct dev-CSS fix bundled into a Fast Refresh PR. Please either (a) add it to the PR description so reviewers know what they're approving, or (b) split it into its own PR. Right now the described validation section doesn't cover it.

Non-blocking notes

  1. Hand-rolled import regex is fragile (dev-server.ts:166-167). /(?:import|export)\s*(?:[^"']*?from\s*)?["']([^"']+)["']/gi will also match string literals that merely look like imports and any import(...)/export ... from forms. It runs on Vite-transformed client code (mostly trusted, rewritten specifiers), so false positives are unlikely to produce real hrefs, but this is a maintenance hazard. Consider es-module-lexer (already common in Vite land) instead of a regex, or at least a comment noting the transformed-code assumption.

  2. Per-request recursive graph walk in dev (collectTransformedStylesheetAssets). This runs on every dev SSR render and recursively transformRequests + regex-scans every /-prefixed client import transitively reachable from _app + the page. transformRequest is cached, but the traversal and regex scan repeat each request and grow with app size. Dev-only, so acceptable, but worth a follow-up if large apps see slow dev renders. The seenModules guard correctly prevents infinite recursion.

  3. E2E test mutates a committed fixture (tests/fixtures/pages-basic/pages/hmr-state.tsx). Restored in finally, but a hard kill mid-run leaves it dirty. Accepted pattern for HMR tests; noting for local runs.

  4. page.waitForTimeout(1_000) in the syntax-error recovery step is a fixed sleep and could flake on slow CI. Next.js's own recovery util uses a similar wait, so it mirrors upstream — consider polling on the error overlay disappearing if it flakes.

Parity

Dev-only change; prod-server.ts / worker-entry.ts untouched, so dev/prod parity holds. The second commit narrows a dev-only FOUC gap rather than introducing dev-only styles that would be absent in prod.

Overall: core fix is solid and well-tested. Main ask is documenting/splitting the second commit; the regex parser and per-request walk are non-blocking but worth a follow-up. LGTM pending CI.

@ask-bonk

ask-bonk Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

I completed a thorough review of PR #2544 and posted it. Summary of what I found:

Verdict: LGTM pending CI — core fix is correct

Core mechanism (verified against Vite internals):

  • Traced the hotUpdate object hook with order: "post" + SSR return [] through vite-plus-core's hmr()/updateModules(). The [no modules matched] SSR early-return does NOT trigger full-reload (that branch is gated on environment.name === "client"). Client env keeps its modules so @vitejs/plugin-react handles Fast Refresh. Confirmed this.environment binding resolves per-environment correctly.
  • Route add/delete full-reload was correctly relocated to the add/unlink watcher handlers, gated on !hasAppDir.
  • vp check clean; new unit test, new CSS test, and all existing dev Pages CSS tests (incl. CSS-modules) pass.

Most important finding — undocumented scope creep:

  • The PR contains a second commit (8e2b5e6 "preload transformed dev styles", +117 lines) that is NOT mentioned anywhere in the PR description. It's a distinct dev-CSS fix bundled into a Fast Refresh PR. Asked the author to document it in the PR body or split it out.

Non-blocking notes flagged:

  • Hand-rolled import regex in collectTransformedStylesheetAssets is fragile (suggested es-module-lexer).
  • That collector does a recursive per-request client-graph walk on every dev SSR render — dev-only, acceptable, but a perf follow-up candidate for large apps.
  • E2E test mutates a committed fixture; fixed waitForTimeout(1_000) could flake.

Dev/prod parity holds (prod-server and worker-entry untouched).

github run

@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: fix(pages): preserve fast refresh state

Reviewed both commits (1627925 preserve fast refresh state, 0e81996 preload transformed dev styles) against main, traced the Vite HMR/CSS-graph internals, and ran the targeted tests. The core mechanism is correct and I found no blocking issues. Notes below, ordered by importance.

Core Fast Refresh mechanism — verified correct

  • hotUpdate is now object form with order: "post", running after @vitejs/plugin-react, so client React Fast Refresh handles the client update instead of vinext forcing a full-reload.
  • The SSR branch (this.environment?.name === "ssr") manually invalidates the changed modules with invalidateModule(mod, new Set(), timestamp, true) and return []. Returning [] empties the SSR env's module list so Vite doesn't hit a dead-end boundary and fall back to full-reload. Because the SSR environment does not push HMR updates to the browser (only the client env does), the manual invalidation is behaviorally equivalent to Vite's own SSR invalidation — the SSR entry re-renders fresh HTML on the next request. ✅
  • Route add/delete full-reload was correctly relocated to the add/unlink watcher handlers. Parity holds across all three modes: pages-only via the new if (!hasAppDir) server.ws.send({ type: "full-reload" }) (index.ts:4285, 4321), hybrid via invalidateHybridClientEntries() (which sends full-reload, index.ts:4118), and app-only via invalidateRscEntryModule(). ✅

Hybrid-mode SSR return [] — checked, not a defect

I specifically investigated whether the SSR return [] could swallow App Router Fast Refresh in hybrid apps. isPotentialPagesAssetGraphScript returns true for any script under root outside appDir (e.g. a shared lib/*.ts imported by an app-router client component), so the SSR branch fires for those files in hybrid mode too. However, App Router HMR is driven by the RSC plugin + client re-fetch (app-browser-navigation-controller.ts), not by the Pages plugin's SSR hotUpdate result, and the SSR env never pushes updates to the browser. The manual invalidateModule(..., true) still invalidates the shared module for the SSR env, so App Router re-renders fresh on the next request. No parity regression. Worth adding a hybrid HMR unit test to lock this in, since the current unit test only exercises pages-only mode.

Second commit (transformed dev styles) — now documented, verified

  • collectTransformedStylesheetAssets walks the client module graph (transformRequest + moduleGraph.getModuleByUrlimportedModules) to discover CSS introduced by client transforms (including \0-prefixed virtual CSS, re-encoded as /@id/__x00__...). This mirrors what production discovers via the build's SSR manifest (prod-server.ts:1199), so dev/prod first-paint parity holds. ✅
  • try/catch around the walk preserves the source-manifest fallback if a third-party client transform throws while the server render is still valid. Good defensive choice.
  • Dedup against manifestHTML via html.includes(href="...") is sound for identical hrefs.

Non-blocking notes

  1. Duplicate <link> risk when hrefs differ for the same CSS. The manifest path emits createPagesDevAssetUrl(file) while the transformed path may emit a raw importedModule.url or /@id/__x00__.... If the same underlying stylesheet surfaces under two different URLs, the html.includes(href=...) dedup won't catch it and the page loads the CSS twice. Dev-only, cosmetic (no FOUC), but worth a follow-up if it shows up.

  2. Per-request recursive client-graph walk. collectTransformedStylesheetAssets runs on every dev SSR render and recursively transformRequests + scans the transitive client graph from _app + the page. The cacheKey cache mitigates repeat cost, and it's fully invalidated on any add/change/unlink — but the cache clears on every file change (not just relevant ones), so an app with frequent unrelated edits re-walks each time. seenModules correctly guards recursion. Dev-only, acceptable; follow-up candidate for large apps.

  3. CSS discovery only recurses into type: "js" importers. Modules with other/undefined types that transitively import CSS would be skipped. Unlikely in practice for Pages Router, noting for completeness.

  4. Watcher listeners aren't removed on server.close(). The add/change/unlink cache-clear listeners registered in collectTransformedStylesheetAssets persist for the server's lifetime (they close over the per-server cache Map). WeakMap/WeakSet keying means it's GC'd with the server, so no real leak — noting for awareness.

  5. E2E test mutates a committed fixture (tests/fixtures/pages-basic/pages/hmr-state.tsx, restored in finally) and uses a fixed page.waitForTimeout(1_000) in the syntax-error recovery step. Both mirror Next.js's own HMR test utilities; consider polling on the error overlay disappearing if the sleep flakes on slow CI.

Verification

  • vp check packages/vinext/src/index.ts packages/vinext/src/server/dev-server.ts tests/pages-router.test.ts — clean (format, lint, types).
  • New unit test does not force full reload for Pages Router Fast Refresh updates — passes; asserts order: "post", no full-reload on shared/_app/page edits, the SSR return [] + invalidateModule(..., true), and the route add/unlink reloads.
  • New test dev Pages client assets expose virtual CSS added by client transforms — passes.
  • All 8 existing dev Pages stylesheet tests (incl. CSS-modules, ISR, custom error, transitive imports) — pass.

Dev-only change; prod-server.ts / worker-entry.ts untouched. Overall: solid, well-tested fix. Main suggestion is a hybrid-mode HMR unit test; the rest are non-blocking follow-ups. LGTM pending CI.

@ask-bonk

ask-bonk Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2544.

Summary

I reviewed both commits of PR #2544 end-to-end, traced the Vite HMR and CSS-graph internals, and ran the targeted tests. The core fix is correct — no blocking issues.

Verified correct:

  • hotUpdate → object form with order: "post" lets @vitejs/plugin-react handle client Fast Refresh instead of vinext forcing a full reload.
  • SSR branch manually invalidates + return [] to avoid Vite's dead-end full-reload fallback; behaviorally equivalent since the SSR env doesn't push HMR to the browser.
  • Route add/delete full-reload correctly relocated to add/unlink watchers, with parity across pages-only / hybrid / app-only modes.

Investigated but not a defect: the hybrid-mode SSR return [] fires for shared scripts imported by App Router client components too, but App Router HMR is driven by the RSC plugin + client re-fetch (not the Pages SSR hotUpdate), and the manual invalidateModule(..., true) still invalidates the module — so App Router re-renders fresh. I did suggest adding a hybrid HMR unit test, since current coverage only exercises pages-only mode.

Second (transformed-CSS) commit: walks the client module graph to discover transform-injected/virtual CSS, mirroring what prod discovers via the build's SSR manifest — dev/prod first-paint parity holds.

Non-blocking follow-ups flagged: possible duplicate <link> when the same CSS surfaces under two URLs; per-request recursive graph walk with over-eager cache clearing; CSS discovery only recursing into type:"js" importers; watcher listeners not removed on close; and the E2E test's committed-fixture mutation + fixed waitForTimeout.

vp check clean; all new and existing stylesheet/HMR tests pass. LGTM pending CI.

github run

@james-elicx
james-elicx force-pushed the codex/fix-pages-fast-refresh branch from 0e81996 to a23f701 Compare July 6, 2026 11:21
@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: fix(pages): preserve fast refresh state

Reviewed both commits (1627925 preserve fast refresh state, a23f701 preload transformed dev styles) against main, traced the vite-plus-core HMR + CSS-graph internals, and ran the targeted tests. The core mechanism is correct — no blocking issues. Findings ordered by importance.

Core Fast Refresh mechanism — verified correct against vite-plus-core internals

I traced the object-form hotUpdate (order: "post") through vite-plus-core hmr():

  • The hook runs in two separate loops (node.js:32430 client-only, node.js:32453 per non-client env), each called with that environment's pluginContainer.minimalContext, so this.environment?.name resolves correctly per call.
  • Client env: vinext returns undefined (SSR branch guarded by name === "ssr"), so clientHotUpdateOptions.modules is preserved and @vitejs/plugin-react (runs before, since vinext is order:"post") handles React Fast Refresh. ✅
  • SSR env: returns []hot.options.modules = [] (node.js:32460) → hmr() hits !options.modules.length and the full-reload there is gated on file.endsWith(".html") && environment.name === "client" (node.js:32471), so a .tsx SSR edit only logs [no modules matched]no reload. The manual invalidateModule(mod, new Set(), timestamp, true) matches Vite's own call in updateModules (node.js:32504), so SSR re-renders fresh HTML on the next request. ✅
  • RSC env (hybrid): returns undefined, so RSC modules are preserved and the RSC plugin drives App Router HMR. The invalidateModule(..., true) still invalidates shared modules for the SSR env, so hybrid App Router pages re-render fresh. No parity regression. ✅
  • Route add/delete full-reload was correctly relocated to the add/unlink watcher handlers, gated on !hasAppDir (index.ts:4285, 4321). Hybrid uses invalidateHybridClientEntries(); app-only uses invalidateRscEntryModule(). ✅

vp check is clean on all changed files. New unit tests (does not force full reload for Pages Router Fast Refresh updates, does not force full reload for shared App Router code in hybrid apps) and the CSS test (dev Pages client assets expose virtual CSS added by client transforms) all pass, as do all 8 existing dev Pages stylesheet tests. Good coverage — and the hybrid-mode unit test suggested in the prior review round is now present.

Behavior-change note (non-blocking) — non-script Pages extensions

The old hotUpdate had a pages-only client-env fallback (options.file.startsWith(pagesDir) && fileMatcher.extensionRegex.test(options.file)full-reload) that covered non-script page extensions (e.g. .mdx, .md). The new handler only special-cases scripts (isScriptModuleId.[cm]?[jt]sx?), CSS, and _app. For an .mdx page edit in pages-only mode, vinext no longer participates, so Vite's default HMR applies — which is fine when the MDX plugin produces a Fast-Refresh-accepting JS module, but if the SSR page module hits a dead-end boundary Vite will fall back to full-reload (acceptable, matches old behavior). Worth a quick confirmation that MDX Pages Router HMR still behaves, since there's no test for non-script page extensions.

Transformed dev CSS collector — non-blocking follow-ups

collectTransformedStylesheetAssets (dev-server.ts:154) walks the client module graph to discover transform-injected/virtual CSS, mirroring prod's SSR-manifest discovery. Dev/prod first-paint parity holds. A few follow-ups:

  1. Duplicate <link> when the same CSS surfaces under different URLs. Manifest path emits createPagesDevAssetUrl(file); transformed path emits raw importedModule.url or /@id/__x00__.... The html.includes(href="...") dedup (dev-server.ts:241) only catches identical hrefs, so the same stylesheet reachable under two URL forms would load twice. Dev-only, cosmetic.
  2. CSS discovery only recurses into type: "js" importers (dev-server.ts:195). Modules with other/undefined .type that transitively import CSS are skipped. Unlikely for Pages Router; noting for completeness.
  3. Per-request recursive client-graph walk on every dev SSR render. The cacheKey cache mitigates repeat cost but is cleared on every add/change/unlink (dev-server.ts:166-169), not just relevant ones, so an app with frequent unrelated edits re-walks each time. seenModules correctly guards recursion. Dev-only; follow-up candidate for large apps.
  4. Watcher listeners aren't removed on server.close() — they close over the per-server cache Map keyed by WeakMap/WeakSet, so they're GC'd with the server. No real leak; noting for awareness.

E2E test — non-blocking

hmr.spec.ts mutates a committed fixture (tests/fixtures/pages-basic/pages/hmr-state.tsx, restored in finally) and uses a fixed page.waitForTimeout(1_000) for syntax-error recovery. Both mirror Next.js's own HMR test utilities. Note that pages-router runs all ~24 spec files against one shared vp dev server; the mutation triggers a server-wide watcher change (clears the CSS cache + invalidates the pages-client-assets module), but hmr-state is a dedicated route no other test loads, so cross-test interference is negligible. Consider polling on the error-overlay disappearing instead of the fixed sleep if it flakes on slow CI.

Parity

Dev-only change; prod-server.ts / worker-entry.ts untouched, so dev/prod parity holds. The CSS commit narrows a dev-only FOUC gap rather than introducing dev-only styles absent in prod. The __HMR_MARKER__ assertion in the E2E test correctly guards the fix (a full reload would wipe the marker).

Overall: solid, well-scoped, well-tested fix, and the scope-creep/hybrid-test concerns from prior rounds are now addressed. LGTM pending CI (full Vitest + Playwright).

@ask-bonk

ask-bonk Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2544.

Summary

I reviewed both commits of PR #2544 (fix(pages): preserve fast refresh state) end-to-end, traced the vite-plus-core HMR and CSS-graph internals in node_modules, and ran the targeted tests. The core mechanism is correct — no blocking issues.

Verified correct against Vite internals:

  • The object-form hotUpdate with order: "post" fires in two loops (client-only + per non-client env), each with the correct per-environment this.environment binding. Client env returns undefined (React Fast Refresh preserved), SSR env returns [] (dead-end full-reload avoided — that reload is gated on .html + client env at node.js:32471), RSC env returns undefined (RSC plugin drives App Router HMR).
  • The manual invalidateModule(mod, new Set(), timestamp, true) exactly matches Vite's own call in updateModules (node.js:32504).
  • Route add/delete full-reload correctly relocated to the add/unlink watchers, gated on !hasAppDir.

Tests: vp check clean; all new unit tests (including the hybrid-mode test that was a prior-round suggestion) and CSS tests pass, plus all 8 existing dev Pages stylesheet tests.

Notes flagged (all non-blocking):

  • One behavior change worth confirming: non-script page extensions (.mdx/.md) are no longer specially handled in hotUpdate, unlike the old code. No test covers this.
  • CSS collector follow-ups: possible duplicate <link> across differing URL forms, recursion only into type:"js" importers, per-request graph walk with over-eager cache clearing, watcher listeners not removed on close.
  • E2E test mutates a committed fixture + fixed waitForTimeout(1_000).

The scope-creep concern from earlier rounds is resolved — the PR body now documents both the Fast Refresh and CSS-preload changes. LGTM pending CI.

github run

@james-elicx
james-elicx marked this pull request as ready for review July 6, 2026 11:27
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx
james-elicx force-pushed the codex/fix-pages-fast-refresh branch from a23f701 to 4c4f440 Compare July 6, 2026 14:01
@Mad-Kat

Mad-Kat commented Jul 6, 2026

Copy link
Copy Markdown

I think

pseudo-element styles raced the first computed-style read because next-yak's transformed virtual CSS was only injected during hydration

is wrong, but the solution to follow vite's CSS resolving behaviour sounds correct to me. Thanks for looking into it

@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: fix(pages): preserve fast refresh state

Reviewed both commits against main, traced the vite-plus-core HMR/CSS internals, and ran the targeted Vitest files. Core mechanism is correct — no blocking issues. One correction to prior review rounds, plus a couple of non-blocking notes.

Correction to the "full-reload is gated on client env" claim

Prior review rounds asserted the SSR return [] avoids a reload because the full-reload in hmr() is gated on environment.name === "client". That gate (node.js:32471) only applies to the !options.modules.length early-return path. When options.modules.length > 0, hmr() calls updateModules(), whose full-reload send at node.js:32525 (if (needFullReload || isClientHtmlChange)) is not gated on the client env — it fires for any environment on a dead-end boundary.

The reason this PR is still correct is different: the SSR environment's hot is a noop channel (node.js:30432 — only the client env carries the WS transport), so an SSR-env full-reload never reaches the browser regardless. The actual fix here is removing the explicit options.server.ws.send({ type: "full-reload" }) / client.hot.send(...) calls (which targeted the real browser WS) from the old hotUpdate. The return [] + manual invalidateModule(mod, new Set(), timestamp, true) is then just to (a) skip the SSR updateModules walk and (b) hard-invalidate the SSR module so the next request re-renders fresh HTML. I verified the 4th positional arg (isHmr=true) matches Vite's own updateModules invalidation at node.js:32504. This mechanism is sound.

MDX path — verified, not a defect

The new reactOptions.include: /\.(?:[tj]sx?|mdx)$/ is a strict superset of @vitejs/plugin-react's default /\.[tj]sx?$/ (adds .mdx, loses nothing), so React Fast Refresh now applies to compiled MDX. For .mdx edits, isPotentialPagesAssetGraphScript is false (not a script id), so the SSR return [] branch does not fire — but that's fine: the SSR-env updateModules full-reload goes to the noop channel, and the client env's refresh-transformed MDX module self-accepts. So MDX state is preserved, matching the new E2E assertion. @mdx-js/rollup is hoisted in the workspace, so the hmr-mdx.mdx fixture builds via vinext's auto-detected MDX proxy.

Non-blocking notes

  1. Possible duplicate <link> for the same CSS under differing URL forms. The manifest path emits createPagesDevAssetUrl(file) while collectTransformedStylesheetAssets emits the raw importedModule.url (or /@id/__x00__...). The html.includes(href="...") dedup (dev-server.ts:241) only catches identical hrefs, so a stylesheet reachable under two URL forms would emit twice. Dev-only, cosmetic (no FOUC). Existing stylesheet tests use .toContain, so they won't catch a duplicate.

  2. CSS discovery only recurses into type: "js" importers (dev-server.ts:195). Modules with null/other .type that transitively import CSS are skipped. Unlikely for Pages Router; noting for completeness.

  3. Per-request recursive client-graph walk, cleared over-eagerly. collectTransformedStylesheetAssets runs on every dev SSR render; the cacheKey cache mitigates repeat cost but is cleared on every add/change/unlink (dev-server.ts:166-169), not just relevant edits. seenModules correctly guards recursion. Dev-only; follow-up candidate for large apps. Its watcher listeners aren't removed on server.close(), but WeakMap/WeakSet keying means they GC with the server — no real leak.

  4. E2E test mutates committed fixtures (hmr-state.tsx, hmr-mdx.mdx, restored in finally) against the shared localhost:4173 dev server, and uses a fixed page.waitForTimeout(1_000) for syntax-error recovery. Both mirror Next.js's own HMR utilities; consider polling on the error-overlay disappearing if the sleep flakes on slow CI.

Verification

  • vp check packages/vinext/src/index.ts packages/vinext/src/server/dev-server.ts — clean.
  • New unit test does not force full reload for Pages Router Fast Refresh updates and dev Pages client assets expose virtual CSS added by client transforms — pass.
  • All 8 existing dev Pages stylesheet tests (incl. CSS-modules, ISR, basePath, transitive) — pass; the basePath test confirms the transformed collector doesn't break non-root-base hrefs.
  • tests/next-config.test.ts + tests/routing.test.ts (325 tests) — pass, so the pageExtensions: [...,"mdx"] addition to the shared pages-basic fixture doesn't perturb route discovery.

Dev-only change; prod-server.ts / worker-entry.ts untouched, so dev/prod parity holds. LGTM pending CI (full Vitest + Playwright).

@ask-bonk

ask-bonk Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2544.

Summary

I reviewed both commits of PR #2544 (fix(pages): preserve fast refresh state) end-to-end, traced the vite-plus-core HMR/CSS internals in node_modules, and ran the targeted tests. The core mechanism is correct — no blocking issues.

Most important finding — a correction to prior review rounds

Earlier /bigbonk rounds repeatedly claimed the SSR return [] works because the full-reload in Vite's hmr() is "gated on environment.name === "client"". That's only true for the !options.modules.length early-return path (node.js:32471). When options.modules.length > 0, hmr() calls updateModules(), whose full-reload at node.js:32525 is not client-gated.

The PR is still correct, but for a different reason: the SSR environment's hot is a noop channel (node.js:30432 — only the client env carries the WS transport), so an SSR-env reload never reaches the browser anyway. The real fix is removing the explicit server.ws.send({ type: "full-reload" }) calls that hit the actual browser WS. The return [] + manual invalidateModule(..., isHmr=true) (matching Vite's own call at node.js:32504) just skips the SSR walk and forces fresh SSR HTML on the next request.

Other verified items

  • The reactOptions.include MDX addition is a strict superset of plugin-react's default — nothing lost; MDX state preservation holds because the client env self-accepts the refresh-transformed module.
  • pageExtensions: [...,"mdx"] on the shared pages-basic fixture doesn't perturb route discovery (325 config/routing tests pass); @mdx-js/rollup is hoisted so the MDX fixture builds.

Non-blocking notes flagged

  • Possible duplicate <link> when the same CSS surfaces under differing URL forms (manifest vs transformed collector; dev-only, cosmetic).
  • CSS discovery only recurses into type:"js" importers.
  • Per-request recursive graph walk with over-eager cache clearing; watcher listeners GC'd with server (no leak).
  • E2E test mutates committed fixtures + fixed waitForTimeout(1_000).

vp check clean; all new + existing stylesheet/HMR tests pass. LGTM pending CI.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx
james-elicx force-pushed the codex/fix-pages-fast-refresh branch from 4c4f440 to 8e1e504 Compare July 6, 2026 14:15

@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: fix(pages): preserve fast refresh state

Reviewed both commits against main, ran the targeted Vitest files, and traced the @vitejs/plugin-react split. Found one CI-blocking regression that prior /bigbonk rounds missed, plus context on the new React/MDX plugin restructuring that the PR body doesn't mention.

Blocking — existing test registers vinext:mdx proxy plugin now fails

This PR splits the old single vinext:mdx proxy plugin into two: vinext:mdx (transform only) and a new vinext:mdx-config (config only). But the pre-existing, unmodified test still asserts that vinext:mdx carries the config hook:

tests/pages-router.test.ts:4056
  expect(typeof mdxProxy.config).toBe("function");  // ← now undefined

Reproduced in isolation:

$ vp test run tests/pages-router.test.ts -t "vinext:mdx"
 FAIL  Plugin config > registers vinext:mdx proxy plugin with enforce pre for correct ordering
 AssertionError: expected 'undefined' to be 'function'
   4056|     expect(typeof mdxProxy.config).toBe("function");

The config hook moved to vinext:mdx-config, so plugins.find(p => p.name === "vinext:mdx").config is now undefined. This test passed on main and is not touched by the PR, so the Vitest required check will fail. Fix: update the assertion to look up vinext:mdx-config for the config hook (or point the test at the new plugin), e.g.:

const mdxConfig = plugins.find((p) => p.name === "vinext:mdx-config");
expect(typeof mdxConfig.config).toBe("function");
expect(mdxConfig.config({}, { command: "build", mode: "production" })).toBeUndefined();

Every prior review round in this thread reported "vp check clean; all tests pass," but none of them ran this specific existing test — vp check only type/lint/format-checks, and the new-test runs were name-filtered to the Fast Refresh / CSS tests. Worth running the full Plugin config describe block (vp test run tests/pages-router.test.ts -t "Plugin config") before merge.

Undocumented scope: React plugin is now instantiated twice

The PR body only describes Fast Refresh state + transformed CSS. It does not mention that reactPluginPromise now:

  • instantiates @vitejs/plugin-react twice — a build set with the configured options and a serve set that adds .mdx to include — and wraps each plugin in a limitToCommand(...) apply guard (index.ts:1511-1540);
  • splits the MDX proxy into vinext:mdx + vinext:mdx-config (index.ts:1617-1649).

I traced the apply filtering and it is correct:

  • vite:react:refresh-wrapper (apply: "serve") is excluded from the build set ("serve" === "build" → false) and kept in serve. ✓
  • undefined-apply plugins (e.g. vite:react-babel) run in both commands. ✓
  • The existing configResolved duplicate-detection (index.ts:3326, "Duplicate @vitejs/plugin-react detected") does not false-positive, because config.plugins is already apply-filtered per command — verified via -t "react" (the double-registration test still passes).

But this is a meaningful architectural change (doubling the react plugin set to give dev an MDX include) bundled into a "preserve fast refresh state" PR. Please document it in the PR body so reviewers know what they're approving, and confirm it's the intended approach vs. simply passing a merged include to a single react instance.

Core Fast Refresh mechanism — verified correct

  • hotUpdate → object form with order: "post", SSR branch manually invalidates (invalidateModule(mod, new Set(), timestamp, true), matching Vite's own call) and return []. The explicit browser ws.send({ type: "full-reload" }) calls were removed and relocated to the add/unlink watchers gated on !hasAppDir (index.ts:4313, 4349). Route add/delete still full-reloads; content edits are state-preserving. ✓
  • New E2E hmr.spec.ts covers state preservation, syntax-error recovery, and — good — an MDX HMR case (hmr-mdx.mdx), which addresses the non-script-extension gap flagged in earlier rounds.
  • The new HMR unit tests (pages-only + hybrid shared-code) and the transformed-CSS test all pass.

Non-blocking notes

  • False failure I hit while testing: vp test run ... -t "renders" reports a Static export failure (blog/hello-world.html missing). That's a shared-state artifact — the name filter skips the setup test that populates exportDir. Running -t "Static export" passes all 7. Not a regression; noting so it isn't mistaken for one.
  • Transformed-CSS collector (dev-server.ts:154): still carries the previously-flagged follow-ups — possible duplicate <link> when the same CSS surfaces under differing URL forms (html.includes(href=...) only dedups identical hrefs), recursion only into type: "js" importers, and the per-request graph walk whose cache is cleared on every add/change/unlink rather than relevant edits only. All dev-only.
  • E2E mutates committed fixtures (restored in finally) and uses a fixed page.waitForTimeout(1_000) for syntax-error recovery — consider polling the overlay if it flakes.

Verdict

Core fix is sound, but not mergeable as-is: the vinext:mdx config-hook assertion is a real, reproducible Vitest failure introduced by the plugin split. Update that test (and document the React/MDX plugin restructuring), then LGTM pending CI.

@ask-bonk

ask-bonk Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2544.

Summary

I reviewed both commits of PR #2544 end-to-end, ran the targeted Vitest files, and traced the @vitejs/plugin-react/MDX plugin restructuring. I found one CI-blocking regression that every prior /bigbonk round in this thread missed.

Blocking finding — an existing test now fails

The PR splits the old single vinext:mdx proxy plugin into two — vinext:mdx (transform only) and a new vinext:mdx-config (config only) — but did not update the pre-existing test registers vinext:mdx proxy plugin with enforce pre for correct ordering, which still asserts vinext:mdx carries the config hook. Reproduced in isolation:

FAIL  Plugin config > registers vinext:mdx proxy plugin...
AssertionError: expected 'undefined' to be 'function'  (line 4056)

This test passed on main and isn't touched by the PR, so the required Vitest check will fail. The prior review rounds all reported "vp check clean; all tests pass" — but vp check only lints/types/formats, and their test runs were name-filtered to the new Fast Refresh/CSS tests, so they never exercised this existing case.

Other notable findings

  • Undocumented scope creep: the PR body only mentions Fast Refresh + transformed CSS, but it also now instantiates @vitejs/plugin-react twice (a build set + a serve set with .mdx added to include), each wrapped in a limitToCommand apply guard. I verified the apply filtering and the existing duplicate-react-plugin detection do NOT false-positive (both apply-filtered per command), but this architectural change should be documented.
  • Core Fast Refresh mechanism verified correct — object-form hotUpdate with order: "post", SSR return [] + manual invalidate, full-reload relocated to add/unlink watchers. The MDX E2E test is a good addition.
  • Non-blocking: flagged a shared-state test-filter artifact (so it isn't mistaken for a regression), plus the previously-noted CSS-collector follow-ups and the E2E fixed timeout.

Verdict: core fix is sound but not mergeable as-is; the vinext:mdx config assertion is a real, reproducible Vitest failure that must be fixed (and the React/MDX plugin split documented).

github run

@james-elicx
james-elicx force-pushed the codex/fix-pages-fast-refresh branch from 8e1e504 to 40e9096 Compare July 6, 2026 14:37
@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: fix(pages): preserve fast refresh state

Reviewed both commits (fast refresh state + preload transformed dev styles) against main, verified the previously-flagged blocking regression is fixed, and ran the targeted Vitest files. No blocking issues — the core mechanism is correct and the prior-round regression is resolved.

Prior-round blocking regression is fixed

The last /bigbonk round reported that the pre-existing test registers vinext:mdx proxy plugin with enforce pre for correct ordering would fail because the PR split vinext:mdx into vinext:mdx (transform) + vinext:mdx-config (config). That test has now been updated (tests/pages-router.test.ts:4050-4071) to assert mdxProxy.config is undefined and mdxConfigProxy.config is a function. I ran the full Plugin config describe block and it passes (15 tests, incl. the vinext:mdx ordering + lazy-compile cases). The React/MDX plugin restructuring is also now documented in the PR body.

Core Fast Refresh mechanism — verified correct

  • hotUpdate is object-form with order: "post", running after @vitejs/plugin-react so client React Fast Refresh handles the update. The SSR branch manually invalidates changed modules via invalidateModule(mod, new Set(), timestamp, true) (matching Vite's own call) and return []. The explicit browser ws.send({ type: "full-reload" }) calls were removed.
  • Route add/delete full-reload correctly relocated to the add/unlink watchers, gated on !hasAppDir (index.ts:4315, 4351); hybrid uses invalidateHybridClientEntries(). routeChanged fires only for files matching pageExtensions under pagesDir or app-route files. Correct.
  • New unit tests (does not force full reload..., hybrid shared-code) and the E2E hmr.spec.ts (state preservation, syntax-error recovery, MDX route updates) all pass.

Production parity for MDX — verified

The serve-only include: /\.(?:[tj]sx?|mdx)$/i is what enables MDX Fast Refresh in dev. I confirmed against main that the react plugin was previously invoked as default(reactOptions) with no .mdx in include for either command — so compiled MDX JSX never went through react's babel in build before this PR either. The build set here keeps the configured/default include (no .mdx), so production MDX parsing is unchanged. The limitToCommand apply guards are correct: refresh-wrapper (apply: "serve") is excluded from the build set, undefined-apply plugins run in both.

Transformed dev CSS collector — verified, non-blocking follow-ups

collectTransformedStylesheetAssets (dev-server.ts:154) walks the client module graph to discover transform-injected/virtual CSS, mirroring prod's SSR-manifest discovery. Dev/prod first-paint parity holds. Same non-blocking follow-ups noted in prior rounds still stand:

  1. Possible duplicate <link> when the same CSS surfaces under differing URL forms (manifest createPagesDevAssetUrl vs raw importedModule.url//@id/__x00__...); html.includes(href=...) only dedups identical hrefs. Dev-only, cosmetic.
  2. CSS discovery only recurses into type: "js" importers (dev-server.ts:195) — other/undefined types transitively importing CSS are skipped. Unlikely for Pages Router.
  3. Per-request recursive graph walk with the cache cleared on every add/change/unlink rather than relevant edits only. seenModules guards recursion. Dev-only.
  4. E2E mutates committed fixtures (restored in finally) + fixed page.waitForTimeout(1_000) for syntax-error recovery — consider polling the overlay if it flakes.

Verification

  • vp check packages/vinext/src/index.ts packages/vinext/src/server/dev-server.ts tests/pages-router.test.ts — clean.
  • Plugin config (15) — pass, including the previously-failing vinext:mdx test.
  • full reload HMR unit tests (2) — pass. virtual CSS transform test — pass. All 9 stylesheet tests — pass.
  • react tests incl. double-registration guard (2) — pass. tests/next-config.test.ts (212) — pass, so the pageExtensions: [...,"mdx"] fixture addition doesn't perturb config/route parsing.

Dev-only change; prod-server.ts / worker-entry.ts untouched. LGTM pending CI (full Vitest + Playwright).

@ask-bonk

ask-bonk Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2544.

Summary

I reviewed both commits of PR #2544 (fix(pages): preserve fast refresh state) against main, focusing on whether the blocking regression flagged in the previous /bigbonk round was fixed, then verified the core mechanism and ran targeted tests. No blocking issues — the PR is now mergeable pending CI.

Key finding: the prior-round blocker is resolved

The last review round found a real, reproducible Vitest failure — the pre-existing registers vinext:mdx proxy plugin test still asserted the old single-plugin shape after the PR split vinext:mdx into vinext:mdx (transform) + vinext:mdx-config (config). That test has since been updated (tests/pages-router.test.ts:4050-4071) to assert the new split, and I confirmed the full Plugin config block (15 tests) now passes. The React/MDX plugin restructuring is also now documented in the PR body, resolving the scope-creep concern.

Verified correct

  • Fast Refresh mechanism: object-form hotUpdate with order: "post", SSR return [] + manual invalidateModule(..., true), route add/delete relocated to add/unlink watchers gated on !hasAppDir.
  • Production parity for MDX: the serve-only .mdx include enables dev Fast Refresh without changing build behavior — confirmed against main that compiled MDX never went through react's babel in build previously either.
  • limitToCommand apply guards correctly split build/serve plugin sets.

Non-blocking follow-ups (dev-only)

Possible duplicate <link> for CSS under differing URL forms; CSS recursion only into type:"js" importers; per-request graph walk with over-eager cache clearing; E2E fixed-timeout flake risk.

All targeted tests (Plugin config, HMR, stylesheet, react, next-config) pass and vp check is clean. LGTM pending CI.

github run

@james-elicx
james-elicx merged commit 60ad0b1 into main Jul 6, 2026
107 of 109 checks passed
@james-elicx
james-elicx deleted the codex/fix-pages-fast-refresh branch July 6, 2026 22:29
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