Skip to content

Commit aa44574

Browse files
juliusmarmingec-ehrlichMuhammed Mustafa
authored
feat: single env file (#1092)
* single env file * docs * fixy * better comments * serverside -> server-side add prop to error msg * dont crash when calling next-auth in gssp * add min(1) to all string schema * Update www/src/pages/ar/usage/env-variables.md Co-authored-by: Muhammed Mustafa <[email protected]> * simplify error formatting * allow empty strings for discord env vars * incorrect import * here also * add changeset * don't leak env keys in prod * fix bad merge * cleanup --------- Co-authored-by: Christopher Ehrlich <[email protected]> Co-authored-by: Muhammed Mustafa <[email protected]>
1 parent efe8b7f commit aa44574

File tree

23 files changed

+431
-206
lines changed

23 files changed

+431
-206
lines changed

Diff for: .changeset/loud-coats-sniff.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-t3-app": minor
3+
---
4+
5+
single env file

Diff for: cli/src/installers/envVars.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const envVariablesInstaller: Installer = ({ projectDir, packages }) => {
99

1010
const envContent = getEnvContent(!!usingAuth, !!usingPrisma);
1111

12-
const envSchemaFile =
12+
const envFile =
1313
usingAuth && usingPrisma
1414
? "with-auth-prisma.mjs"
1515
: usingAuth
@@ -18,13 +18,13 @@ export const envVariablesInstaller: Installer = ({ projectDir, packages }) => {
1818
? "with-prisma.mjs"
1919
: "";
2020

21-
if (envSchemaFile !== "") {
21+
if (envFile !== "") {
2222
const envSchemaSrc = path.join(
2323
PKG_ROOT,
24-
"template/extras/src/env/schema",
25-
envSchemaFile,
24+
"template/extras/src/env",
25+
envFile,
2626
);
27-
const envSchemaDest = path.join(projectDir, "src/env/schema.mjs");
27+
const envSchemaDest = path.join(projectDir, "src/env.mjs");
2828
fs.copySync(envSchemaSrc, envSchemaDest);
2929
}
3030

Diff for: cli/template/base/next.config.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
55
* This is especially useful for Docker builds.
66
*/
7-
!process.env.SKIP_ENV_VALIDATION && (await import("./src/env/server.mjs"));
7+
!process.env.SKIP_ENV_VALIDATION && (await import("./src/env.mjs"));
88

99
/** @type {import("next").NextConfig} */
1010
const config = {

Diff for: cli/template/base/src/env.mjs

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/* eslint-disable @typescript-eslint/ban-ts-comment */
2+
import { z } from "zod";
3+
4+
/**
5+
* Specify your server-side environment variables schema here.
6+
* This way you can ensure the app isn't built with invalid env vars.
7+
*/
8+
const server = z.object({
9+
NODE_ENV: z.enum(["development", "test", "production"]),
10+
});
11+
12+
/**
13+
* Specify your client-side environment variables schema here.
14+
* This way you can ensure the app isn't built with invalid env vars.
15+
* To expose them to the client, prefix them with `NEXT_PUBLIC_`.
16+
*/
17+
const client = z.object({
18+
// NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
19+
});
20+
21+
/**
22+
* You can't destruct `process.env` as a regular object in the Next.js
23+
* edge runtimes (e.g. middlewares) or client-side so we need to destruct manually.
24+
* @type {Record<keyof z.infer<typeof server> | keyof z.infer<typeof client>, string | undefined>}
25+
*/
26+
const processEnv = {
27+
NODE_ENV: process.env.NODE_ENV,
28+
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
29+
};
30+
31+
// Don't touch the part below
32+
// --------------------------
33+
34+
const merged = server.merge(client);
35+
/** @type z.infer<merged>
36+
* @ts-ignore - can't type this properly in jsdoc */
37+
let env = process.env;
38+
39+
if (!!process.env.SKIP_ENV_VALIDATION == false) {
40+
const isServer = typeof window === "undefined";
41+
42+
const parsed = isServer
43+
? merged.safeParse(processEnv) // on server we can validate all env vars
44+
: client.safeParse(processEnv); // on client we can only validate the ones that are exposed
45+
46+
if (parsed.success === false) {
47+
console.error(
48+
"❌ Invalid environment variables:",
49+
parsed.error.flatten().fieldErrors,
50+
);
51+
throw new Error("Invalid environment variables");
52+
}
53+
54+
/** @type z.infer<merged>
55+
* @ts-ignore - can't type this properly in jsdoc */
56+
env = new Proxy(parsed.data, {
57+
get(target, prop) {
58+
if (typeof prop !== "string") return undefined;
59+
// Throw a descriptive error if a server-side env var is accessed on the client
60+
// Otherwise it would just be returning `undefined` and be annoying to debug
61+
if (!isServer && !prop.startsWith("NEXT_PUBLIC_"))
62+
throw new Error(
63+
process.env.NODE_ENV === "production"
64+
? "❌ Attempted to access a server-side environment variable on the client"
65+
: `❌ Attempted to access server-side environment variable '${prop}' on the client`,
66+
);
67+
/* @ts-ignore - can't type this properly in jsdoc */
68+
return target[prop];
69+
},
70+
});
71+
}
72+
73+
export { env };

Diff for: cli/template/base/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"target": "es2017",
44
"lib": ["dom", "dom.iterable", "esnext"],
55
"allowJs": true,
6+
"checkJs": true,
67
"skipLibCheck": true,
78
"strict": true,
89
"forceConsistentCasingInFileNames": true,

Diff for: cli/template/extras/src/env/schema/with-auth-prisma.mjs

-56
This file was deleted.

Diff for: cli/template/extras/src/env/schema/with-auth.mjs

-54
This file was deleted.

Diff for: cli/template/extras/src/env/schema/with-prisma.mjs

-39
This file was deleted.

Diff for: cli/template/extras/src/env/with-auth-prisma.mjs

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/* eslint-disable @typescript-eslint/ban-ts-comment */
2+
import { z } from "zod";
3+
4+
/**
5+
* Specify your server-side environment variables schema here.
6+
* This way you can ensure the app isn't built with invalid env vars.
7+
*/
8+
const server = z.object({
9+
DATABASE_URL: z.string().url(),
10+
NODE_ENV: z.enum(["development", "test", "production"]),
11+
NEXTAUTH_SECRET:
12+
process.env.NODE_ENV === "production"
13+
? z.string().min(1)
14+
: z.string().min(1).optional(),
15+
NEXTAUTH_URL: z.preprocess(
16+
// This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
17+
// Since NextAuth.js automatically uses the VERCEL_URL if present.
18+
(str) => process.env.VERCEL_URL ?? str,
19+
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
20+
process.env.VERCEL ? z.string().min(1) : z.string().url(),
21+
),
22+
// Add `.min(1) on ID and SECRET if you want to make sure they're not empty
23+
DISCORD_CLIENT_ID: z.string(),
24+
DISCORD_CLIENT_SECRET: z.string(),
25+
});
26+
27+
/**
28+
* Specify your client-side environment variables schema here.
29+
* This way you can ensure the app isn't built with invalid env vars.
30+
* To expose them to the client, prefix them with `NEXT_PUBLIC_`.
31+
*/
32+
const client = z.object({
33+
// NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
34+
});
35+
36+
/**
37+
* You can't destruct `process.env` as a regular object in the Next.js
38+
* edge runtimes (e.g. middlewares) or client-side so we need to destruct manually.
39+
* @type {Record<keyof z.infer<typeof server> | keyof z.infer<typeof client>, string | undefined>}
40+
*/
41+
const processEnv = {
42+
DATABASE_URL: process.env.DATABASE_URL,
43+
NODE_ENV: process.env.NODE_ENV,
44+
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
45+
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
46+
DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID,
47+
DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET,
48+
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
49+
};
50+
51+
// Don't touch the part below
52+
// --------------------------
53+
54+
const merged = server.merge(client);
55+
/** @type z.infer<merged>
56+
* @ts-ignore - can't type this properly in jsdoc */
57+
let env = process.env;
58+
59+
if (!!process.env.SKIP_ENV_VALIDATION == false) {
60+
const isServer = typeof window === "undefined";
61+
62+
const parsed = isServer
63+
? merged.safeParse(processEnv) // on server we can validate all env vars
64+
: client.safeParse(processEnv); // on client we can only validate the ones that are exposed
65+
66+
if (parsed.success === false) {
67+
console.error(
68+
"❌ Invalid environment variables:",
69+
parsed.error.flatten().fieldErrors,
70+
);
71+
throw new Error("Invalid environment variables");
72+
}
73+
74+
/** @type z.infer<merged>
75+
* @ts-ignore - can't type this properly in jsdoc */
76+
env = new Proxy(parsed.data, {
77+
get(target, prop) {
78+
if (typeof prop !== "string") return undefined;
79+
// Throw a descriptive error if a server-side env var is accessed on the client
80+
// Otherwise it would just be returning `undefined` and be annoying to debug
81+
if (!isServer && !prop.startsWith("NEXT_PUBLIC_"))
82+
throw new Error(
83+
process.env.NODE_ENV === "production"
84+
? "❌ Attempted to access a server-side environment variable on the client"
85+
: `❌ Attempted to access server-side environment variable '${prop}' on the client`,
86+
);
87+
/* @ts-ignore - can't type this properly in jsdoc */
88+
return target[prop];
89+
},
90+
});
91+
}
92+
93+
export { env };

0 commit comments

Comments
 (0)