feat(envex): add Standard Schema validation for environment variables#133
feat(envex): add Standard Schema validation for environment variables#133daniel-rose wants to merge 8 commits into
Conversation
- Add `@standard-schema/spec` dependency for library-agnostic validation - Add `EnvexValidationError` with `issues` array for detailed error reporting - Add `validateEnv` utility that supports both sync and async schemas - Accept optional `schema` on `getEnv`, `getPublicEnv`, `createEnvRouteHandler`, `EnvScript`, `InlineEnvScript`, and `EnvexProvider` - Add generic overload to `useEnv<TSchema>()` for inferred output types - Validate `window.ENV` and fetched endpoint responses in `EnvexProvider` - Propagate validation errors via React error state for proper boundary handling - Ignore `coverage/` directory in eslint config - Update README with full validation docs including Zod examples
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds Standard Schema (StandardSchemaV1) validation across envex: new EnvexValidationError, validateEnv utility, schema-aware APIs (EnvexProvider, useEnv, getEnv, getPublicEnv, createEnvRouteHandler, EnvScript, InlineEnvScript), docs, tests, dependency, and a minor ESLint config tweak. ChangesSchema Validation Feature
Sequence DiagramsequenceDiagram
actor User
participant Provider as EnvexProvider (Client)
participant Hook as useEnv (Client)
participant Script as EnvScript / InlineEnvScript (Server)
participant Route as createEnvRouteHandler (Server)
participant Getter as getPublicEnv / getEnv (Server)
participant Validator as validateEnv (Utility)
participant Schema as StandardSchemaV1 (Library)
User->>Provider: Render with optional schema
activate Provider
alt server-side script/route
Script->>Getter: request public env with schema
Route->>Getter: request public env with schema
end
Getter->>Validator: pass filtered env + schema
activate Validator
Validator->>Schema: schema['~standard'].validate(env)
activate Schema
Schema-->>Validator: { value } or { issues }
deactivate Schema
alt validation passes
Validator-->>Getter: return typed value
Getter-->>Provider: deliver validated env
Provider->>Provider: set env state
else validation fails
Validator-->>Getter: throw EnvexValidationError
Getter-->>Provider: propagate error
Provider->>Provider: set error state -> throw during render
end
deactivate Validator
deactivate Provider
User->>Hook: call useEnv<TSchema>()
activate Hook
Hook-->>User: return context env (schema-typed if used)
deactivate Hook
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/envex/tests/react/EnvexProvider/index.spec.tsx (1)
11-123: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winPlease add one failing-schema provider test to lock in error propagation.
These additions cover only happy paths. Since schema validation errors are now part of the public runtime behavior, add at least one failing-schema case (window path or endpoint path) asserting the provider surfaces an
EnvexValidationErrorto the render/error boundary flow.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/envex/tests/react/EnvexProvider/index.spec.tsx` around lines 11 - 123, Add a negative test that causes the schema validator to fail and assert the provider surfaces EnvexValidationError; create a failing schema (e.g., makeFailSchema or reuse makePassSchema but return validate: () => ({ errors: [{ message: 'invalid' }] }) or throw) and use it with EnvexProvider on both window and endpoint paths: set window.ENV to an invalid shape and render <EnvexProvider schema={failingSchema}>Children</EnvexProvider> expecting the render to throw EnvexValidationError (toBeInstanceOf(EnvexValidationError)), and similarly mock fetch to return an invalid payload and render <EnvexProvider endpoint="/api/env" schema={failingSchema}> to assert the same error is surfaced; reference EnvexProvider and EnvexValidationError to locate where to add the tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/envex/README.md`:
- Around line 304-309: Update the docs for getPublicEnv to include the missing
option prefix?: string | null in the options table (alongside schema in the
getEnv / getPublicEnv section); document that getPublicEnv accepts prefix to
filter returned keys (null or undefined = no prefix filtering), explain its
default behavior and security impact (how keys are included/excluded), and
ensure the table row references the exact option name "prefix" and its type so
readers can find it when using the getPublicEnv API.
In `@packages/envex/src/react/EnvexProvider/index.tsx`:
- Around line 30-38: The catch handlers currently treat non-Error throws
inconsistently: in the endpoint branch the non-Error is logged to console.error,
but in the window.ENV branch it is swallowed; update both places (the promise
.catch block and the window.ENV handling block that uses setError and
isCancelled) to behave the same by logging the non-Error value with
console.error('[envex] Failed to fetch env from endpoint:' or a matching
'[envex] Failed to load env from window.ENV:') and then call setError(new
Error(String(err))) (while still guarding with isCancelled and preserving the
existing setError(err) behavior for actual Error instances) so non-Error throws
are both visible and converted to a consistent Error state.
---
Outside diff comments:
In `@packages/envex/tests/react/EnvexProvider/index.spec.tsx`:
- Around line 11-123: Add a negative test that causes the schema validator to
fail and assert the provider surfaces EnvexValidationError; create a failing
schema (e.g., makeFailSchema or reuse makePassSchema but return validate: () =>
({ errors: [{ message: 'invalid' }] }) or throw) and use it with EnvexProvider
on both window and endpoint paths: set window.ENV to an invalid shape and render
<EnvexProvider schema={failingSchema}>Children</EnvexProvider> expecting the
render to throw EnvexValidationError (toBeInstanceOf(EnvexValidationError)), and
similarly mock fetch to return an invalid payload and render <EnvexProvider
endpoint="/api/env" schema={failingSchema}> to assert the same error is
surfaced; reference EnvexProvider and EnvexValidationError to locate where to
add the tests.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 6d5a1df2-f527-4f97-a051-f504c1c0a30b
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!**/pnpm-lock.yaml
📒 Files selected for processing (18)
packages/envex/README.mdpackages/envex/eslint.config.jspackages/envex/package.jsonpackages/envex/src/errors.tspackages/envex/src/nextjs/EnvScript/index.tsxpackages/envex/src/nextjs/InlineEnvScript/index.tsxpackages/envex/src/nextjs/utils/createEnvRouteHandler/index.tspackages/envex/src/nextjs/utils/createEnvRouteHandler/types.tspackages/envex/src/nextjs/utils/getEnv/index.tspackages/envex/src/nextjs/utils/getPublicEnv/index.tspackages/envex/src/react/EnvexProvider/hooks/useEnv/index.tspackages/envex/src/react/EnvexProvider/index.tsxpackages/envex/src/react/EnvexProvider/types.tspackages/envex/src/types.tspackages/envex/src/utils/index.tspackages/envex/src/utils/validateEnv/index.tspackages/envex/tests/react/EnvexProvider/index.spec.tsxpackages/envex/tests/utils/validateEnv/index.spec.ts
…lure tests - Surface non-Error throws via console.error and setError in both the endpoint and window.ENV catch handlers so they are visible and converted to a consistent Error state - Add makeFailSchema helper and two negative tests that assert EnvexValidationError is surfaced on schema validation failure for both the window.ENV and endpoint paths - Document getPublicEnv prefix option in README
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/envex/src/react/EnvexProvider/index.tsx (1)
46-50:⚠️ Potential issue | 🟠 Major | ⚡ Quick winInconsistent error handling: synchronous throw inside useEffect.
Lines 46-50 throw
EnvexWindowEnvIsMissingErrorsynchronously insideuseEffect. This bypasses thesetErrorpattern used everywhere else in this component.React error boundaries don't catch errors thrown in effects—only errors during render. This means:
- Validation errors → caught by error boundaries ✓
- Missing
window.ENV→ uncaught in error boundaries ✗For consistent behavior, use
setErrorhere too:Proposed fix
if (!window.ENV || typeof window.ENV !== 'object') { - throw new EnvexWindowEnvIsMissingError( + setError(new EnvexWindowEnvIsMissingError( 'window.ENV is required. Use EnvScript (Next.js), set window.ENV manually via a <script> tag, or use the endpoint prop.' - ) + )) + return }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/envex/src/react/EnvexProvider/index.tsx` around lines 46 - 50, In EnvexProvider's useEffect, don't throw EnvexWindowEnvIsMissingError synchronously; instead create the EnvexWindowEnvIsMissingError with the same message and call setError(error) so the component follows the existing error state pattern (and return early from the effect afterward); update the check in the useEffect that currently references window.ENV to use setError(new EnvexWindowEnvIsMissingError(...)) instead of throw, keeping the same validation logic and message so error boundaries and the component's error rendering handle it consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/envex/CHANGELOG.md`:
- Line 5: Update the CHANGELOG.md entry on line documenting request
deduplication to instead describe the shipped Standard Schema validation
feature: mention support for Standard Schema validation, the new
EnvexValidationError class, and the validateEnv function; replace the existing
bullet text ("add request deduplication for EnvexProvider endpoint fetching...")
with a concise note like "add Standard Schema validation support and expose
EnvexValidationError and validateEnv" so the release note matches the actual PR
scope and shipped API.
In `@packages/envex/README.md`:
- Around line 219-231: The docs claim EnvexProvider validates "initialEnv" but
the implementation only validates window.ENV and fetched payloads; update
EnvexProvider so the initialEnv prop is passed through the same schema
validation path as other sources (i.e., call the existing validation function
used for window.ENV/fetch payloads) before or as part of filterPublicEnv; ensure
you reference and reuse the same validator (e.g., validateEnv/validateSchema)
and mirror the same error/logging/handling behavior used for validated sources
so invalid initialEnv is handled consistently.
In `@packages/envex/tests/react/EnvexProvider/index.spec.tsx`:
- Around line 137-148: The test "Schema validation failure on window.ENV
surfaces EnvexValidationError." uses a try/catch so it will silently succeed if
render() does not throw; change it to assert the promise rejects by using await
expect(render(<EnvexProvider
schema={makeFailSchema()}>Children</EnvexProvider>)).rejects.toBeInstanceOf(EnvexValidationError)
(or alternatively call fail() immediately after a successful render to ensure
the test fails), and apply the same fix to the other similar test block that
validates errors for window.ENV and makeFailSchema() to ensure assertions run
when no error is thrown.
---
Outside diff comments:
In `@packages/envex/src/react/EnvexProvider/index.tsx`:
- Around line 46-50: In EnvexProvider's useEffect, don't throw
EnvexWindowEnvIsMissingError synchronously; instead create the
EnvexWindowEnvIsMissingError with the same message and call setError(error) so
the component follows the existing error state pattern (and return early from
the effect afterward); update the check in the useEffect that currently
references window.ENV to use setError(new EnvexWindowEnvIsMissingError(...))
instead of throw, keeping the same validation logic and message so error
boundaries and the component's error rendering handle it consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3e8d22bf-db0b-44a3-a60d-06902f7cfcc4
📒 Files selected for processing (4)
packages/envex/CHANGELOG.mdpackages/envex/README.mdpackages/envex/src/react/EnvexProvider/index.tsxpackages/envex/tests/react/EnvexProvider/index.spec.tsx
- Fix README: validation does not run on initialEnv, only on window.ENV and fetched endpoint responses - Add expect.assertions(1) to failing-schema tests so they cannot pass silently when no error is thrown - Revert unintentional Prettier formatting change in CHANGELOG.md
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
… pattern Fold the window.ENV guard into the Promise chain so the error reaches setError via the .catch handler, matching the async pattern used for all other error paths in EnvexProvider and enabling consistent error boundary handling.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/envex/src/react/EnvexProvider/index.tsx`:
- Around line 13-15: The state initializer currently exposes raw initialEnv on
first render; modify the EnvexProvider useState initializer so it performs
synchronous schema validation/transformation (or throws) before setting state:
inside the useState(() => ...) initializer, if schema is provided run
schema.parse/validateSync (or equivalent) on initialEnv, then pass the
validated/parsed result through filterPublicEnv(prefix) and return that; if
validation fails allow the synchronous throw so children never see the raw
initialEnv. Refer to EnvexProvider, useState<Env>, initialEnv, schema and
filterPublicEnv when making the change.
- Around line 23-29: The fetch effect currently depends on both endpoint and
schema which causes refetches when schema identity changes; change the effect
using fetchEnv so its dependency array includes only endpoint (keep the
isCancelled check and setEnv(result)), and move the schema validation
(validateEnv(schema, data)) into a separate effect that watches schema and the
stored env value: i.e., fetchEnv(...) should return raw Env and set a rawEnv
state, then create a second effect that runs validateEnv when schema or rawEnv
changes and sets the final env via setEnv; update references to fetchEnv,
validateEnv, schema, endpoint, setEnv and any isCancelled cleanup accordingly
(also apply the same split at the other occurrence around line 72).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 837a691c-2126-4818-afd5-a939b38bb8b5
📒 Files selected for processing (1)
packages/envex/src/react/EnvexProvider/index.tsx
- Decouple fetch from schema validation: a dedicated fetch effect depends only on `endpoint` so schema identity changes no longer cause redundant re-fetches or re-reads of window.ENV - Introduce `rawEnv` intermediate state; a second effect validates it against `schema` and publishes the result, running only when raw data or schema actually changes - Skip `initialEnv` as initial state when `schema` is provided so children never observe unvalidated data before the validation effect runs
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/envex/src/react/EnvexProvider/index.tsx`:
- Around line 81-83: The fetch handling bypasses prefix filtering: when schema
is absent the code assigns rawEnv directly to env instead of running
filterPublicEnv(rawEnv, prefix), so ensure the Promise resolution path applies
filterPublicEnv when schema is falsy (use result => { const filtered = schema ?
result as Env : filterPublicEnv(result as RawEnv, prefix); if (!isCancelled)
setEnv(filtered); }) and also include prefix in the useEffect/useMemo dependency
array that triggers the fetch (so changes to prefix re-run the fetch logic).
Reference functions/values: filterPublicEnv, validateEnv, rawEnv, setEnv,
isCancelled and the prefix prop.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 4d84eae6-c7f4-484a-ae37-1f2bed884c53
📒 Files selected for processing (1)
packages/envex/src/react/EnvexProvider/index.tsx
… to deps When no schema is provided, filterPublicEnv is now applied to the raw env data in the validation effect so prefix filtering is consistently applied across all data sources (window.ENV and endpoint), not only initialEnv. Add prefix to the validation effect dependency array so a prefix change re-applies the filter without needing a re-fetch.
- Add zod as dependency for Standard Schema-compatible validation
- Define shared envSchema (NEXT_PUBLIC_FOO: z.string()) in src/app/env.ts
- Add /schema page: server component calling getPublicEnv({ schema })
and rendering the Zod-validated value
- Add /api/env-schema route: createEnvRouteHandler({ schema }) returning
only schema-typed public env keys
- Add schema.browser.spec.ts with two E2E tests covering both the API
route and the server-rendered page
Move inline interface/type definitions into dedicated types.ts files within each module directory for consistent separation of concerns. Consolidate the identical EnvScriptProps and InlineEnvScriptProps into a single shared definition in nextjs/types.ts, re-exported under their respective names.
Summary
EnvexValidationErrorwithissues: StandardSchemaV1.Issue[]for detailed error reportingschema?prop/option on all touch points:getEnv,getPublicEnv,createEnvRouteHandler,EnvScript,InlineEnvScript,EnvexProvideruseEnv<TSchema>()returningStandardSchemaV1.InferOutput<TSchema>for end-to-end type safetyBackward compatible — without
schema, behavior is identical to before.Test plan
validateEnvunit tests: sync schema pass/fail, async schema pass/fail, issue aggregation, value transformationEnvexProviderschema tests: validateswindow.ENV, validates fetched endpoint responsepnpm build)pnpm lint)Summary by CodeRabbit
New Features
Documentation
Tests
Chores