Repository navigation
Route type emitter does not group by pattern, producing duplicate RouteMap keys #301
Description
Summary
generateRouteTypes emits one RouteMap member per route file, with no grouping by route.pattern. Two route files that resolve to the same pattern therefore produce a duplicate key in a single interface.
packages/void/src/codegen/route-types.ts:147 (and :176 for WebSocketRouteMap):
for (const route of scan.routes) {
...
lines.push(` ${JSON.stringify(route.pattern)}: {`);Reproducers
Verified by running the real scanner and the real generator on temp fixtures. "dup" means the emitted interface RouteMap contains the same key twice.
| Layout | unfiltered | dev | prod |
|---|---|---|---|
api/health.dev.ts + api/health.prod.ts |
dup | ok | ok |
api/health.ts + api/health.dev.ts |
dup | dup | ok |
api/health.ts + api/health.prod.ts |
dup | ok | dup |
api/health.ts + api/health/index.ts |
dup | dup | dup |
thing.ts + (admin)/thing.ts |
dup | dup | dup |
The last two carry no environment suffix at all. Trailing index is stripped at parse-filename.ts:57-60 and route groups at :63, so both layouts are legal today and both collide in every environment.
Impact
The emitted file is a .d.ts, so the visible symptom depends on skipLibCheck.
skipLibCheck: false—TS2300duplicate identifier, plusTS2717.skipLibCheck: true— whichvoid initscaffolds and every playground sets — the typecheck passes and TypeScript keeps the first declaration.tinyglobbyreturnshealth.dev.tsbeforehealth.prod.ts, so a CI typecheck silently validates calls against the handler that never ships.
The second case is the dangerous one: a green typecheck that lets a wrong call reach production.
Open question: what environment should void prepare mean
void prepare boots no Vite, so it has no command to read an environment from. It currently passes none, at packages/void/src/codegen/prepare.ts:73, and so generates types for both environments. Every other writer of .void/routes.d.ts is environment-specific.
Choosing an environment for prepare is not a clean fix, and it interacts with the bug above:
- It does not resolve rows 4 and 5 of the table, which are independent of the suffix.
hasRouteTypesatprepare.ts:74-81is computed from the same scan and drives deletion. For a project whose routes are all.dev.ts, filtering to'prod'makespreparedeleteroutes.d.tsand emptytsconfig.files. The nextvoid genrewrites the tsconfig to reference the deleted file, becausecreateProjectTsconfigfalls back toexistsSync(routesDir)atproject-tsconfig.ts:36and every CLI caller exceptpreparepassesundefined. Result:error TS6053, a project-wide failure.docs/reference/cli.mddescribesprepareas serving CI, fresh clones, and editor bootstrap. CI wants production types; editor bootstrap wants development types.
So the emitter should probably be fixed first, and the prepare environment decided separately.
Suggested direction
- Group by
route.patterningenerateRouteTypesand decide what a collision emits — merge the members, or pick a winner and warn. - Then decide what
void prepareshould generate, given that its documented audiences disagree.
Note on tooling
tsgo, which every playground typecheck script uses, exits 0 with no output on TS6053. Plain tsc reports it. Any test covering this should not rely on tsgo alone.
Context
Found while reviewing #297, which enforces the .dev / .prod suffix. That PR does not touch this code path: isRouteEnabledForEnvironment returns true when no environment is passed, so prepare emits byte-identical output before and after it. This defect is pre-existing on main.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity