diff --git a/.changeset/violet-dryers-cross.md b/.changeset/violet-dryers-cross.md new file mode 100644 index 00000000..7f323feb --- /dev/null +++ b/.changeset/violet-dryers-cross.md @@ -0,0 +1,14 @@ +--- +"@t3-oss/env-nextjs": minor +"@t3-oss/env-core": minor +"@t3-oss/env-nuxt": minor +--- + +feat!: added ability to customise schema combination + +Combination of schemas can now be customised using the `createFinalSchema` option. This allows further refinement or transformation of the environment variables. + +For 99% of users, this is a non-breaking change. If you were relying on internal types, there's a type-only breaking change: + +- `CreateEnv` now has the signature `CreateEnv`, instead of the previous `CreateEnv`. + - Previous behaviour can be achieved by using `DefaultCombinedSchema` as the type for `TFinalSchema`. diff --git a/docs/src/app/docs/customization/page.mdx b/docs/src/app/docs/customization/page.mdx index d37d53bb..941a5dc6 100644 --- a/docs/src/app/docs/customization/page.mdx +++ b/docs/src/app/docs/customization/page.mdx @@ -127,3 +127,36 @@ export const env = createEnv({ extends: [authEnv], }); ``` + +## Further refinement or transformation + +You can use the `createFinalSchema` option to further refine or transform the environment variables. + +```ts title="src/env.ts" +import { createEnv } from "@t3-oss/env-core"; +import { z } from "zod"; + +export const env = createEnv({ + server: { + SKIP_AUTH: z.boolean().optional(), + EMAIL: z.string().email().optional(), + PASSWORD: z.string().min(1).optional(), + }, + // ... + createFinalSchema: (shape, isServer) => + z.object(shape).transform((env, ctx) => { + if (env.SKIP_AUTH || !isServer) return { SKIP_AUTH: true } as const; + if (!env.EMAIL || !env.PASSWORD) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "EMAIL and PASSWORD are required if SKIP_AUTH is false", + }); + return z.NEVER; + } + return { + EMAIL: env.EMAIL, + PASSWORD: env.PASSWORD, + }; + }), +}); +``` diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3feafd5b..1962f147 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,5 @@ import type { StandardSchemaDictionary, StandardSchemaV1 } from "./standard"; -import { parseWithDictionary } from "./standard"; +import { ensureSynchronous, parseWithDictionary } from "./standard"; export type { StandardSchemaV1, StandardSchemaDictionary }; @@ -8,6 +8,13 @@ export type Simplify = { [P in keyof T]: T[P]; } & {}; +type PossiblyUndefinedKeys = { + [K in keyof T]: undefined extends T[K] ? K : never; +}[keyof T]; + +type UndefinedOptional = Partial>> & + Omit>; + // biome-ignore lint/suspicious/noExplicitAny: type Impossible> = Partial< Record @@ -27,7 +34,7 @@ type Reduce< : never; export interface BaseOptions< - TShared extends Record, + TShared extends StandardSchemaDictionary, TExtends extends Array>, > { /** @@ -82,7 +89,7 @@ export interface BaseOptions< } export interface LooseOptions< - TShared extends Record, + TShared extends StandardSchemaDictionary, TExtends extends Array>, > extends BaseOptions { runtimeEnvStrict?: never; @@ -97,9 +104,9 @@ export interface LooseOptions< export interface StrictOptions< TPrefix extends string | undefined, - TServer extends Record, - TClient extends Record, - TShared extends Record, + TServer extends StandardSchemaDictionary, + TClient extends StandardSchemaDictionary, + TShared extends StandardSchemaDictionary, TExtends extends Array>, > extends BaseOptions { /** @@ -131,7 +138,7 @@ export interface StrictOptions< export interface ClientOptions< TPrefix extends string | undefined, - TClient extends Record, + TClient extends StandardSchemaDictionary, > { /** * The prefix that client-side variables must have. This is enforced both at @@ -154,7 +161,7 @@ export interface ClientOptions< export interface ServerOptions< TPrefix extends string | undefined, - TServer extends Record, + TServer extends StandardSchemaDictionary, > { /** * Specify your server-side environment variables schema here. This way you can ensure the app isn't @@ -173,10 +180,26 @@ export interface ServerOptions< }>; } +export interface CreateSchemaOptions< + TServer extends StandardSchemaDictionary, + TClient extends StandardSchemaDictionary, + TShared extends StandardSchemaDictionary, + TFinalSchema extends StandardSchemaV1<{}, {}>, +> { + /** + * A custom function to combine the schemas. + * Can be used to add further refinement or transformation. + */ + createFinalSchema?: ( + shape: TServer & TClient & TShared, + isServer: boolean, + ) => TFinalSchema; +} + export type ServerClientOptions< TPrefix extends string | undefined, - TServer extends Record, - TClient extends Record, + TServer extends StandardSchemaDictionary, + TClient extends StandardSchemaDictionary, > = | (ClientOptions & ServerOptions) | (ServerOptions & Impossible>) @@ -184,38 +207,41 @@ export type ServerClientOptions< export type EnvOptions< TPrefix extends string | undefined, - TServer extends Record, - TClient extends Record, - TShared extends Record, + TServer extends StandardSchemaDictionary, + TClient extends StandardSchemaDictionary, + TShared extends StandardSchemaDictionary, TExtends extends Array>, -> = + TFinalSchema extends StandardSchemaV1<{}, {}>, +> = ( | (LooseOptions & ServerClientOptions) | (StrictOptions & - ServerClientOptions); + ServerClientOptions) +) & + CreateSchemaOptions; type TPrefixFormat = string | undefined; -type TServerFormat = Record; -type TClientFormat = Record; -type TSharedFormat = Record; +type TServerFormat = StandardSchemaDictionary; +type TClientFormat = StandardSchemaDictionary; +type TSharedFormat = StandardSchemaDictionary; type TExtendsFormat = Array>; -export type CreateEnv< +export type DefaultCombinedSchema< TServer extends TServerFormat, TClient extends TClientFormat, TShared extends TSharedFormat, +> = StandardSchemaV1< + {}, + UndefinedOptional< + StandardSchemaDictionary.InferOutput + > +>; + +export type CreateEnv< + TFinalSchema extends StandardSchemaV1<{}, {}>, TExtends extends TExtendsFormat, > = Readonly< - Simplify< - Reduce< - [ - StandardSchemaDictionary.InferOutput, - StandardSchemaDictionary.InferOutput, - StandardSchemaDictionary.InferOutput, - ...TExtends, - ] - > - > + Simplify, ...TExtends]>> >; export function createEnv< @@ -224,9 +250,14 @@ export function createEnv< TClient extends TClientFormat = NonNullable, TShared extends TSharedFormat = NonNullable, const TExtends extends TExtendsFormat = [], + TFinalSchema extends StandardSchemaV1<{}, {}> = DefaultCombinedSchema< + TServer, + TClient, + TShared + >, >( - opts: EnvOptions, -): CreateEnv { + opts: EnvOptions, +): CreateEnv { const runtimeEnv = opts.runtimeEnvStrict ?? opts.runtimeEnv ?? process.env; const emptyStringAsUndefined = opts.emptyStringAsUndefined ?? false; @@ -248,7 +279,7 @@ export function createEnv< const isServer = opts.isServer ?? (typeof window === "undefined" || "Deno" in window); - const finalSchema = isServer + const finalSchemaShape = isServer ? { ..._server, ..._shared, @@ -259,7 +290,13 @@ export function createEnv< ..._shared, }; - const parsed = parseWithDictionary(finalSchema, runtimeEnv); + const parsed = + opts + .createFinalSchema?.(finalSchemaShape as never, isServer) + ["~standard"].validate(runtimeEnv) ?? + parseWithDictionary(finalSchemaShape, runtimeEnv); + + ensureSynchronous(parsed, "Validation must be synchronous"); const onValidationError = opts.onValidationError ?? diff --git a/packages/core/src/presets-valibot.ts b/packages/core/src/presets-valibot.ts index 262327af..a099e85d 100644 --- a/packages/core/src/presets-valibot.ts +++ b/packages/core/src/presets-valibot.ts @@ -41,7 +41,7 @@ export const vercel = () => VERCEL_GIT_COMMIT_AUTHOR_NAME: optional(string()), VERCEL_GIT_PREVIOUS_SHA: optional(string()), VERCEL_GIT_PULL_REQUEST_ID: optional(string()), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -67,7 +67,7 @@ export const neonVercel = () => POSTGRES_DATABASE: optional(string()), POSTGRES_URL_NO_SSL: optional(pipe(string(), url())), POSTGRES_PRISMA_URL: optional(pipe(string(), url())), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -78,7 +78,7 @@ export const uploadthingV6 = () => createEnv({ server: { UPLOADTHING_TOKEN: string(), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -89,7 +89,7 @@ export const uploadthing = () => createEnv({ server: { UPLOADTHING_TOKEN: string(), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -114,7 +114,7 @@ export const render = () => picklist(["web", "pserv", "cron", "worker", "static"]), ), RENDER: optional(string()), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -148,7 +148,7 @@ export const railway = () => RAILWAY_GIT_REPO_NAME: optional(string()), RAILWAY_GIT_REPO_OWNER: optional(string()), RAILWAY_GIT_COMMIT_MESSAGE: optional(string()), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -170,7 +170,7 @@ export const fly = () => FLY_PROCESS_GROUP: optional(string()), FLY_VM_MEMORY_MB: optional(string()), PRIMARY_REGION: optional(string()), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -194,7 +194,7 @@ export const netlify = () => DEPLOY_ID: optional(string()), SITE_NAME: optional(string()), SITE_ID: optional(string()), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -207,6 +207,6 @@ export const upstashRedis = () => server: { UPSTASH_REDIS_REST_URL: pipe(string(), url()), UPSTASH_REDIS_REST_TOKEN: string(), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); diff --git a/packages/core/src/presets-zod.ts b/packages/core/src/presets-zod.ts index e8e1f4bf..7423d7e6 100644 --- a/packages/core/src/presets-zod.ts +++ b/packages/core/src/presets-zod.ts @@ -41,7 +41,7 @@ export const vercel = () => VERCEL_GIT_COMMIT_AUTHOR_NAME: z.string().optional(), VERCEL_GIT_PREVIOUS_SHA: z.string().optional(), VERCEL_GIT_PULL_REQUEST_ID: z.string().optional(), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -67,7 +67,7 @@ export const neonVercel = () => POSTGRES_DATABASE: z.string().optional(), POSTGRES_URL_NO_SSL: z.string().url().optional(), POSTGRES_PRISMA_URL: z.string().url().optional(), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -78,7 +78,7 @@ export const uploadthingV6 = () => createEnv({ server: { UPLOADTHING_TOKEN: z.string(), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -89,7 +89,7 @@ export const uploadthing = () => createEnv({ server: { UPLOADTHING_TOKEN: z.string(), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -114,7 +114,7 @@ export const render = () => .enum(["web", "pserv", "cron", "worker", "static"]) .optional(), RENDER: z.string().optional(), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -148,7 +148,7 @@ export const railway = () => RAILWAY_GIT_REPO_NAME: z.string().optional(), RAILWAY_GIT_REPO_OWNER: z.string().optional(), RAILWAY_GIT_COMMIT_MESSAGE: z.string().optional(), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -170,7 +170,7 @@ export const fly = () => FLY_PROCESS_GROUP: z.string().optional(), FLY_VM_MEMORY_MB: z.string().optional(), PRIMARY_REGION: z.string().optional(), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -194,7 +194,7 @@ export const netlify = () => DEPLOY_ID: z.string().optional(), SITE_NAME: z.string().optional(), SITE_ID: z.string().optional(), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); @@ -207,6 +207,6 @@ export const upstashRedis = () => server: { UPSTASH_REDIS_REST_URL: z.string().url(), UPSTASH_REDIS_REST_TOKEN: z.string(), - } satisfies StandardSchemaDictionary.Matching, + } satisfies StandardSchemaDictionary, runtimeEnv: process.env, }); diff --git a/packages/core/src/standard.ts b/packages/core/src/standard.ts index 9cccb66b..329f45c3 100644 --- a/packages/core/src/standard.ts +++ b/packages/core/src/standard.ts @@ -69,17 +69,14 @@ export declare namespace StandardSchemaV1 { >["output"]; } -export type StandardSchemaDictionary = Record; +export type StandardSchemaDictionary< + Input = Record, + Output extends Record = Input, +> = { + [K in keyof Input]-?: StandardSchemaV1; +}; + export namespace StandardSchemaDictionary { - /** - * A dictionary of Standard Schemas that match the input and output types. - */ - export type Matching< - Input, - Output extends Record = Input, - > = { - [K in keyof Input]-?: StandardSchemaV1; - }; export type InferInput = { [K in keyof T]: StandardSchemaV1.InferInput; }; @@ -88,6 +85,15 @@ export namespace StandardSchemaDictionary { }; } +export function ensureSynchronous( + value: T | Promise, + message: string, +): asserts value is T { + if (value instanceof Promise) { + throw new Error(message); + } +} + export function parseWithDictionary( dictionary: TDict, value: Record, @@ -95,14 +101,13 @@ export function parseWithDictionary( const result: Record = {}; const issues: StandardSchemaV1.Issue[] = []; for (const key in dictionary) { - const schema = dictionary[key]; - const prop = value[key]; - const propResult = schema["~standard"].validate(prop); - if (propResult instanceof Promise) { - throw new Error( - `Validation must be synchronous, but ${key} returned a Promise.`, - ); - } + const propResult = dictionary[key]["~standard"].validate(value[key]); + + ensureSynchronous( + propResult, + `Validation must be synchronous, but ${key} returned a Promise.`, + ); + if (propResult.issues) { issues.push( ...propResult.issues.map((issue) => ({ diff --git a/packages/core/test/smoke-valibot.test.ts b/packages/core/test/smoke-valibot.test.ts index 245f6337..acd460fc 100644 --- a/packages/core/test/smoke-valibot.test.ts +++ b/packages/core/test/smoke-valibot.test.ts @@ -630,6 +630,117 @@ describe("extending presets", () => { }); }); +describe("createFinalSchema", () => { + test("custom schema combiner", () => { + let receivedIsServer = false; + const env = createEnv({ + server: { + SERVER_ENV: v.string(), + }, + shared: { + SHARED_ENV: v.string(), + }, + clientPrefix: "CLIENT_", + client: { + CLIENT_ENV: v.string(), + }, + runtimeEnv: { + SERVER_ENV: "server", + SHARED_ENV: "shared", + CLIENT_ENV: "client", + }, + createFinalSchema: (shape, isServer) => { + expectTypeOf(isServer).toEqualTypeOf(); + if (typeof isServer === "boolean") receivedIsServer = true; + return v.object(shape); + }, + }); + expectTypeOf(env).toEqualTypeOf< + Readonly<{ + SERVER_ENV: string; + SHARED_ENV: string; + CLIENT_ENV: string; + }> + >(); + expect(env).toMatchObject({ + SERVER_ENV: "server", + SHARED_ENV: "shared", + CLIENT_ENV: "client", + }); + expect(receivedIsServer).toBe(true); + }); + test("schema combiner with further refinement", () => { + const env = createEnv({ + server: { + SKIP_AUTH: v.optional(v.boolean()), + EMAIL: v.optional(v.pipe(v.string(), v.email())), + PASSWORD: v.optional(v.pipe(v.string(), v.minLength(1))), + }, + runtimeEnv: { + SKIP_AUTH: true, + }, + createFinalSchema: (shape) => + v.pipe( + v.object(shape), + v.check((env) => env.SKIP_AUTH || !!(env.EMAIL && env.PASSWORD)), + ), + }); + expectTypeOf(env).toEqualTypeOf< + Readonly<{ + SKIP_AUTH?: boolean; + EMAIL?: string; + PASSWORD?: string; + }> + >(); + expect(env).toMatchObject({ SKIP_AUTH: true }); + }); + test("schema combiner that changes the type", () => { + const env = createEnv({ + server: { + SKIP_AUTH: v.optional(v.boolean()), + EMAIL: v.optional(v.pipe(v.string(), v.email())), + PASSWORD: v.optional(v.pipe(v.string(), v.minLength(1))), + }, + runtimeEnv: { + SKIP_AUTH: true, + }, + createFinalSchema: (shape) => + v.pipe( + v.object(shape), + v.rawTransform(({ addIssue, dataset, NEVER }) => { + const env = dataset.value; + if (env.SKIP_AUTH) return { SKIP_AUTH: true } as const; + if (!env.EMAIL || !env.PASSWORD) { + addIssue({ + message: + "EMAIL and PASSWORD are required if SKIP_AUTH is false", + }); + return NEVER; + } + return { + EMAIL: env.EMAIL, + PASSWORD: env.PASSWORD, + }; + }), + ), + }); + expectTypeOf(env).toEqualTypeOf< + Readonly< + | { + readonly SKIP_AUTH: true; + EMAIL?: undefined; + PASSWORD?: undefined; + } + | { + readonly SKIP_AUTH?: undefined; + EMAIL: string; + PASSWORD: string; + } + > + >(); + expect(env).toMatchObject({ SKIP_AUTH: true }); + }); +}); test("empty 'extends' array should not cause type errors", () => { const env = createEnv({ clientPrefix: "FOO_", diff --git a/packages/core/test/smoke-zod.test.ts b/packages/core/test/smoke-zod.test.ts index 7853abf3..2d8c2b89 100644 --- a/packages/core/test/smoke-zod.test.ts +++ b/packages/core/test/smoke-zod.test.ts @@ -628,6 +628,120 @@ describe("extending presets", () => { }); }); +describe("createFinalSchema", () => { + test("custom schema combiner", () => { + let receivedIsServer = false; + const env = createEnv({ + server: { + SERVER_ENV: z.string(), + }, + shared: { + SHARED_ENV: z.string(), + }, + clientPrefix: "CLIENT_", + client: { + CLIENT_ENV: z.string(), + }, + runtimeEnv: { + SERVER_ENV: "server", + SHARED_ENV: "shared", + CLIENT_ENV: "client", + }, + createFinalSchema: (shape, isServer) => { + expectTypeOf(isServer).toEqualTypeOf(); + if (typeof isServer === "boolean") receivedIsServer = true; + return z.object(shape); + }, + }); + + expectTypeOf(env).toEqualTypeOf< + Readonly<{ + SERVER_ENV: string; + SHARED_ENV: string; + CLIENT_ENV: string; + }> + >(); + + expect(env).toMatchObject({ + SERVER_ENV: "server", + SHARED_ENV: "shared", + CLIENT_ENV: "client", + }); + + expect(receivedIsServer).toBe(true); + }); + test("schema combiner with further refinement", () => { + const env = createEnv({ + server: { + SKIP_AUTH: z.boolean().optional(), + EMAIL: z.string().email().optional(), + PASSWORD: z.string().min(1).optional(), + }, + runtimeEnv: { + SKIP_AUTH: true, + }, + createFinalSchema: (shape) => + z.object(shape).refine((env) => { + expectTypeOf(env).toEqualTypeOf<{ + SKIP_AUTH?: boolean; + EMAIL?: string; + PASSWORD?: string; + }>(); + return env.SKIP_AUTH || (env.EMAIL && env.PASSWORD); + }), + }); + expectTypeOf(env).toEqualTypeOf< + Readonly<{ + SKIP_AUTH?: boolean; + EMAIL?: string; + PASSWORD?: string; + }> + >(); + expect(env).toMatchObject({ SKIP_AUTH: true }); + }); + test("schema combiner that changes the type", () => { + const env = createEnv({ + server: { + SKIP_AUTH: z.boolean().optional(), + EMAIL: z.string().email().optional(), + PASSWORD: z.string().min(1).optional(), + }, + createFinalSchema: (shape) => + z.object(shape).transform((env, ctx) => { + if (env.SKIP_AUTH) return { SKIP_AUTH: true } as const; + if (!env.EMAIL || !env.PASSWORD) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "EMAIL and PASSWORD are required if SKIP_AUTH is false", + }); + return z.NEVER; + } + return { + EMAIL: env.EMAIL, + PASSWORD: env.PASSWORD, + }; + }), + runtimeEnv: { + SKIP_AUTH: true, + }, + }); + expectTypeOf(env).toEqualTypeOf< + Readonly< + | { + readonly SKIP_AUTH: true; + EMAIL?: undefined; + PASSWORD?: undefined; + } + | { + readonly SKIP_AUTH?: undefined; + EMAIL: string; + PASSWORD: string; + } + > + >(); + expect(env).toMatchObject({ SKIP_AUTH: true }); + }); +}); test("empty 'extends' array should not cause type errors", () => { const env = createEnv({ clientPrefix: "FOO_", diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 58d6c96c..fc3f3a31 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -1,6 +1,9 @@ import type { CreateEnv, + CreateSchemaOptions, + DefaultCombinedSchema, ServerClientOptions, + StandardSchemaDictionary, StandardSchemaV1, StrictOptions, } from "@t3-oss/env-core"; @@ -10,13 +13,15 @@ const CLIENT_PREFIX = "NEXT_PUBLIC_" as const; type ClientPrefix = typeof CLIENT_PREFIX; type Options< - TServer extends Record, + TServer extends StandardSchemaDictionary, TClient extends Record<`${ClientPrefix}${string}`, StandardSchemaV1>, - TShared extends Record, + TShared extends StandardSchemaDictionary, TExtends extends Array>, + TFinalSchema extends StandardSchemaV1<{}, {}>, > = Omit< StrictOptions & - ServerClientOptions, + ServerClientOptions & + CreateSchemaOptions, "runtimeEnvStrict" | "runtimeEnv" | "clientPrefix" > & ( @@ -54,16 +59,21 @@ type Options< ); export function createEnv< - TServer extends Record = NonNullable, + TServer extends StandardSchemaDictionary = NonNullable, TClient extends Record< `${ClientPrefix}${string}`, StandardSchemaV1 > = NonNullable, - TShared extends Record = NonNullable, + TShared extends StandardSchemaDictionary = NonNullable, const TExtends extends Array> = [], + TFinalSchema extends StandardSchemaV1<{}, {}> = DefaultCombinedSchema< + TServer, + TClient, + TShared + >, >( - opts: Options, -): CreateEnv { + opts: Options, +): CreateEnv { const client = typeof opts.client === "object" ? opts.client : {}; const server = typeof opts.server === "object" ? opts.server : {}; const shared = opts.shared; @@ -75,7 +85,14 @@ export function createEnv< ...opts.experimental__runtimeEnv, }; - return createEnvCore({ + return createEnvCore< + ClientPrefix, + TServer, + TClient, + TShared, + TExtends, + TFinalSchema + >({ ...opts, shared, client, diff --git a/packages/nuxt/src/index.ts b/packages/nuxt/src/index.ts index 0cd2b43a..e37d5b3f 100644 --- a/packages/nuxt/src/index.ts +++ b/packages/nuxt/src/index.ts @@ -1,6 +1,9 @@ import type { CreateEnv, + CreateSchemaOptions, + DefaultCombinedSchema, ServerClientOptions, + StandardSchemaDictionary, StandardSchemaV1, StrictOptions, } from "@t3-oss/env-core"; @@ -10,32 +13,46 @@ const CLIENT_PREFIX = "NUXT_PUBLIC_" as const; type ClientPrefix = typeof CLIENT_PREFIX; type Options< - TServer extends Record, + TServer extends StandardSchemaDictionary, TClient extends Record<`${ClientPrefix}${string}`, StandardSchemaV1>, - TShared extends Record, + TShared extends StandardSchemaDictionary, TExtends extends Array>, + TFinalSchema extends StandardSchemaV1<{}, {}>, > = Omit< StrictOptions & - ServerClientOptions, + ServerClientOptions & + CreateSchemaOptions, "runtimeEnvStrict" | "runtimeEnv" | "clientPrefix" >; export function createEnv< - TServer extends Record = NonNullable, + TServer extends StandardSchemaDictionary = NonNullable, TClient extends Record< `${ClientPrefix}${string}`, StandardSchemaV1 > = NonNullable, - TShared extends Record = NonNullable, + TShared extends StandardSchemaDictionary = NonNullable, const TExtends extends Array> = [], + TFinalSchema extends StandardSchemaV1<{}, {}> = DefaultCombinedSchema< + TServer, + TClient, + TShared + >, >( - opts: Options, -): CreateEnv { + opts: Options, +): CreateEnv { const client = typeof opts.client === "object" ? opts.client : {}; const server = typeof opts.server === "object" ? opts.server : {}; const shared = opts.shared; - return createEnvCore({ + return createEnvCore< + ClientPrefix, + TServer, + TClient, + TShared, + TExtends, + TFinalSchema + >({ ...opts, shared, client,