Skip to content

Commit

Permalink
feat: add ability to extend presets (#170)
Browse files Browse the repository at this point in the history
  • Loading branch information
juliusmarminge committed Jan 24, 2024
1 parent 66f003c commit 9858b27
Show file tree
Hide file tree
Showing 17 changed files with 554 additions and 43 deletions.
9 changes: 9 additions & 0 deletions .changeset/cyan-years-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@t3-oss/env-nextjs": minor
"@t3-oss/env-core": minor
"@t3-oss/env-nuxt": minor
---

feat!: add ability to extend presets. Read more [in the docs](https://env.t3.gg/docs/customization#extending-presets).

**BREAKING CHANGE**: The required TypeScript version is now ^5.0.0
10 changes: 10 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ const config = {
},
],
},
overrides: [
{
files: ["**/*.test.ts"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
},
},
],
reportUnusedDisableDirectives: true,
ignorePatterns: [
"**/dist/**",
"**/node_modules/**",
Expand Down
Binary file modified bun.lockb
Binary file not shown.
46 changes: 46 additions & 0 deletions docs/src/app/docs/customization/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,49 @@ export const env = createEnv({
isServer: typeof window === "undefined",
});
```

## Extending presets

Your env object may extend other presets by using the `extends` property. This can be used to include system environment variables for your deployment provider, or if you have a monorepo with multiple packages that share some environment variables.

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

export const env = createEnv({
// ...
// Extend the Vercel preset.
extends: [vercel],
});

env.VERCEL_URL; // string
```

T3 Env ships the following presets out of the box, all importable from the `/presets` entrypoint.

- `vercel` - Vercel environment variables. See full list [here](https://vercel.com/docs/projects/environment-variables/system-environment-variables#system-environment-variables).

<Callout type="info">

Feel free to open a PR with more presets!

</Callout>

A preset is just like any other env object, so you can easily create your own:

```ts
// packages/auth/env.ts
import { createEnv } from "@t3-oss/env-core";
export const env = createEnv({
// ...
});

// apps/web/env.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { env as authEnv } from "@repo/auth/env";

export const env = createEnv({
// ...
extends: [authEnv],
});
```
2 changes: 2 additions & 0 deletions examples/nextjs/app/env.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { vercel } from "@t3-oss/env-nextjs/presets";
import { z } from "zod";

export const env = createEnv({
Expand All @@ -15,4 +16,5 @@ export const env = createEnv({
NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_GREETING: process.env.NEXT_PUBLIC_GREETING,
},
extends: [vercel],
});
23 changes: 19 additions & 4 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,24 @@
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./presets": {
"import": {
"types": "./dist/presets.d.ts",
"default": "./dist/presets.js"
},
"require": {
"types": "./dist/presets.d.cts",
"default": "./dist/presets.cjs"
}
},
"./package.json": "./package.json"
},
Expand All @@ -39,7 +54,7 @@
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"typescript": ">=4.7.2",
"typescript": ">=5.0.0",
"zod": "^3.0.0"
},
"peerDependenciesMeta": {
Expand Down
62 changes: 49 additions & 13 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,26 @@ type Impossible<T extends Record<string, any>> = Partial<
Record<keyof T, never>
>;

export interface BaseOptions<TShared extends Record<string, ZodType>> {
type UnReadonlyObject<T> = T extends Readonly<infer U> ? U : T;

type Reduce<
TArr extends Array<Record<string, unknown>>,
// eslint-disable-next-line @typescript-eslint/ban-types
TAcc = {}
> = TArr extends []
? TAcc
: TArr extends undefined
? TAcc
: TArr extends [infer Head, ...infer Tail]
? Tail extends Array<Record<string, unknown>>
? Head & Reduce<Tail, TAcc>
: never
: never;

export interface BaseOptions<
TShared extends Record<string, ZodType>,
TExtends extends Array<Record<string, unknown>>
> {
/**
* How to determine whether the app is running on the server or the client.
* @default typeof window === "undefined"
Expand All @@ -24,6 +43,11 @@ export interface BaseOptions<TShared extends Record<string, ZodType>> {
*/
shared?: TShared;

/**
* Extend presets
*/
extends?: TExtends;

/**
* Called when validation fails. By default the error is logged,
* and an error is thrown telling what environment variables are invalid.
Expand Down Expand Up @@ -58,8 +82,10 @@ export interface BaseOptions<TShared extends Record<string, ZodType>> {
emptyStringAsUndefined?: boolean;
}

export interface LooseOptions<TShared extends Record<string, ZodType>>
extends BaseOptions<TShared> {
export interface LooseOptions<
TShared extends Record<string, ZodType>,
TExtends extends Array<Record<string, unknown>>
> extends BaseOptions<TShared, TExtends> {
runtimeEnvStrict?: never;

/**
Expand All @@ -74,8 +100,9 @@ export interface StrictOptions<
TPrefix extends string | undefined,
TServer extends Record<string, ZodType>,
TClient extends Record<string, ZodType>,
TShared extends Record<string, ZodType>
> extends BaseOptions<TShared> {
TShared extends Record<string, ZodType>,
TExtends extends Array<Record<string, unknown>>
> extends BaseOptions<TShared, TExtends> {
/**
* Runtime Environment variables to use for validation - `process.env`, `import.meta.env` or similar.
* Enforces all environment variables to be set. Required in for example Next.js Edge and Client runtimes.
Expand Down Expand Up @@ -160,24 +187,28 @@ export type EnvOptions<
TPrefix extends string | undefined,
TServer extends Record<string, ZodType>,
TClient extends Record<string, ZodType>,
TShared extends Record<string, ZodType>
TShared extends Record<string, ZodType>,
TExtends extends Array<Record<string, unknown>>
> =
| (LooseOptions<TShared> & ServerClientOptions<TPrefix, TServer, TClient>)
| (StrictOptions<TPrefix, TServer, TClient, TShared> &
| (LooseOptions<TShared, TExtends> &
ServerClientOptions<TPrefix, TServer, TClient>)
| (StrictOptions<TPrefix, TServer, TClient, TShared, TExtends> &
ServerClientOptions<TPrefix, TServer, TClient>);

export function createEnv<
TPrefix extends string | undefined,
TServer extends Record<string, ZodType> = NonNullable<unknown>,
TClient extends Record<string, ZodType> = NonNullable<unknown>,
TShared extends Record<string, ZodType> = NonNullable<unknown>
TShared extends Record<string, ZodType> = NonNullable<unknown>,
const TExtends extends Array<Record<string, unknown>> = []
>(
opts: EnvOptions<TPrefix, TServer, TClient, TShared>
opts: EnvOptions<TPrefix, TServer, TClient, TShared, TExtends>
): Readonly<
Simplify<
z.infer<ZodObject<TServer>> &
z.infer<ZodObject<TClient>> &
z.infer<ZodObject<TShared>>
z.infer<ZodObject<TShared>> &
UnReadonlyObject<Reduce<TExtends>>
>
> {
const runtimeEnv = opts.runtimeEnvStrict ?? opts.runtimeEnv ?? process.env;
Expand Down Expand Up @@ -231,7 +262,12 @@ export function createEnv<
return onValidationError(parsed.error);
}

const env = new Proxy(parsed.data, {
const extendedObj = (opts.extends ?? []).reduce((acc, curr) => {
return Object.assign(acc, curr);
}, {});
const fullObj = Object.assign({}, parsed.data, extendedObj);

const env = new Proxy(fullObj, {
get(target, prop) {
if (
typeof prop !== "string" ||
Expand All @@ -247,7 +283,7 @@ export function createEnv<
) {
return onInvalidAccess(prop);
}
return target[prop as keyof typeof target];
return target[prop];
},
// Maybe reconsider this in the future:
// https://github.com/t3-oss/t3-env/pull/111#issuecomment-1682931526
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/presets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { z } from "zod";
import { createEnv } from ".";

/**
* Vercel System Environment Variables
* @see https://vercel.com/docs/projects/environment-variables/system-environment-variables#system-environment-variables
*/
export const vercel = createEnv({
server: {
VERCEL: z.string().optional(),
VERCEL_ENV: z.enum(["development", "preview", "production"]).optional(),
VERCEL_URL: z.string().optional(),
VERCEL_BRANCH_URL: z.string().optional(),
VERCEL_REGION: z.string().optional(),
VERCEL_AUTOMATION_BYPASS_SECRET: z.string().optional(),
VERCEL_GIT_PROVIDER: z.string().optional(),
VERCEL_GIT_REPO_SLUG: z.string().optional(),
VERCEL_GIT_REPO_OWNER: z.string().optional(),
VERCEL_GIT_REPO_ID: z.string().optional(),
VERCEL_GIT_COMMIT_REF: z.string().optional(),
VERCEL_GIT_COMMIT_SHA: z.string().optional(),
VERCEL_GIT_COMMIT_MESSAGE: z.string().optional(),
VERCEL_GIT_COMMIT_AUTHOR_LOGIN: z.string().optional(),
VERCEL_GIT_COMMIT_AUTHOR_NAME: z.string().optional(),
VERCEL_GIT_PREVIOUS_SHA: z.string().optional(),
VERCEL_GIT_PULL_REQUEST_ID: z.string().optional(),
},
runtimeEnv: process.env,
});

1 comment on commit 9858b27

@vercel
Copy link

@vercel vercel bot commented on 9858b27 Jan 24, 2024

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 – ./docs

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

Please sign in to comment.