fix(transforms): stop the veryfront server barrel from leaking into client chunks - #3025
Merged
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c00f6983d1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
… chunks
A *used* value import from the `veryfront` root barrel in a client-reachable
module — e.g. `import { getEnv } from "veryfront"` — drags the entire framework
server graph (~130 modules) into the browser bundle. The browser/SSR pipelines
transform per-file (no cross-module tree-shaking), so an ESM re-export eagerly
loads its source: the barrel re-exports the server bootstrap surface
(`createHandler`, `startServer`, `toNodeHandler`) from `#veryfront/server`, which
transitively pulls `server/production-server.ts`. That module has top-level await
and cannot transform to the es2020 browser target → HTTP 500 on the chunk →
`Failed to fetch dynamically imported module` → hydration aborts, cascading
app-wide via `<Link>` prefetch. Browser-only: SSR and `veryfront build` stay
green. (The existing strip only removes *unused* barrel imports.)
Fix: add a client/SSR-safe mirror barrel (`src/index.client.ts`) that re-exports
the same browser-safe surface minus the server bootstrap value export, and
redirect `veryfront` to it for the browser/ssr targets in the import rewriter —
the same mechanism `veryfront/workflow` already uses. The client now pulls ~5
browser-safe sub-barrels instead of the whole server runtime.
Ship the barrel in the npm package: dnt derives entry points from deno.json
exports, so `src/index.client.ts` is added to `exports` (and `imports` +
BROWSER_SAFE_EXPORTS). Without this, the packaged module server 404s on
`_veryfront/index.client.js` and the redirect breaks in the npm distribution
(dev worked only because it serves straight from src/). Addresses the Codex P1.
Validated end to end: the `pages-server-import-leak` reproducer hydrates clean on
both the source-run and the packaged `npm pack` artifact (index.client.js serves
200, not 404); a new router-testing guard confirms no server module reaches the
client bundle. The canonical default-config gate gains zero new mismatches. The
sibling `.ts`/`.tsx` extension-normalization fix this reproducer also surfaced
landed independently on main (alias-strategy `getProjectRelativePath` refactor),
so it is not part of this change.
mattboon
force-pushed
the
fix/server-barrel-client-leak
branch
from
July 22, 2026 15:40
c00f698 to
0b6c575
Compare
kojiwakayama
approved these changes
Jul 22, 2026
kojiwakayama
enabled auto-merge (squash)
July 22, 2026 17:27
kojiwakayama
added a commit
that referenced
this pull request
Jul 22, 2026
* Keep stripped server hooks out of browser dependency graphs Browser artifacts now remove the module-scope closure used only by server hooks while preserving client references and unrelated side-effect imports. Scope-aware free-variable analysis covers function parameters, hoisting, loops, switch cases, destructuring, JSX, and TypeScript wrappers. Constraint: Browser stripping must not erase unrelated project import side effects Rejected: Redirect every server barrel in the browser | direct project subpath imports would still leak Confidence: high Scope-risk: moderate Directive: Add a focused scope regression before broadening the pruning analysis Tested: browser-server-exports-strip.test.ts (72 steps); deno check; deno lint; deno fmt --check * Prevent SSR discovery from preloading server-only browser modules Development HTML keeps explicit page, layout, JSX runtime, and release-manifest preloads, but no longer treats the legacy SSR route manifest as a browser preload source. Constraint: SSR traversal may include server-only modules that must never reach the browser Rejected: Filter known server module names | transitive project imports are open-ended Confidence: high Scope-risk: narrow Tested: html-shell-generator.test.ts and html-shell-manifest.test.ts; deno check; deno lint; deno fmt --check * Preserve modern ESM during framework module compilation Both direct compilation and browser bundling target ES2022 so framework modules using top-level await remain valid through development hydration. Constraint: The framework already ships modules that use top-level await Rejected: Rewrite top-level await call sites | it duplicates module initialization semantics Confidence: high Scope-risk: narrow Tested: compile.test.ts and browser-module-bundler.test.ts; deno check; deno lint; deno fmt --check * Keep runtime coordination files from restarting development pages The watcher now ignores OMX runtime state and logs, matching other generated output directories and avoiding unrelated HMR work during agent-driven development. Constraint: OMX writes runtime state inside the project tree Confidence: high Scope-risk: narrow Tested: file-watch-setup.test.ts; deno check; deno lint; deno fmt --check * Share cold transforms across concurrent module requests Same-key cache misses now join one in-flight computation while different keys remain concurrent. Failed flights are cleared so later requests can recover normally. Constraint: Concurrent hydration requests can request the same uncached transform Rejected: Raise the module-load timeout alone | duplicate cold work would remain and failures would only be delayed Confidence: high Scope-risk: narrow Directive: Keep flight lifetime tied to the cache lifecycle and preserve retry after failure Tested: transform-cache.test.ts; combined focused suite (183 steps); deno task typecheck * Keep server handlers out of static page builds Pages Router collection now skips only files beneath the configured pages api directory while preserving a legitimate root api page. This prevents API handlers from entering browser code splitting and SSG rendering. Constraint: pages/api.tsx remains a valid page at /api; only api directory descendants are handlers Rejected: Exclude every /api URL | that would remove legitimate page routes and couple collection to URL naming Confidence: high Scope-risk: narrow Tested: server/build-routes.test.ts (38 steps); deno task typecheck; deno check; deno lint; deno fmt --check * Apply server boundaries before production browser splitting Project JavaScript and TypeScript modules now pass through the existing server-hook stripper before esbuild builds browser chunks. The splitter retains real source identities and owns local dependency resolution while hook-only Node and root-framework imports disappear before graph traversal. Constraint: Full browser transforms rewrite imports for module-server delivery and cannot be fed directly into the chunk bundler Rejected: Externalize Node builtins and the root framework barrel | that would hide and ship an invalid server dependency graph Confidence: high Scope-risk: moderate Directive: Keep the splitter source transform limited to boundary stripping unless a virtual module resolver is added Tested: real production CodeSplitter node:crypto/root-veryfront regression; splitter plugin tests; browser strip tests; deno task typecheck; deno check; deno lint; deno fmt --check * Make production splitting self-sufficient The production splitter can run before server bootstrap, so it now lazily registers the first-party Babel parser and verifies that lifecycle without test-only setup. Constraint: Production builds may invoke the splitter without extension bootstrap. Rejected: Keep parser registration in test setup | it masked the production lifecycle failure. Confidence: high Scope-risk: narrow Tested: Code-splitter regression suite; framework targeted suite; linked app production build. * Keep browser entrypoints free of generated Deno shims The router and UI barrels reach runtime core and color-mode modules that DNT had left connected to Node compatibility shims. Marking those emitted modules browser-safe removes that server graph at package build time. Constraint: Browser code splitting must consume the published npm shape without externalizing all framework UI modules. Rejected: Externalize every Veryfront client subpath | hides packaging defects and prevents normal chunk optimization. Confidence: high Scope-risk: narrow Related: #3025 Tested: Browser-safe package tests; npm package build; esbuild shim scan; linked app production build. * Preserve server data while generating concrete pages Pages Router SSG now renders with a matching synthetic GET request and URL, allowing page and layout data hooks to receive their normal context. Dynamic route templates are excluded because this build path does not yet expand getStaticPaths and cannot render a literal bracket route. Constraint: Static generation currently has no dynamic path expansion phase. Rejected: Render bracket routes with empty params | produces invalid pages and crashes server-data hooks. Confidence: high Scope-risk: moderate Directive: Add getStaticPaths expansion before including dynamic Pages routes in SSG collection. Tested: Static-generation and route-collection tests; linked app production build and full npm run check. * Preserve client initializers beside server-only bindings Server-hook closure pruning now tracks each simple variable declarator independently, so removing a hook-only binding cannot erase a co-declared client side effect. Constraint: Browser pruning must remain conservative for destructuring declarations. Rejected: Keep the whole declaration | leaks hook-only server dependencies into browser graphs. Confidence: high Scope-risk: narrow Tested: browser-server-exports-strip suite (73 steps); focused heartbeat/framework suite (226 steps) * Keep active module graphs alive through cold starts Module loading now uses a 10-second idle deadline that resets only after concrete transform milestones, plus a non-resettable 45-second hard cap. Shared cold transforms broadcast progress to followers, while timed-out callers detach without cancelling work still needed by other renders. Constraint: The outer render pipeline remains capped at 60 seconds. Rejected: Raise the fixed 10-second timeout | would hide genuinely stalled module graphs. Confidence: high Scope-risk: moderate Directive: Add progress marks only after meaningful work completes; never weaken the hard cap. Tested: verify:quick; 14 targeted suites (304 steps); cold six-route comparison * Let bounded transform queues drain before shedding load Cold framework work can hold the default three transform permits longer than 500ms. Wait up to five seconds for bounded capacity, preserve active same-key singleflight work, and re-check coordination after a leader failure so followers cannot stampede into duplicate retries. Constraint: Transform concurrency remains bounded by the existing semaphore and per-project limits. Rejected: Raise the default transform capacity to 200 | the same six-route workload passes with the safe default of three when queueing is allowed to drain. Confidence: high Scope-risk: moderate Directive: Do not timeout-delete live singleflight entries; only retry after the leader rejects. Tested: transform concurrency validation; SSR loader suite; six cold concurrent routes returned 200 in 7.6-14.2 seconds * Keep cycle-alias verification stable across valid short hashes Module artifact hashes come from an unpadded 32-bit hexadecimal value, so randomized transformed paths can legitimately produce fewer than eight characters. Verify the alias against the exact returned artifact instead of assuming fixed hash width. Constraint: Preserve the existing module filename and cache contract while removing a nondeterministic pre-push failure Rejected: Pad every module hash to eight characters | would change production cache identities to satisfy a test-only assumption Confidence: high Scope-risk: narrow Reversibility: clean Directive: Cycle alias tests should compare the alias target with the loader result, not infer a fixed hash width Tested: 50 consecutive module-loader suite runs; module-persistence suite; deno fmt --check Not-tested: None * Close review gaps in cold-build safeguards Keep already-aborted followers listener-free, preserve ordinary import side effects when only hook-owned bindings are stripped, and make production Pages rendering explicitly static-data-only so request hooks cannot run during builds. Constraint: Static generation may provide URL context to getStaticData but must never execute getServerData. Rejected: Treat a synthetic GET request as safe build context | it can trigger request-only data and bake private state into static output. Confidence: high Scope-risk: moderate Directive: Keep staticDataOnly as a strict getStaticData gate when extending render context. Tested: 223 focused Deno test steps across transform cache, browser export stripping, static generation, render pipeline, and data fetching. Not-tested: Full repository CI runs after the separate AG-UI work is complete. * Keep static module data isolated during shared renders Layout and page data hooks can execute for the same pathname, so URL-only cache keys let the first module's props leak into the next. Thread the loaded module path through data fetching and include it in static cache identity. Constraint: Preserve existing pathname and params cache behavior for callers without module identity Rejected: Disable static data caching for layouts | removes valid production caching instead of fixing its identity Confidence: high Scope-risk: narrow Directive: Every independently loaded data module must contribute its module path to static cache identity Tested: Five focused suites, 152 steps; formatting and diff checks Not-tested: Full repository suite deferred to pre-push * Keep direct project agents aligned with runtime tools Direct project agents can now preserve hosted-style invoke_agent declarations as runtime-local tools, serialize that authored tool id across the project-agent boundary, and hydrate explicitly allowed first-party project-file tool schemas when the Veryfront API MCP catalog omits them. The branch also keeps the cold SSR progress and import-stripping review fixes in source and generated bridge artifacts. Constraint: Direct AG-UI routes do not run inside the hosted chat envelope Constraint: Explicit veryfront-api MCP allow policies must remain capability-minimal Rejected: Add legacy agent_* wrapper tools beside invoke_agent | it widens the orchestrator tool surface and breaks hosted skill metadata Rejected: Trust arbitrary missing remote tool names | fallback is limited to known first-party project-file primitives Confidence: high Scope-risk: moderate Tested: deno test --allow-all src/agent/runtime/agent-delegation.test.ts src/agent/factory.test.ts src/agent/project/agent-runtime.test.ts src/agent/runtime/mcp-server-tool-sources.test.ts src/agent/ag-ui/handler.test.ts src/agent/runtime/chat-stream-handler.test.ts Tested: deno task typecheck Tested: deno task build:npm Tested: linked app direct POST /api/ag-ui streamed to RunFinished without Unknown tool references * Keep cold transform recovery generation-safe Cold concurrent renders need progress-aware idle deadlines without letting abandoned transform leaders overwrite replacement cache state. Scope this branch back to SSR loading, serialize per-key transform publication across registry resets, and use identity-aware eviction so callers remain bounded without retry stampedes. Constraint: A caller may detach at its hard deadline while shared transform work continues for other requests Rejected: Delete in-progress state at every caller timeout | permits duplicate leaders and stale cache publication Rejected: Publish cache writes without per-key ordering | an already-started stale backend write can finish after its replacement Confidence: high Scope-risk: moderate Directive: Keep stale-flight eviction, generation identity checks, and per-key cache publication ordering coupled Tested: deno test --allow-all loader.test.ts transform-cache.test.ts singleflight.test.ts (68 steps); deno check focused sources/tests; git diff --check HEAD^ Not-tested: Full repository test suite in this final review pass * Close cold-graph recovery gaps without weakening liveness Bound repeated rejected-leader retries without adding an async leader-election gap, key static data by the URL query visible to hooks, isolate stale-eviction observers, and document the browser stripping and stage-wide timeout contracts. Canonicalize the splitter project root once so the production boundary check stays cheap. Constraint: Slow healthy transforms must remain progress-aware and shared leaders must not be evicted by impatient followers Rejected: Extract the follower loop into an async helper | an extra await before leader registration allowed concurrent callers to elect multiple leaders Rejected: Sort query parameters in the cache key | URLSearchParams order is observable to getStaticData Rejected: Retry rejected leaders until the outer render deadline | deterministic failures would amplify work and surface as misleading timeouts Confidence: high Scope-risk: moderate Directive: Keep leader election synchronous when no flight exists; keep the one-replacement retry budget identity-guarded Tested: 266 focused test steps; 20 consecutive SSR loader stress runs; changed-file format, lint, typecheck, and diff checks Not-tested: Full repository pre-push gate reruns during push * Keep progress observers outside module-load correctness Module-load heartbeats now isolate listener failures while preserving the abort check ahead of every notification. A focused regression proves a throwing observer cannot fail a transform and a pre-aborted render never invokes the observer. Constraint: Progress callbacks reset idle deadlines but are not part of transformed module semantics. Rejected: Let observer errors propagate for visibility | diagnostic callbacks must not turn healthy module work into render failure. Confidence: high Scope-risk: narrow Directive: Keep abort checks authoritative and ahead of any isolated progress notification. Tested: module-loader index test (14 steps); targeted format and lint. Not-tested: Full repository gate will run in the pre-push hook.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A used value import from the
veryfrontroot barrel in a client-reachable module —import { getEnv } from "veryfront"— drags the entire framework server graph (~130 modules) into the browser bundle. The browser/SSR pipelines transform per-file (no cross-module tree-shaking), so an ESM re-export eagerly loads its source: the barrel re-exports the server bootstrap surface (createHandler,startServer,toNodeHandler) from#veryfront/server, which transitively pullsserver/production-server.ts. That module has top-level await and can't transform to the es2020 browser target → HTTP 500 on the chunk →Failed to fetch dynamically imported module→ hydration aborts, cascading app-wide via<Link>prefetch.Browser-only: SSR and
veryfront buildstay green, so it doesn't show up in an SSR sweep. Reported in the field as "server bundles going into the client, making it slow" — literally ~130 server modules per client chunk. The existing strip only removes unused barrel imports.Fix
src/index.client.ts): mirrors the root barrel minus the#veryfront/serverbootstrap value export. The import rewriter redirectsveryfront→ it for thebrowser/ssrtargets (same mechanism asveryfront/workflow). The client now pulls ~5 browser-safe sub-barrels instead of the whole server runtime.deno.jsonexports, so the barrel is added toexports(+imports+BROWSER_SAFE_EXPORTS). Without this the packaged module server 404s on_veryfront/index.client.jsand the redirect breaks in the npm distribution — dev worked only because it serves straight fromsrc/. (Addresses the Codex P1.)Validation
veryfront-router-testing/pages-server-import-leak(both vectors) hydrates clean on the source-run and on the packagednpm packartifact —GET /_vf_modules/_veryfront/index.client.jsreturns 200 (not 404), built barrel is server-free.default-configgate: zero new mismatches.deno task test:unit: 2544 passed, 0 failed. Build tests (browser-safe-exports,npm-package-metadata) green.Scope note
This reproducer also surfaced a sibling
.ts/.tsxextension-normalization bug. That fix landed independently on main (alias-strategygetProjectRelativePathrefactor), so it is not part of this PR — rebased onto it.🤖 Generated with Claude Code