Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: directly pass zod objects to server and client #272

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 104 additions & 54 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { TypeOf, ZodError, ZodObject, ZodType } from "zod";
import type {
TypeOf,
UnknownKeysParam,
ZodError,
ZodObject,
ZodRawShape,
ZodTypeAny,
} from "zod";
import { object } from "zod";

export type ErrorMessage<T extends string> = T;
Expand All @@ -25,7 +32,7 @@ type Reduce<
: never;

export interface BaseOptions<
TShared extends Record<string, ZodType>,
TShared extends SchemaObject,
TExtends extends Array<Record<string, unknown>>,
> {
/**
Expand Down Expand Up @@ -80,7 +87,7 @@ export interface BaseOptions<
}

export interface LooseOptions<
TShared extends Record<string, ZodType>,
TShared extends SchemaObject,
TExtends extends Array<Record<string, unknown>>,
> extends BaseOptions<TShared, TExtends> {
runtimeEnvStrict?: never;
Expand All @@ -95,9 +102,9 @@ export interface LooseOptions<

export interface StrictOptions<
TPrefix extends string | undefined,
TServer extends Record<string, ZodType>,
TClient extends Record<string, ZodType>,
TShared extends Record<string, ZodType>,
TServer extends SchemaObject,
TClient extends SchemaObject,
TShared extends SchemaObject,
TExtends extends Array<Record<string, unknown>>,
> extends BaseOptions<TShared, TExtends> {
/**
Expand All @@ -106,30 +113,32 @@ export interface StrictOptions<
*/
runtimeEnvStrict: Record<
| {
[TKey in keyof TClient]: TPrefix extends undefined
[TKey in keyof SchemaShape<TClient>]: TPrefix extends undefined
? never
: TKey extends `${TPrefix}${string}`
? TKey
: never;
}[keyof TClient]
}[keyof SchemaShape<TClient>]
| {
[TKey in keyof TServer]: TPrefix extends undefined
[TKey in keyof SchemaShape<TServer>]: TPrefix extends undefined
? TKey
: TKey extends `${TPrefix}${string}`
? never
: TKey;
}[keyof TServer]
}[keyof SchemaShape<TServer>]
| {
[TKey in keyof TShared]: TKey extends string ? TKey : never;
}[keyof TShared],
[TKey in keyof SchemaShape<TShared>]: TKey extends string
? TKey
: never;
}[keyof SchemaShape<TShared>],
string | boolean | number | undefined
>;
runtimeEnv?: never;
}

export interface ClientOptions<
TPrefix extends string | undefined,
TClient extends Record<string, ZodType>,
TClientSchema extends SchemaObject,
> {
/**
* The prefix that client-side variables must have. This is enforced both at
Expand All @@ -141,61 +150,105 @@ export interface ClientOptions<
* Specify your client-side environment variables schema here. This way you can ensure the app isn't
* built with invalid env vars.
*/
client: Partial<{
[TKey in keyof TClient]: TKey extends `${TPrefix}${string}`
? TClient[TKey]
: ErrorMessage<`${TKey extends string
? TKey
: never} is not prefixed with ${TPrefix}.`>;
}>;
client: TClientSchema extends ZodObject<
infer R1,
infer R2,
infer R3,
infer R4,
unknown
>
? TClientSchema extends never
? TClientSchema
: ZodObject<
R1,
R2,
R3,
R4,
{
[TKey in keyof TClientSchema["_input"]]: TKey extends `${TPrefix}${string}`
? TClientSchema[TKey]
: ErrorMessage<`${TKey extends string
? TKey
: never} is not prefixed with ${TPrefix}.`>;
}
>
: never;
}

export type SchemaShape<T extends SchemaObject> = T["_input"]; // extends Schema<any, any, infer Shape> ? Shape : never

export type SchemaObject = ZodObject<
ZodRawShape,
UnknownKeysParam,
ZodTypeAny,
{},
{}
>;

export interface ServerOptions<
TPrefix extends string | undefined,
TServer extends Record<string, ZodType>,
TServerSchema extends SchemaObject,
> {
/**
* Specify your server-side environment variables schema here. This way you can ensure the app isn't
* built with invalid env vars.
*/
server: Partial<{
[TKey in keyof TServer]: TPrefix extends undefined
? TServer[TKey]
: TPrefix extends ""
? TServer[TKey]
: TKey extends `${TPrefix}${string}`
? ErrorMessage<`${TKey extends `${TPrefix}${string}`
? TKey
: never} should not prefixed with ${TPrefix}.`>
: TServer[TKey];
}>;
server: TServerSchema extends ZodObject<
infer R1,
infer R2,
infer R3,
infer R4,
unknown
>
? TServerSchema extends never
? TServerSchema
: TPrefix extends undefined
? TServerSchema
: TPrefix extends ""
? TServerSchema
: ZodObject<
R1,
R2,
R3,
R4,
{
[TKey in keyof TServerSchema["_input"]]: TKey extends `${TPrefix}${string}`
? ErrorMessage<`${TKey extends `${TPrefix}${string}`
? TKey
: never} should not prefixed with ${TPrefix}.`>
: TServerSchema["_input"][TKey];
}
>
: never;
}

export type ServerClientOptions<
TPrefix extends string | undefined,
TServer extends Record<string, ZodType>,
TClient extends Record<string, ZodType>,
TServer extends SchemaObject,
TClient extends SchemaObject,
> =
| (ClientOptions<TPrefix, TClient> & ServerOptions<TPrefix, TServer>)
| (ServerOptions<TPrefix, TServer> & Impossible<ClientOptions<never, never>>)
| (ClientOptions<TPrefix, TClient> & Impossible<ServerOptions<never, never>>);

export type EnvOptions<
TPrefix extends string | undefined,
TServer extends Record<string, ZodType>,
TClient extends Record<string, ZodType>,
TShared extends Record<string, ZodType>,
TServer extends SchemaObject,
TClient extends SchemaObject,
TShared extends SchemaObject,
TExtends extends Array<Record<string, unknown>>,
> =
| (LooseOptions<TShared, TExtends> &
ServerClientOptions<TPrefix, TServer, TClient>)
| (StrictOptions<TPrefix, TServer, TClient, TShared, TExtends> &
ServerClientOptions<TPrefix, TServer, TClient>);
| ((StrictOptions<TPrefix, TServer, TClient, TShared, TExtends> &
ServerClientOptions<TPrefix, TServer, TClient>) & {
a?: ServerClientOptions<TPrefix, TServer, TClient>["client"];
});

type TPrefixFormat = string | undefined;
type TServerFormat = Record<string, ZodType>;
type TClientFormat = Record<string, ZodType>;
type TSharedFormat = Record<string, ZodType>;
type TServerFormat = SchemaObject;
type TClientFormat = SchemaObject;
type TSharedFormat = SchemaObject;
type TExtendsFormat = Array<Record<string, unknown>>;

export type CreateEnv<
Expand All @@ -205,18 +258,18 @@ export type CreateEnv<
TExtends extends TExtendsFormat,
> = Readonly<
Simplify<
TypeOf<ZodObject<TServer>> &
TypeOf<ZodObject<TClient>> &
TypeOf<ZodObject<TShared>> &
TypeOf<TServer> &
TypeOf<TClient> &
TypeOf<TShared> &
UnReadonlyObject<Reduce<TExtends>>
>
>;

export function createEnv<
TPrefix extends TPrefixFormat,
TServer extends TServerFormat = NonNullable<unknown>,
TClient extends TClientFormat = NonNullable<unknown>,
TShared extends TSharedFormat = NonNullable<unknown>,
TServer extends TServerFormat = SchemaObject,
TClient extends TClientFormat = SchemaObject,
TShared extends TSharedFormat = SchemaObject,
const TExtends extends TExtendsFormat = [],
>(
opts: EnvOptions<TPrefix, TServer, TClient, TShared, TExtends>,
Expand All @@ -236,12 +289,9 @@ export function createEnv<
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
if (skip) return runtimeEnv as any;

const _client = typeof opts.client === "object" ? opts.client : {};
const _server = typeof opts.server === "object" ? opts.server : {};
const _shared = typeof opts.shared === "object" ? opts.shared : {};
const client = object(_client);
const server = object(_server);
const shared = object(_shared);
const client = typeof opts.client === "object" ? opts.client : object({});
const server = typeof opts.server === "object" ? opts.server : object({});
const shared = typeof opts.shared === "object" ? opts.shared : object({});
const isServer =
opts.isServer ?? (typeof window === "undefined" || "Deno" in window);

Expand Down
20 changes: 10 additions & 10 deletions packages/core/src/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createEnv } from ".";
*/
export const vercel = () =>
createEnv({
server: {
server: z.object({
VERCEL: z.string().optional(),
VERCEL_ENV: z.enum(["development", "preview", "production"]).optional(),
VERCEL_URL: z.string().optional(),
Expand All @@ -26,7 +26,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(),
},
}),
runtimeEnv: process.env,
});

Expand All @@ -35,10 +35,10 @@ export const vercel = () =>
*/
export const uploadthing = () =>
createEnv({
server: {
server: z.object({
UPLOADTHING_SECRET: z.string(),
UPLOADTHING_APP_ID: z.string().optional(),
},
}),
runtimeEnv: process.env,
});

Expand All @@ -48,7 +48,7 @@ export const uploadthing = () =>
*/
export const render = () =>
createEnv({
server: {
server: z.object({
IS_PULL_REQUEST: z.string().optional(),
RENDER_DISCOVERY_SERVICE: z.string().optional(),
RENDER_EXTERNAL_HOSTNAME: z.string().optional(),
Expand All @@ -63,7 +63,7 @@ export const render = () =>
.enum(["web", "pserv", "cron", "worker", "static"])
.optional(),
RENDER: z.string().optional(),
},
}),
runtimeEnv: process.env,
});

Expand All @@ -73,7 +73,7 @@ export const render = () =>
*/
export const railway = () =>
createEnv({
server: {
server: z.object({
RAILWAY_PUBLIC_DOMAIN: z.string().optional(),
RAILWAY_PRIVATE_DOMAIN: z.string().optional(),
RAILWAY_TCP_PROXY_DOMAIN: z.string().optional(),
Expand All @@ -97,7 +97,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(),
},
}),
runtimeEnv: process.env,
});

Expand All @@ -107,7 +107,7 @@ export const railway = () =>
*/
export const fly = () =>
createEnv({
server: {
server: z.object({
FLY_APP_NAME: z.string().optional(),
FLY_MACHINE_ID: z.string().optional(),
FLY_ALLOC_ID: z.string().optional(),
Expand All @@ -119,6 +119,6 @@ export const fly = () =>
FLY_PROCESS_GROUP: z.string().optional(),
FLY_VM_MEMORY_MB: z.string().optional(),
PRIMARY_REGION: z.string().optional(),
},
}),
runtimeEnv: process.env,
});
Loading