Repository navigation
Binding inference ignores the .dev / .prod route suffix #302
Description
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
- The environment is not knowable at the call site.
inferProjectBindingsruns atindex.ts:678, during plugin construction.routeEnvironmentis first assigned atindex.ts:1324, inside theconfighook. - Ten consumers read
bindingsbefore that hook, three of which cannot be deferred: Durable Object name reservation (:769), plugin-array membership (:1828), androutingPluginoptions (:1844). - 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 namedqueues/sync.dev.tswould 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.tsgrows anenvironment?: RouteEnvironmentoption that filters only files underpaths.routesDir, viaparseRouteFilename, leavingsrc/,queues/,pages/,crons/,middleware/and every framework directory untouched.index.tssplits into an eager unfiltered set — for name reservation, plugin membership,needsNodejsCompat, and the framework / node-target guards — and a lazy environment-filtered set recomputed insidemergeBindings.cli/deploy.tsthreads'prod'into all three call sites::2526,:5657,:5999.codegen/prepare.tsstays 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.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity