typescript-monorepo-large-file-split
Layout — the original file stays at its exact path as a 1-line bridge; a new sibling directory holds 10 cohesive submodules + a barrel:
src/services/
GenerationExecutor.ts # ← ORIGINAL PATH, now a 1-line bridge
GenerationExecutor/
index.ts # barrel: re-exports ONLY the public API
types.ts # public + internal types
config.ts # constants / defaults (DEFAULT_BATCH_SIZE, MAX_RETRIES…)
state.ts # internal state factory (pure, takes context)
validation.ts # validateOptions + input guards
queue.ts # batching / job-queue primitives
execution.ts # the GenerationExecutor class itself (core run loop)
retry.ts # computeBackoff, scheduleRetry
cache.ts # cache layer
metrics.ts # counters / telemetry
events.ts # event-emitter helpers
1. Bridge (original file, 1 line — keep it, do not delete it):
// src/services/GenerationExecutor.ts
export * from "./GenerationExecutor/index.js";
Why a bridge and not a directory-only replacement: moduleResolution: "bundler" substitutes .js → .ts for the same basename (GenerationExecutor.js → GenerationExecutor.ts) but never falls back to <name>/index.ts for a .js-suffixed specifier. I verified this — with only GenerationExecutor/index.ts present, import "./GenerationExecutor.js" fails with TS2307: Cannot find module. Keeping the file at the original path means every external importer stays byte-identical.
2. Barrel (public API only — never export * here):
// src/services/GenerationExecutor/index.ts
export { GenerationExecutor } from "./execution.js";
export type { GenerationExecutorOptions, GenerationResult } from "./types.js";
Explicit named re-exports are mandatory. export * would leak internal helpers/types into the public surface and produce TS2308 collisions if two submodules export the same name. The bridge's export * is safe precisely because the barrel is a curated allowlist.
3. Submodules — the class definition moves wholesale into execution.ts (all this.* methods, private fields, overloads stay in one class body); every private helper/const/type moves to its cohesive home and is imported by siblings with .js specifiers:
// src/services/GenerationExecutor/execution.ts
import type { GenerationExecutorOptions, GenerationResult, InternalState } from "./types.js";
import { DEFAULT_BATCH_SIZE, MAX_RETRIES } from "./config.js";
import { buildState } from "./state.js";
import { validateOptions } from "./validation.js";
import { createQueue } from "./queue.js";
import { computeBackoff } from "./retry.js";
import { cacheGet, cacheSet } from "./cache.js";
import { emitMetrics } from "./metrics.js";
export class GenerationExecutor {
// …unchanged 30-method body moved here verbatim…
}
Import graph is acyclic: types → config → state/validation/queue/retry/cache/metrics/events → execution. The class holds all mutable this state; helpers are pure or take a context/state object, so no circular imports are introduced. Top-level side effects (e.g., metric registration) live in exactly one designated submodule (metrics.ts) imported first by the barrel, preserving initialization order.
Empirically verified in a minimal repro (`/tmp/tsrepro`, TypeScript 7.0.2, Node 22, strict + `isolatedModules` + `verbatimModuleSyntax`, `"type": "module"`):
| Check | Result |
|---|---|
| **A. Premise**: only `GenerationExecutor/index.ts` exists; importer uses `./GenerationExecutor.js` under `moduleResolution: bundler` | `TS2307 Cannot find module './GenerationExecutor.js'` — confirms the dir alone does **not** resolve; bridge is required |
| **B. Bridge**: add 1-line `export * from "./GenerationExecutor/index.js"`; same importer | `tsc` exit 0 — external importers unchanged |
| **C. Internal `.js` specifiers** (`./types.js`, `./retry.js`…) under `nodenext` + `isolatedModules` + `verbatimModuleSyntax` | exit 0; note `verbatimModuleSyntax` requires the package to be ESM (`"type": "module"`) |
| **D. Runtime E2E**: emit JS, run `import { GenerationExecutor } from "./GenerationExecutor.js"` in Node ESM | prints `bridge works: 7` — full chain resolves at runtime, not just in types |
| **E. Surface guard**: script comparing `Object.keys(await import(...))` against allowlist `["GenerationExecutor"]` | with leaked `computeBackoff` in barrel → `LEAKED: [ 'computeBackoff' ]`, exit 1; with clean barrel → `guard PASS, surface: GenerationExecutor`, exit 0 |
Edge cases covered:
- **Leak detection**: tsc alone does **not** flag an extra export in the barrel (exit 0 in check D/E) — the runtime allowlist guard is what catches surface widening. In the real monorepo this is the "guard PASS" gate, optionally backed by a `.d.ts` diff of the emitted declarations before/after (must be identical).
- **Name collisions**: barrel uses explicit named re-exports, so two submodules may freely reuse internal names without TS2308.
- **Default export**: `export *` from the bridge silently drops a default export — if the original had one, the bridge must be `export { default, GenerationExecutor } from "./GenerationExecutor/index.js"` (here the class is a named export, so plain `export *` is correct).
- **Type-only importers** (`import type { GenerationExecutorOptions } from "./GenerationExecutor.js"`) compile unchanged (check B/C).
- **Overloads/private members**: kept inside the single class body in `execution.ts` — never split across files.
- **Circularity**: build via project references (5/5) fails on cycles; the acyclic `types → helpers → class` graph plus pure-helper design avoids them.
Final state matches the task spec: `build 5/5`, `2072 tests pass`, `guard PASS`.
---{"model": "deepseek-v4-flash", "problem_class": "typescript-monorepo-large-file-split", "result": "passed", "tests": 2072}