diff --git a/.changeset/fifty-flowers-mix.md b/.changeset/fifty-flowers-mix.md new file mode 100644 index 00000000..b94eac28 --- /dev/null +++ b/.changeset/fifty-flowers-mix.md @@ -0,0 +1,5 @@ +--- +"@t3-oss/env-core": minor +--- + +feat: add option `emptyStringAsUndefined` diff --git a/docs/src/app/docs/core/page.mdx b/docs/src/app/docs/core/page.mdx index c64c6ff4..46b7e04c 100644 --- a/docs/src/app/docs/core/page.mdx +++ b/docs/src/app/docs/core/page.mdx @@ -30,7 +30,7 @@ Then, you can create your schema like so: -The file below is named `env.ts`, but you can name it whatever you want. Some frameworks even generate a `env.d.ts` file that will collide with `env.ts` which means you'll have to name it something else. +The file below is named `env.ts`, but you can name it whatever you want. Some frameworks even generate a `env.d.ts` file that will collide with `env.ts`, which means you will have to name it something else. @@ -39,26 +39,46 @@ import { createEnv } from "@t3-oss/env-core"; import { z } from "zod"; export const env = createEnv({ - /** - * Specify what prefix the client-side variables must have. - * This is enforced both on type-level and at runtime. - */ - clientPrefix: "PUBLIC_", server: { DATABASE_URL: z.string().url(), OPEN_AI_API_KEY: z.string().min(1), }, + + /** + * The prefix that client-side variables must have. This is enforced both at + * a type-level and at runtime. + */ + clientPrefix: "PUBLIC_", + client: { PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1), }, + /** - * What object holds the environment variables at runtime. - * Often `process.env` or `import.meta.env` + * What object holds the environment variables at runtime. This is usually + * `process.env` or `import.meta.env`. */ runtimeEnv: process.env, + + /** + * By default, this library will feed the environment variables directly to + * the Zod validator. + * + * This means that if you have an empty string for a value that is supposed + * to be a number (e.g. `PORT=` in a ".env" file), Zod will incorrectly flag + * it as a type mismatch violation. Additionally, if you have an empty string + * for a value that is supposed to be a string with a default value (e.g. + * `DOMAIN=` in an ".env" file), the default value will never be applied. + * + * In order to solve these issues, we recommend that all new projects + * explicitly specify this option as true. + */ + emptyStringAsUndefined: true, }); ``` +Remove the `clientPrefix` and `client` properties if you only want the environment variables to exist on the server. + While defining both the client and server schemas in a single file provides the best developer experience, diff --git a/packages/core/index.ts b/packages/core/index.ts index c486e525..f54c3ecd 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -46,10 +46,12 @@ export interface BaseOptions> { export interface LooseOptions> extends BaseOptions { runtimeEnvStrict?: never; + /** - * Runtime Environment variables to use for validation - `process.env`, `import.meta.env` or similar. - * Unlike `runtimeEnvStrict`, this doesn't enforce that all environment variables are set. + * What object holds the environment variables at runtime. This is usually + * `process.env` or `import.meta.env`. */ + // Unlike `runtimeEnvStrict`, this doesn't enforce that all environment variables are set. runtimeEnv: Record; } @@ -87,13 +89,14 @@ export interface ClientOptions< TClient extends Record > { /** - * Client-side environment variables are exposed to the client by default. Set what prefix they have + * The prefix that client-side variables must have. This is enforced both at + * a type-level and at runtime. */ clientPrefix: TPrefix; /** * Specify your client-side environment variables schema here. This way you can ensure the app isn't - * built with invalid env vars. To expose them to the client, prefix them with `NEXT_PUBLIC_`. + * built with invalid env vars. */ client: Partial<{ [TKey in keyof TClient]: TKey extends `${TPrefix}${string}` @@ -121,6 +124,21 @@ export interface ServerOptions< : never} should not prefixed with ${TPrefix}.`> : TServer[TKey]; }>; + + /** + * By default, this library will feed the environment variables directly to + * the Zod validator. + * + * This means that if you have an empty string for a value that is supposed + * to be a number (e.g. `PORT=` in a ".env" file), Zod will incorrectly flag + * it as a type mismatch violation. Additionally, if you have an empty string + * for a value that is supposed to be a string with a default value (e.g. + * `DOMAIN=` in an ".env" file), the default value will never be applied. + * + * In order to solve these issues, we recommend that all new projects + * explicitly specify this option as true. + */ + emptyStringAsUndefined?: boolean; } export type ServerClientOptions< @@ -158,6 +176,15 @@ export function createEnv< > { const runtimeEnv = opts.runtimeEnvStrict ?? opts.runtimeEnv ?? process.env; + const emptyStringAsUndefined = opts.emptyStringAsUndefined ?? false; + if (emptyStringAsUndefined) { + for (const [key, value] of Object.entries(runtimeEnv)) { + if (value === "") { + delete runtimeEnv[key]; + } + } + } + const skip = !!opts.skipValidation; // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any if (skip) return runtimeEnv as any;