Repository navigation
fix(env): apply env.ts in TanStack Start, React Router and vinext Workers - #820
Draft
morinokami wants to merge 3 commits into
Draft
morinokami wants to merge 3 commits into
morinokami wants to merge 3 commits into
Conversation
…kers In a TanStack Start, React Router or vinext app, `env` from `void/env` ignored env.ts: a default read as undefined, a boolean stayed a string, and a missing required key went unnoticed. Only Void's own Worker entry loaded env.ts, so no schema was registered in a framework Worker. The Class A wrapper now imports env.ts ahead of the framework entry and awaits the new internal `_prepareEnv(env)` on every request, in dev and in builds. It validates every key, async validators included, and never rejects: a read of a key that failed throws that key's error, so a request that does not read it still runs, and so does a build-time prerender without production secrets. A read of a missing required key now throws rather than returning undefined. Unlike Void's own entry, an invalid value does not fail every request. SvelteKit, Nuxt, Analog and Astro Workers do not load env.ts yet. `_prepareEnv` and `_validateEnvOnce` share one validation, which now keeps a result for each key, and the results stay with the env object that was validated. A D1 app's framework reads outside a request scope, through the `cloudflare:workers` env, another object. Such a read gets the latest validation's result, failures included, while that env holds the raw value the result came from. A validation in which a validator threw serves its reads and runs again on the next call. `void deploy` bundles a framework app's cron and queue jobs apart from the app, with a copy of `void/env` of their own. They would see the schema that the app registers and fail to read async keys, so the jobs of an app with env.ts now prepare the env through the `_prepareEnv` that the registering copy publishes on the env registry. Results by env object move to that registry, so every copy reads them. The latest validation moves to a new registry field, `latestValidation`: its shape changed, and two Void versions in one process share the registry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PostgreSQL dev and build wrapper tests run the generated wrapper in Node. The wrapper imports env.ts, and the build wrapper also the routing runtime, by absolute path, which Vite resolves. Node reads a Windows path such as `C:/…` or `C:\…` as a URL with the scheme `c:` and rejects it with ERR_UNSUPPORTED_ESM_URL_SCHEME, so both tests would fail in the Windows unit shard. The runner now registers a resolve hook that gives Node a file URL for such a path, so the tests still run the wrapper as it is generated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test that the copy of `void/env` registering a schema publishes its `_prepareEnv` compared the registry's entry with that copy's export. Without `_prepareEnv`, both are undefined, so the test passed without the change it covers. It now also checks that the entry is a function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
In a TanStack Start, React Router or vinext app,
envfromvoid/envignoresenv.ts:With
DEBUG=falseand neitherSTRIPE_KEYnorPORTset, the route readsPORTasundefinedandDEBUGas the string"false", and the missingSTRIPE_KEYreads asundefinedrather than failing. This happens invp devand in a production build. An async validator never runs.Only the Worker entry of a Void app imports
env.ts. A framework's Worker never loads it, so no schema is registered there, andenvreturns raw values, whatever the Vite plugin loaded in its own process.Fix
The entry that Void generates around the framework's Worker entry (
.void/<framework>-trigger-wrapper.ts) now importsenv.tsahead of the framework entry, and awaits a new internal_prepareEnv(env)on every request, invp devand in builds._prepareEnvvalidates every key, async validators included, and keeps each key's result. It never rejects: a read of a key that failed throws that key's error, also for an async validator, so a request that doesn't read the key still runs, and so does a build-time prerender without production secrets._validateEnvOncesince fix(env): serve async env.ts values to Void app requests #792, it reuses a validation for the same raw values and keeps the results with the env object it receives. A request whose env holds the same raw values compares them and validates nothing.cloudflare:workersenv, which is another object. Such a read gets the latest validation's result while that env holds the raw value the result came from. The results are never copied to that object, which can come to hold other values.vp dev, the wrapper also prepares the env before it dispatches a cron or queue job.void deploybundles a framework app's cron and queue jobs apart from the app, with a copy ofvoid/envof their own. With this change, they would see the schema that the app registers and fail to read an async key. The jobs of an app withenv.tsnow prepare the env through the_prepareEnvthat the registering copy publishes on the env registry, so the publicVITE_*values embedded in the app are the ones validated. Results by env object move from the module to the registry, where every copy reads them.Unlike the entry of a Void app, an invalid value doesn't fail every request. That would also stop a build-time prerender, which runs without production secrets, and every request of an existing app with one invalid value. Checking the whole schema in production is left for a follow-up.
Only TanStack Start, React Router and vinext are covered here, since Void generates their Worker entry. SvelteKit, Nuxt, Analog and Astro build their Workers differently and are left for follow-ups.
Other behavior changes:
EnvValidationErrorinstead of returningundefined.env.tsthat throws when it loads stops the Worker from starting, since the Worker now loads it._validateEnvOnce, which Void apps use, now keeps a result for each key too. A read that doesn't go through its whole-schema check, such as one in a Durable Object handler, gets the key's own result from the latest validation, even when another key failed. It used to validate the key again, which fails for an async validator._validateEnvOncesince fix(env): serve async env.ts values to Void app requests #792. Until then, a read of that key throws what the validator threw._validateEnvOnceand_prepareEnv.validateAllEnv, which the dev server's warning and the deploy check use, still runs them in order.latestValidation. 1.8.0 stores another shape undervalidation, and two Void versions in one process share the registry.Docs
docs/integrations/frameworks/overview.mdlists environment variables among the features of TanStack Start, React Router and vinext apps.packages/void/AGENTS.mdrecords why the wrapper loadsenv.ts, and how reads find the results.Tests
packages/void/test/unit/env-schema.test.ts(10 cases):_prepareEnvserves defaults, converted values and async results, resolves with invalid keys and fails only their reads (what a validator throws included), redacts the raw value from an async validator's failure, validates once for the same raw values and again for others, runs a validator that threw again, and resolves without an env.void/env, one reads what the other prepared, the registering copy's_prepareEnvis published, and registering a schema again drops the results in every copy.packages/void/test/unit/plugin-class-a-wrapper.test.ts(4 cases):env.tsahead of the framework entry and calls_prepareEnv.packages/void/test/unit/wrapper.test.ts(2 cases): in workerd, the cron job and queue consumer thatvoid deploybundles for a D1 and a PostgreSQL app read a default and an async value that the app prepared. The jobs of an app withoutenv.tsstay as they were.env.tswith a default that nothing else supplies, and a route that returns it. Their dev integration suites read it, andtest/check-playground-build-artifacts.mjschecks that each server bundle containsenv.ts.All 16 unit cases and the 4 dev cases fail without this fix.
Verification
vp test run: 798 files pass, 1 skipped (16,879 tests pass, 30 skipped).vp run check: 34/34 pass.vp previewof the four playgrounds' production builds:/api/envanswers{"SCHEMA_DEFAULT":48213}.TOKENkey that falls back to a value, and a prerendered page that reads it andSCHEMA_DEFAULT: the build prerenders the page with both values. React Router'svp builddoesn't exit after prerendering, with or withoutenv.ts."sideEffects": false: the server bundle keepsenv.ts, and the prerendered page has both values.void deployagainst Cloudflare, a dev server with a PostgreSQL database (the unit tests run the PostgreSQL dev wrapper in Node), and Windows.🤖 Generated with Claude Code