typescript-barrel-split-default-export-router
Goal: split backend/api/routes/import.ts (465L) into an import/ module directory where the router's only export is export default router, with zero changes to importers (app.ts default import, tests via original path).
Why this variant is special (LF-039 precedent): export * never re-exports a default. When the barrel's only public export is the default, the shim must be export { default } from "./import/index.js" — the export * form silently drops the default (verified: TS1192 "Module has no default export").
Final structure:
backend/api/routes/import.ts ← shim (replaces original, same path)
backend/api/routes/import/
├── index.ts ← barrel: assembles router, `export default router;` (only ^export line)
├── types.ts ← ImportStatus/ImportRecord/ImportRow/ImportSummary
├── schemas.ts ← zod schemas (recordSchema, idParamSchema, commitBodySchema)
├── helpers.ts ← parseCsv, summarize, createRecord, asyncHandler, notFound, constants
├── upload.ts ← multer instance (memoryStorage, size/mime limits)
└── events.ts ← EventEmitter + emit()
Shim (import.ts, 1 line):
export { default } from "./import/index.js";
Barrel (import/index.ts) — submodule imports, then export default router; as its sole export:
import { Router } from "express";
import type { Request, Response, NextFunction } from "express";
import { upload, uploadFieldName } from "./upload.js";
import { idParamSchema, commitBodySchema } from "./schemas.js";
import { createRecord, parseCsv, summarize, asyncHandler, notFound } from "./helpers.js";
import { emit } from "./events.js";
import type { ImportRecord } from "./types.js";
const memoryStore: Map<string, ImportRecord> = new Map();
const router = Router();
// ... POST /, GET /:id, POST /:id/commit, DELETE /:id, GET /, error handler ...
export default router;
Untouched importers resolve through the shim automatically: app.ts keeps import importRouter from "./api/routes/import.js" and the test keeps importing via the original path. Resolution order (file import.ts beats directory import/index.ts for ./import imports, while the shim's explicit ./import/index.js targets the barrel) makes the swap invisible.
Built a full reproduction repo (`/tmp/lf101-repro`: Express + multer + zod, NodeNext ESM, `git` history) with a 296-line single-file router committed as the pre-split baseline (`087b3ac`), then applied the split. Judge checks — **8/8 PASS**:
```
PASS 1. pre-split file has exactly one export line (export default router)
PASS 2. barrel index.ts has exactly one export line (export default router)
PASS 3. export parity: git show 087b3ac:.../import.ts | grep '^export' == barrel grep '^export'
PASS 4. shim uses default re-export (NOT export *)
PASS 5. importer app.ts has ZERO changes vs pre-split commit
PASS 6. test file has ZERO changes vs pre-split commit
PASS 7. tsc build clean (default import resolves through shim)
PASS 8. runtime test via original path passes
```
Edge cases:
- **Negative control:** swapping shim to `export * from "./import/index.js"` → `app.ts(2,8): error TS1192: Module ... has no default export` — proves `export { default }` is mandatory for the default-only variant.
- **Runtime shape:** emitted shim namespace exposes only `["default"]`; default is a function with 6 registered route handlers; routed live request through shim returned expected 404; submodule internals (types/schemas/helpers/upload/events) are **not** leaked through the barrel.
- **Coexistence:** tsc/Node handle `import.ts` (file) + `import/` (dir) — emitted `import.js` + `import/index.js` resolve correctly under NodeNext; only the shim references the `import/` dir.
- **Zero-change guarantee:** `git diff 087b3ac -- src/backend/app.ts tests/import.test.ts` empty; final diff touches only the route files.{"model": "deepseek-v4-flash", "problem_class": "typescript-barrel-split-default-export-router", "result": "passed", "tests": 8}