Skip to content

fix(transforms): stop the veryfront server barrel from leaking into client chunks - #3025

Merged
kojiwakayama merged 2 commits into
mainfrom
fix/server-barrel-client-leak
Jul 22, 2026
Merged

fix(transforms): stop the veryfront server barrel from leaking into client chunks#3025
kojiwakayama merged 2 commits into
mainfrom
fix/server-barrel-client-leak

Conversation

@mattboon

@mattboon mattboon commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Problem

A used value import from the veryfront root 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 pulls server/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 build stay 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

  • Client/SSR-safe barrel (src/index.client.ts): mirrors the root barrel minus the #veryfront/server bootstrap value export. The import rewriter redirects veryfront → it for the browser/ssr targets (same mechanism as veryfront/workflow). The client now pulls ~5 browser-safe sub-barrels instead of the whole server runtime.
  • Ship it in the npm package: dnt derives entry points from deno.json exports, so the barrel is added to exports (+ 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.)

Validation

  • Reproducer veryfront-router-testing/pages-server-import-leak (both vectors) hydrates clean on the source-run and on the packaged npm pack artifactGET /_vf_modules/_veryfront/index.client.js returns 200 (not 404), built barrel is server-free.
  • New router-testing guard asserts no server module reaches the client bundle (PASS on the fix; FAIL on 0.1.1103 catching 15+ server modules/route).
  • Canonical default-config gate: 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/.tsx extension-normalization bug. That fix landed independently on main (alias-strategy getProjectRelativePath refactor), so it is not part of this PR — rebased onto it.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/transforms/import-rewriter/strategies/veryfront-strategy.ts
… 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
mattboon force-pushed the fix/server-barrel-client-leak branch from c00f698 to 0b6c575 Compare July 22, 2026 15:40
@mattboon mattboon changed the title fix(transforms): stop the veryfront server barrel + raw .ts/.tsx from leaking into client chunks fix(transforms): stop the veryfront server barrel from leaking into client chunks Jul 22, 2026
@kojiwakayama
kojiwakayama enabled auto-merge (squash) July 22, 2026 17:27
@kojiwakayama
kojiwakayama merged commit 06e7788 into main Jul 22, 2026
30 checks passed
@kojiwakayama
kojiwakayama deleted the fix/server-barrel-client-leak branch July 22, 2026 17:32
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.
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