Skip to content

Binding inference ignores the .dev / .prod route suffix #302

Closed

Description

@Brooooooklyn

Summary

inferProjectBindings scans source files for binding imports without parsing route filenames. Since #297 enforces the .dev / .prod route suffix, a dev-only route is excluded from the production bundle but its bindings are still emitted and provisioned.

packages/void/src/plugin-inference.ts:148:

files = globSync(SOURCE_GLOB, { cwd: dirPath, absolute: true });

SOURCE_GLOB is **/*.{ts,tsx,mts,js,jsx,mjs,astro}. Nothing filters by filename.

Measured

Fixture: routes/api/health.ts plus routes/api/debug.dev.ts, where only the latter imports void/db, void/kv, void/storage, void/ai, void/sandbox, and void/live.

scanRoutePatternsSync(prod)       = ["api/health.ts"]
inferProjectBindings(fixture)     = { needsD1: true, needsKV: true, needsR2: true,
                                      needsAI: true, needsSandbox: true, needsLive: true }
computeDesiredCloudflareResources = { d1: {...}, kvNamespaces: [...], r2: {...} }

The route is gone from production. Its resources are not. The reasons map still reports "app uses env.DB (D1)", which is no longer true for a production deploy.

Impact

Path Consequence Hard failure
void deploy --backend cloudflare, no --provision deploy.ts:3927 checkCloudflareProvisionDrift exits 1, "Deploy blocked" yes
void deploy --backend cloudflare --provision creates a real KV / R2 / D1 no
managed void deploy platform ensureResource creates a real KV / R2 / D1 no
void/sandbox, void/live ships a container app and a Durable Object namespace no

Correction to the original report

The finding was reported as "sandbox-specific preflight can even reject the deployment". That mechanism does not exist. requiresSandboxPlatformImage (packages/void/src/sandbox/config.ts:73) returns false unless void.json contains a sandbox object, and when it does, deploy.ts:6001 sets needsSandbox = true independently of any route file. A route import cannot influence that check in either direction.

The real hard failure is the provision-drift gate on KV, R2, and D1.

Why this was not fixed in #297

  1. The environment is not knowable at the call site. inferProjectBindings runs at index.ts:678, during plugin construction. routeEnvironment is first assigned at index.ts:1324, inside the config hook.
  2. Ten consumers read bindings before that hook, three of which cannot be deferred: Durable Object name reservation (:769), plugin-array membership (:1828), and routingPlugin options (:1844).
  3. The naive fix is wrong. Filtering the glob results by filename would regress projects today. The suffix is meaningful in routes/ only, but inference scans six directories by default (routes, middleware, queues, pages, crons, src) and seven in framework mode, where the same filename pattern carries no routing meaning. A file named queues/sync.dev.ts would silently lose its binding.

Note on the safe direction

Over-emitting is currently deliberate and should stay the default on any ambiguous path. A spare binding costs a resource. A missing one produces an undefined binding at runtime and can change a generated Durable Object class name between commands, orphaning that object's stored state. The same reasoning is already written down for WebSocket routes at packages/void/src/index.ts:759-767.

Suggested shape of a fix

  • plugin-inference.ts grows an environment?: RouteEnvironment option that filters only files under paths.routesDir, via parseRouteFilename, leaving src/, queues/, pages/, crons/, middleware/ and every framework directory untouched.
  • index.ts splits into an eager unfiltered set — for name reservation, plugin membership, needsNodejsCompat, and the framework / node-target guards — and a lazy environment-filtered set recomputed inside mergeBindings.
  • cli/deploy.ts threads 'prod' into all three call sites: :2526, :5657, :5999.
  • codegen/prepare.ts stays unfiltered, since it has no environment. See Route type emitter does not group by pattern, producing duplicate RouteMap keys #301.

Roughly four to five source files plus a reordering of voidPlugin()'s construction.

Current behaviour is documented and pinned

e840dd89 on #297 corrects docs/guide/server-routing.md and packages/void/AGENTS.md, which had claimed a dev-only route is absent from the production Cloudflare config, and points at inference.bindings as the opt-out. packages/void/test/unit/plugin-inference.test.ts pins the boundary with the reasoning, so changing it is deliberate.

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