[GH-ISSUE #253] Refactor: Extract template string code generation from index.ts and app-dev-server.ts into separate modules #65

Open
opened 2026-05-06 12:36:56 +02:00 by BreizhHardware · 3 comments

Originally created by @southpolesteve on GitHub (Mar 5, 2026).
Original GitHub issue: https://github.com/cloudflare/vinext/issues/253

Problem

index.ts (3,528 lines) and app-dev-server.ts (2,734 lines) are the two largest files in the codebase. Both files mix Vite plugin logic with massive template string blocks that generate runtime JavaScript modules. This creates several problems:

  1. AI agents can't hold the entire file in context. These files exceed typical context windows, leading to partial reads and missed interactions between sections.
  2. Template string edits are error-prone. The generated code uses escaped characters (\\n, \\.), var instead of const/let, and mixes interpolated expressions with static code. AI agents routinely break escaping or confuse plugin code with generated runtime code.
  3. No way to test generated code in isolation. The template strings are embedded in plugin functions, so the only way to verify generated output is to run the full dev/build pipeline.

What needs to be extracted

From index.ts

Template Lines Span Function Generates
Pages Router prod server entry 806-1549 ~743 lines generateServerEntry() Complete production server module: SSR, ISR, API routes, i18n, asset injection, middleware, route matching
Middleware runtime 655-801 ~147 lines middlewareExportCode variable Middleware matching and execution, ReDoS protection (embedded inside the server entry)
Pages Router client entry 1576-1634 ~58 lines generateClientEntry() Client hydration: page loaders, _app wrapping, hydrateRoot()

The server entry template (743 lines) is the primary target. It contains the entire Pages Router production request lifecycle as a template string.

From app-dev-server.ts

Template Lines Span Function Generates
RSC entry 201-2060 ~1,860 lines generateRscEntry() The entire App Router request handler: route matching, middleware, CSRF, config redirects/rewrites/headers, metadata routes, server actions, layout tree construction, RSC streaming, caching
SSR entry 2070-2425 ~356 lines generateSsrEntry() RSC-to-HTML rendering, progressive streaming, font/head injection
Browser entry 2436-2733 ~298 lines generateBrowserEntry() Client hydration, RSC navigation, server actions, prefetch cache, HMR

The RSC entry template (1,860 lines) is the largest single template string in the codebase. It contains at least 15 logically distinct subsystems inlined as generated code.

Proposed approach

1. Create an entries/ directory

packages/vinext/src/entries/
  pages-server-entry.ts      # From index.ts generateServerEntry()
  pages-client-entry.ts      # From index.ts generateClientEntry()
  app-rsc-entry.ts           # From app-dev-server.ts generateRscEntry()
  app-ssr-entry.ts           # From app-dev-server.ts generateSsrEntry()
  app-browser-entry.ts       # From app-dev-server.ts generateBrowserEntry()

Each file exports a function that takes the same parameters the current generator functions receive (route table, config, feature flags) and returns the generated code string.

2. Add snapshot tests

For each extracted template function, add a test that:

  • Calls the function with representative inputs (a few routes, middleware present/absent, basePath set/unset, i18n enabled/disabled)
  • Snapshots the generated output
  • When an AI edits a template, the snapshot diff shows exactly what changed in the generated code

This is the key safety improvement. Currently there's no way to see what the generated code looks like without running the full pipeline.

3. Reduce index.ts and app-dev-server.ts to orchestration

After extraction, index.ts should be the Vite plugin shell (resolveId, load, configureServer, config hooks) that calls into the entry generators. app-dev-server.ts should be a thin module that calls the three App Router entry generators.

Non-goals

  • Don't eliminate the template string pattern entirely. Code generation via template strings is the right approach for virtual modules that need dynamic imports based on the scanned route table. The goal is to isolate the templates, not replace them.
  • Don't refactor the generated code's internal logic. That's a separate concern (see the shared request handler issue). This issue is purely about file organization.

Verification

  • pnpm test (all Vitest tests pass)
  • pnpm run test:e2e (all Playwright E2E tests pass)
  • pnpm run typecheck
  • pnpm run lint
  • Manually verify that index.ts is under 1,000 lines and app-dev-server.ts is under 500 lines after extraction
Originally created by @southpolesteve on GitHub (Mar 5, 2026). Original GitHub issue: https://github.com/cloudflare/vinext/issues/253 ## Problem `index.ts` (3,528 lines) and `app-dev-server.ts` (2,734 lines) are the two largest files in the codebase. Both files mix Vite plugin logic with massive template string blocks that generate runtime JavaScript modules. This creates several problems: 1. **AI agents can't hold the entire file in context.** These files exceed typical context windows, leading to partial reads and missed interactions between sections. 2. **Template string edits are error-prone.** The generated code uses escaped characters (`\\n`, `\\.`), `var` instead of `const/let`, and mixes interpolated expressions with static code. AI agents routinely break escaping or confuse plugin code with generated runtime code. 3. **No way to test generated code in isolation.** The template strings are embedded in plugin functions, so the only way to verify generated output is to run the full dev/build pipeline. ## What needs to be extracted ### From `index.ts` | Template | Lines | Span | Function | Generates | |----------|-------|------|----------|-----------| | Pages Router prod server entry | 806-1549 | ~743 lines | `generateServerEntry()` | Complete production server module: SSR, ISR, API routes, i18n, asset injection, middleware, route matching | | Middleware runtime | 655-801 | ~147 lines | `middlewareExportCode` variable | Middleware matching and execution, ReDoS protection (embedded inside the server entry) | | Pages Router client entry | 1576-1634 | ~58 lines | `generateClientEntry()` | Client hydration: page loaders, `_app` wrapping, `hydrateRoot()` | The server entry template (743 lines) is the primary target. It contains the entire Pages Router production request lifecycle as a template string. ### From `app-dev-server.ts` | Template | Lines | Span | Function | Generates | |----------|-------|------|----------|-----------| | RSC entry | 201-2060 | ~1,860 lines | `generateRscEntry()` | The entire App Router request handler: route matching, middleware, CSRF, config redirects/rewrites/headers, metadata routes, server actions, layout tree construction, RSC streaming, caching | | SSR entry | 2070-2425 | ~356 lines | `generateSsrEntry()` | RSC-to-HTML rendering, progressive streaming, font/head injection | | Browser entry | 2436-2733 | ~298 lines | `generateBrowserEntry()` | Client hydration, RSC navigation, server actions, prefetch cache, HMR | The RSC entry template (1,860 lines) is the largest single template string in the codebase. It contains at least 15 logically distinct subsystems inlined as generated code. ## Proposed approach ### 1. Create an `entries/` directory ``` packages/vinext/src/entries/ pages-server-entry.ts # From index.ts generateServerEntry() pages-client-entry.ts # From index.ts generateClientEntry() app-rsc-entry.ts # From app-dev-server.ts generateRscEntry() app-ssr-entry.ts # From app-dev-server.ts generateSsrEntry() app-browser-entry.ts # From app-dev-server.ts generateBrowserEntry() ``` Each file exports a function that takes the same parameters the current generator functions receive (route table, config, feature flags) and returns the generated code string. ### 2. Add snapshot tests For each extracted template function, add a test that: - Calls the function with representative inputs (a few routes, middleware present/absent, basePath set/unset, i18n enabled/disabled) - Snapshots the generated output - When an AI edits a template, the snapshot diff shows exactly what changed in the generated code This is the key safety improvement. Currently there's no way to see what the generated code looks like without running the full pipeline. ### 3. Reduce `index.ts` and `app-dev-server.ts` to orchestration After extraction, `index.ts` should be the Vite plugin shell (resolveId, load, configureServer, config hooks) that calls into the entry generators. `app-dev-server.ts` should be a thin module that calls the three App Router entry generators. ### Non-goals - **Don't eliminate the template string pattern entirely.** Code generation via template strings is the right approach for virtual modules that need dynamic imports based on the scanned route table. The goal is to isolate the templates, not replace them. - **Don't refactor the generated code's internal logic.** That's a separate concern (see the shared request handler issue). This issue is purely about file organization. ## Verification - `pnpm test` (all Vitest tests pass) - `pnpm run test:e2e` (all Playwright E2E tests pass) - `pnpm run typecheck` - `pnpm run lint` - Manually verify that `index.ts` is under 1,000 lines and `app-dev-server.ts` is under 500 lines after extraction
Author
Owner

@james-elicx commented on GitHub (Mar 8, 2026):

For contributors: I think it would be best to do this incrementally (starting with snapshots) instead of all-at-once, so that we can better avoid regressions with our fast rate-of-change.

<!-- gh-comment-id:4018853344 --> @james-elicx commented on GitHub (Mar 8, 2026): For contributors: I think it would be best to do this incrementally (starting with snapshots) instead of all-at-once, so that we can better avoid regressions with our fast rate-of-change.
Author
Owner

@southpolesteve commented on GitHub (Mar 20, 2026):

Reopening. PR #610 hit multiple codegen issues that reinforce the need for this refactor: TypeScript syntax in generated JS causing parse failures, backticks in comments breaking the formatter, snapshot churn on every change, and dynamic detection flags leaking across pipeline stages because the logic is inlined rather than properly scoped in a real function.

The __handleRouteWithIsrCache extraction in PR #610 is a first step toward this, but the function still lives inside the template string. Moving it into a real TypeScript module would be the next step.

<!-- gh-comment-id:4100377483 --> @southpolesteve commented on GitHub (Mar 20, 2026): Reopening. PR #610 hit multiple codegen issues that reinforce the need for this refactor: TypeScript syntax in generated JS causing parse failures, backticks in comments breaking the formatter, snapshot churn on every change, and dynamic detection flags leaking across pipeline stages because the logic is inlined rather than properly scoped in a real function. The __handleRouteWithIsrCache extraction in PR #610 is a first step toward this, but the function still lives inside the template string. Moving it into a real TypeScript module would be the next step.
Author
Owner

@NathanDrake2406 commented on GitHub (May 1, 2026):

The coupled god files have been crippling our speed. I plan to refactor this to be modular with clear separation so that we can parallel tasks/features without wasting time in merge hell.
Also a pattern I've noticed is that agents tend to follow the codebase's existing convention and design. If we refactor to be elegant, well-tested code then there's a higher chance going forward the agents will follow that.

Refactor roadmap

Single thesis: two monoliths today → pure cores + thin shells + thin generated wiring tomorrow. Every PR moves one decision out of an effect-heavy shell.

Legend: 🟢 already merged · 🟦 open PR (in flight) · planned · 🟨 target state

flowchart TB
    classDef pain    fill:#7f1d1d,stroke:#fca5a5,color:#fee2e2
    classDef done    fill:#14532d,stroke:#22c55e,color:#dcfce7,stroke-width:2px
    classDef flight  fill:#064e3b,stroke:#34d399,color:#d1fae5,stroke-width:2px,stroke-dasharray: 4 2
    classDef plan    fill:#1e293b,stroke:#64748b,color:#cbd5e1
    classDef goal    fill:#3f2d04,stroke:#fbbf24,color:#fef3c7,stroke-width:3px

    Today["TODAY — logic still embedded in monoliths<br/><br/>app-rsc-entry.ts — 1,643 LOC<br/>prod-server.ts — 1,747 LOC"]:::pain

    Done["🟢 STEP 0 — extractions already merged<br/><br/>#983 early request pipeline helpers<br/>#968 app route handler dispatch<br/>#967 server action RSC flow<br/>#966 app RSC manifest construction<br/>#965 app prerender endpoints<br/>#964 RSC runtime primitives<br/>#953 RSC route matching<br/>#952 RSC preload hint normalization<br/>#842 build-time layout classification<br/>#838 centralize request-derived page inputs<br/><br/>result: 60+ files in server/, request-pipeline.ts seeded"]:::done

    S1["🟦 STEP 1 — finish App RSC extraction<br/><i>sequential, single hot file</i>"]:::flight
    H1["🟦 #998 route graph builder<br/>🟦 #997 build classification injector<br/>🟦 #996 metadata helper dedupe<br/>🟦 #995 server action dev warmup<br/>🟦 #978 outputHashSalt · 🟦 #973 sourcemaps"]:::flight
    H2["🟦 #961 ISR expire → 🟦 #993 layout config →<br/>🟦 #991 RSC cache-bust → 🟦 #984 stream tees →<br/>🟦 #979 unstable_io → 🟦 #986 page dispatch"]:::flight
    H3["⬜ compose: createAppRscHandler()<br/>exit gate: app-rsc-entry.ts under 300 LOC"]:::plan

    S2["⬜ STEP 2 — pull pure cores out of shells<br/><i>parallel lanes, low file overlap</i>"]:::plan
    LA["⬜ Lane A · App RSC core<br/>RouteOutcome sum type"]:::plan
    LB["⬜ Lane B · Cache policy core<br/>HIT · STALE · MISS · BYPASS"]:::plan
    LC["⬜ Lane C · Middleware outcome core<br/>rewrite/redirect/header plan"]:::plan
    LD["⬜ Lane D · Build & deploy planner<br/>capability + prerender graph"]:::plan
    LCfg["⬜ Lane Cfg · Request pipeline<br/>basePath · redirects · rewrites · headers"]:::plan

    S3["⬜ STEP 3 — kernels on top of stable cores<br/><br/>#726 router kernel<br/>route facts compiler + proofs ledger"]:::plan

    S4["⬜ STEP 4 — Pages adopts shared cores<br/><br/>prod-server.ts dissolves into thin SSR shell<br/>consumes Lane B + C + Cfg"]:::plan

    Target["🟨 TARGET STATE<br/><br/><b>entries/*.ts</b> — thin wiring only (codegen glue)<br/><b>server/*-core.ts</b> — pure deciders, no I/O, unit-tested<br/><b>server/*-shell.ts</b> — ALS, Request/Response, streams, cache I/O<br/><br/>same cores serve App + Pages + worker entry<br/>new variants break compile · every decision testable in isolation"]:::goal

    Today --> Done --> S1
    S1 --> H1 & H2 --> H3 --> S2
    S2 --> LA & LB & LC & LD & LCfg
    LA & LB & LC & LCfg --> S3
    LB & LC & LCfg --> S4
    S3 --> Target
    S4 --> Target
<!-- gh-comment-id:4358103296 --> @NathanDrake2406 commented on GitHub (May 1, 2026): The coupled god files have been crippling our speed. I plan to refactor this to be modular with clear separation so that we can parallel tasks/features without wasting time in merge hell. Also a pattern I've noticed is that agents tend to follow the codebase's existing convention and design. If we refactor to be elegant, well-tested code then there's a higher chance going forward the agents will follow that. ## Refactor roadmap Single thesis: **two monoliths today → pure cores + thin shells + thin generated wiring tomorrow.** Every PR moves one decision out of an effect-heavy shell. **Legend**: 🟢 already merged · 🟦 open PR (in flight) · ⬜ planned · 🟨 target state ```mermaid flowchart TB classDef pain fill:#7f1d1d,stroke:#fca5a5,color:#fee2e2 classDef done fill:#14532d,stroke:#22c55e,color:#dcfce7,stroke-width:2px classDef flight fill:#064e3b,stroke:#34d399,color:#d1fae5,stroke-width:2px,stroke-dasharray: 4 2 classDef plan fill:#1e293b,stroke:#64748b,color:#cbd5e1 classDef goal fill:#3f2d04,stroke:#fbbf24,color:#fef3c7,stroke-width:3px Today["TODAY — logic still embedded in monoliths<br/><br/>app-rsc-entry.ts — 1,643 LOC<br/>prod-server.ts — 1,747 LOC"]:::pain Done["🟢 STEP 0 — extractions already merged<br/><br/>#983 early request pipeline helpers<br/>#968 app route handler dispatch<br/>#967 server action RSC flow<br/>#966 app RSC manifest construction<br/>#965 app prerender endpoints<br/>#964 RSC runtime primitives<br/>#953 RSC route matching<br/>#952 RSC preload hint normalization<br/>#842 build-time layout classification<br/>#838 centralize request-derived page inputs<br/><br/>result: 60+ files in server/, request-pipeline.ts seeded"]:::done S1["🟦 STEP 1 — finish App RSC extraction<br/><i>sequential, single hot file</i>"]:::flight H1["🟦 #998 route graph builder<br/>🟦 #997 build classification injector<br/>🟦 #996 metadata helper dedupe<br/>🟦 #995 server action dev warmup<br/>🟦 #978 outputHashSalt · 🟦 #973 sourcemaps"]:::flight H2["🟦 #961 ISR expire → 🟦 #993 layout config →<br/>🟦 #991 RSC cache-bust → 🟦 #984 stream tees →<br/>🟦 #979 unstable_io → 🟦 #986 page dispatch"]:::flight H3["⬜ compose: createAppRscHandler()<br/>exit gate: app-rsc-entry.ts under 300 LOC"]:::plan S2["⬜ STEP 2 — pull pure cores out of shells<br/><i>parallel lanes, low file overlap</i>"]:::plan LA["⬜ Lane A · App RSC core<br/>RouteOutcome sum type"]:::plan LB["⬜ Lane B · Cache policy core<br/>HIT · STALE · MISS · BYPASS"]:::plan LC["⬜ Lane C · Middleware outcome core<br/>rewrite/redirect/header plan"]:::plan LD["⬜ Lane D · Build & deploy planner<br/>capability + prerender graph"]:::plan LCfg["⬜ Lane Cfg · Request pipeline<br/>basePath · redirects · rewrites · headers"]:::plan S3["⬜ STEP 3 — kernels on top of stable cores<br/><br/>#726 router kernel<br/>route facts compiler + proofs ledger"]:::plan S4["⬜ STEP 4 — Pages adopts shared cores<br/><br/>prod-server.ts dissolves into thin SSR shell<br/>consumes Lane B + C + Cfg"]:::plan Target["🟨 TARGET STATE<br/><br/><b>entries/*.ts</b> — thin wiring only (codegen glue)<br/><b>server/*-core.ts</b> — pure deciders, no I/O, unit-tested<br/><b>server/*-shell.ts</b> — ALS, Request/Response, streams, cache I/O<br/><br/>same cores serve App + Pages + worker entry<br/>new variants break compile · every decision testable in isolation"]:::goal Today --> Done --> S1 S1 --> H1 & H2 --> H3 --> S2 S2 --> LA & LB & LC & LD & LCfg LA & LB & LC & LCfg --> S3 LB & LC & LCfg --> S4 S3 --> Target S4 --> Target ```
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#65
No description provided.