Skip to content

Commit

Permalink
feat: add callback functions to override default behavior (#3)
Browse files Browse the repository at this point in the history
* add callback functions to override default behavior

* changeset

* refactor
  • Loading branch information
juliusmarminge committed Apr 25, 2023
1 parent 3a197f0 commit 54cd334
Show file tree
Hide file tree
Showing 7 changed files with 64 additions and 22 deletions.
6 changes: 6 additions & 0 deletions .changeset/late-rockets-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@t3-oss/env-nextjs": minor
"@t3-oss/env-core": minor
---

adds callback functions that allows overriding the default behavior when validation fails or variables are illegally accessed
4 changes: 1 addition & 3 deletions docs/src/app/docs/customization/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,13 @@ export const env = createEnv({

## Overriding the default error handler

<Callout type="info">This option is not released just yet.</Callout>

```ts title="src/env.ts"
import { createEnv } from "@t3-oss/env-core";

export const env = createEnv({
// ...
// Called when the schema validation fails.
onValidationFail: (error: ZodError) => {
onValidationError: (error: ZodError) => {
console.error(
"❌ Invalid environment variables:",
parsed.error.flatten().fieldErrors
Expand Down
1 change: 0 additions & 1 deletion examples/astro/src/env.d.ts

This file was deleted.

2 changes: 1 addition & 1 deletion examples/nextjs/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// import { env as _env } from "./app/env.mjs";
import { env as _ } from "./app/env.mjs";

/** @type {import('next').NextConfig} */
const nextConfig = {
Expand Down
2 changes: 1 addition & 1 deletion examples/nextjs/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@
"~/*": ["./app/*"]
}
},
"include": ["next-env.d.ts", "app", ".next/types/**/*.ts"],
"include": ["next-env.d.ts", "app", ".next/types/**/*.ts", "next.config.mjs"],
"exclude": ["node_modules"]
}
56 changes: 41 additions & 15 deletions packages/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import z, { ZodObject, ZodType } from "zod";
import z, { type ZodError, type ZodObject, type ZodType } from "zod";

export type ErrorMessage<T extends string> = T;

Expand Down Expand Up @@ -36,6 +36,18 @@ export interface BaseOptions<
*/
isServer?: boolean;

/**
* Called when validation fails. By default the error is logged,
* and an error is thrown telling what environment variables are invalid.
*/
onValidationError?: (error: ZodError) => never;

/**
* Called when a server-side environment variable is accessed on the client.
* By default an error is thrown.
*/
onInvalidAccess?: (variable: string) => never;

/**
* Whether to skip validation of environment variables.
* @default !!process.env.SKIP_ENV_VALIDATION && process.env.SKIP_ENV_VALIDATION !== "false" && process.env.SKIP_ENV_VALIDATION !== "0"
Expand Down Expand Up @@ -86,40 +98,54 @@ export function createEnv<
| LooseOptions<TPrefix, TServer, TClient>
| StrictOptions<TPrefix, TServer, TClient>
): z.infer<ZodObject<TServer>> & z.infer<ZodObject<TClient>> {
const _client = typeof opts.client === "object" ? opts.client : {};
const client = z.object(_client);
const server = z.object(opts.server);
const runtimeEnv = opts.runtimeEnvStrict ?? opts.runtimeEnv ?? process.env;
const isServer = opts.isServer ?? typeof window === "undefined";

const skip =
opts.skipValidation ??
(!!process.env.SKIP_ENV_VALIDATION &&
process.env.SKIP_ENV_VALIDATION !== "false" &&
process.env.SKIP_ENV_VALIDATION !== "0");

// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
if (skip) return runtimeEnv as any;

const _client = typeof opts.client === "object" ? opts.client : {};
const client = z.object(_client);
const server = z.object(opts.server);
const isServer = opts.isServer ?? typeof window === "undefined";

const merged = server.merge(client);
const parsed = isServer
? merged.safeParse(runtimeEnv) // on server we can validate all env vars
: client.safeParse(runtimeEnv); // on client we can only validate the ones that are exposed

const onValidationError =
opts.onValidationError ??
((error: ZodError) => {
console.error(
"❌ Invalid environment variables:",
error.flatten().fieldErrors
);
throw new Error("Invalid environment variables");
});

const onInvalidAccess =
opts.onInvalidAccess ??
((_variable: string) => {
throw new Error(
"❌ Attempted to access a server-side environment variable on the client"
);
});

if (parsed.success === false) {
console.error(
"❌ Invalid environment variables:",
parsed.error.flatten().fieldErrors
);
throw new Error("Invalid environment variables");
return onValidationError(parsed.error);
}

const env = new Proxy(parsed.data, {
get(target, prop) {
if (typeof prop !== "string") return undefined;
if (!isServer && !prop.startsWith(opts.clientPrefix))
throw new Error(
"❌ Attempted to access a server-side environment variable on the client"
);
if (!isServer && !prop.startsWith(opts.clientPrefix)) {
return onInvalidAccess(prop);
}
return target[prop as keyof typeof target];
},
});
Expand Down
15 changes: 14 additions & 1 deletion packages/nextjs/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ZodType } from "zod";
import type { z, ZodType } from "zod";
import { createEnv as createEnvCore, type ErrorMessage } from "../core";

export function createEnv<
Expand Down Expand Up @@ -42,6 +42,19 @@ export function createEnv<
* @default !!process.env.SKIP_ENV_VALIDATION && process.env.SKIP_ENV_VALIDATION !== "false" && process.env.SKIP_ENV_VALIDATION !== "0"
*/
skipValidation?: boolean;

/**
* Called when validation fails. By default the error is logged,
* and an error is thrown telling what environment variables are invalid.
* Function must throw an error at the end.
*/
onValidationError?: (error: z.ZodError) => never;

/**
* Called when a server-side environment variable is accessed on the client.
* By default an error is thrown.
*/
onInvalidAccess?: (variable: string) => never;
}) {
return createEnvCore({
...opts,
Expand Down

1 comment on commit 54cd334

@vercel
Copy link

@vercel vercel bot commented on 54cd334 Apr 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

t3-env – ./

t3-env.vercel.app
t3-env-git-main-t3-oss.vercel.app
env.t3.gg
t3-env-t3-oss.vercel.app

Please sign in to comment.