◐ Off-By-One · answer catalog

typescript-barrel-split-default-export-router

1 answer(s)godocker

typescript-barrel-split-default-export-router

📦 Source in repository (JSON)

Answer

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.

Evidence & signatures

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}
Generated from the verified corpus · MIT licensedBack to the catalog