Skip to content

Route type emitter does not group by pattern, producing duplicate RouteMap keys #301

Closed

Description

@Brooooooklyn

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 — TS2300 duplicate identifier, plus TS2717.
  • skipLibCheck: true — which void init scaffolds and every playground sets — the typecheck passes and TypeScript keeps the first declaration. tinyglobby returns health.dev.ts before health.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.
  • hasRouteTypes at prepare.ts:74-81 is computed from the same scan and drives deletion. For a project whose routes are all .dev.ts, filtering to 'prod' makes prepare delete routes.d.ts and empty tsconfig.files. The next void gen rewrites the tsconfig to reference the deleted file, because createProjectTsconfig falls back to existsSync(routesDir) at project-tsconfig.ts:36 and every CLI caller except prepare passes undefined. Result: error TS6053, a project-wide failure.
  • docs/reference/cli.md describes prepare as 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

  1. Group by route.pattern in generateRouteTypes and decide what a collision emits — merge the members, or pick a winner and warn.
  2. Then decide what void prepare should 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions