关于 env-validation
这是 Fullstack Recipes 中用于在服务器启动和构建前检查环境变量的技能,帮助开发者及早发现缺失或无效配置。它通过配置校验和 Bun 脚本检查环境文件,并可加入构建流程。
主要功能
- 01服务器启动时校验配置
- 02构建前校验环境文件
- 03按 Next.js 顺序加载环境文件
- 04查找并检查 config.ts 文件
- 05提示未使用的变量
- 06配置无效时以错误退出
适用场景
- 01在服务器启动时发现缺失设置
- 02环境值无效时中止构建
- 03检查本地和 CI 构建配置
- 04找出配置未使用的环境变量
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ andrelandgraf/ fullstackrecipes --skill env-validation安装后在 Claude Code 中输入/env-validation确认已加载。
详细介绍
构建时环境变量验证
在服务器启动时和构建前验证环境变量。尽早发现缺失或无效的变量,并显示清晰的错误消息。
实现环境验证
在服务器启动时和构建前验证环境变量。尽早发现缺失或无效的变量,并显示清晰的错误消息。
参见:
- 资源:Fullstack Recipes 中的
env-validation - URL:https://fullstackrecipes.com/recipes/env-validation
在服务器启动时验证配置
某些环境变量由软件包内部读取,而不是作为参数传入。要尽早发现缺失的变量,而非等到运行时再发现,请在 instrumentation.ts 中导入配置:
// src/instrumentation.ts
import * as Sentry from "@sentry/nextjs";
import { sentryConfig } from "./lib/sentry/config";
// Validate required configs on server start
import "./lib/ai/config";
import "./lib/db/config";
export async function register() {
// ... initialization code
}
这些副作用导入会在服务器启动时立即触发 configSchema 验证。如果缺少任何必需的环境变量,服务器会无法启动,并显示清晰的错误,而不是等到执行相应代码路径时才失败。
在构建前验证环境文件
通过 shadcn 注册表安装:
bunx --bun shadcn@latest add https://fullstackrecipes.com/r/validate-env.json
或复制源代码:
scripts/validate-env.ts:
#!/usr/bin/env bun
/**
* Validate environment configuration
*
* Usage:
* bun run validate-env
* bun run validate-env --environment=development
* bun run validate-env --environment=production
*
* This script:
* 1. Loads env files using Next.js's loadEnvConfig
* 2. Finds all config.ts files in src/lib/\*\/
* 3. Validates each config by importing it (triggers configSchema validation)
* 4. Warns about env variables in .env files that aren't used by any config
*/
import { loadEnvConfig } from "@next/env";
import { Glob } from "bun";
import path from "path";
// ANSI colors
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
// Parse CLI args
function parseArgs(): { environment?: string } {
const args = process.argv.slice(2);
const result: { environment?: string } = {};
for (const arg of args) {
if (arg.startsWith("--environment=")) {
result.environment = arg.split("=")[1];
}
}
return result;
}
// Track which env vars are referenced by configs
const referencedEnvVars = new Set<string>();
// Patch process.env to track access
function trackEnvAccess() {
const originalEnv = process.env;
const handler: ProxyHandler<NodeJS.ProcessEnv> = {
get(target, prop) {
if (typeof prop === "string" && !prop.startsWith("_")) {
referencedEnvVars.add(prop);
}
return Reflect.get(target, prop);
},
};
process.env = new Proxy(originalEnv, handler);
}
async function main() {
const args = parseArgs();
const projectDir = process.cwd();
console.log(bold("\n🔍 Environment Configuration Validator\n"));
// Set NODE_ENV if environment specified
const environment = args.environment ?? process.env.NODE_ENV ?? "development";
(process.env as Record<string, string>).NODE_ENV = environment;
console.log(dim(` Environment: ${environment}\n`));
// Load env files
// Second param `dev` tells loadEnvConfig to load .env.development files
const isDev = environment === "development";
console.log(dim(" Loading environment files..."));
const loadedEnvFiles: string[] = [];
const { combinedEnv, loadedEnvFiles: files } = loadEnvConfig(
projectDir,
isDev,
);
for (const file of files) {
loadedEnvFiles.push(file.path);
console.log(dim(` ✓ ${path.relative(projectDir, file.path)}`));
}
if (loadedEnvFiles.length === 0) {
console.log(dim(" No .env files found"));
}
console.log("");
// Start tracking env access before importing configs
trackEnvAccess();
// Find all config.ts files
const configGlob = new Glob("src/lib/*/config.ts");
const configFiles: string[] = [];
for await (const file of configGlob.scan(projectDir)) {
configFiles.push(file);
}
if (configFiles.length === 0) {
console.log(yellow(" ⚠ No config.ts files found in src/lib/*/\n"));
process.exit(0);
}
console.log(dim(` Found ${configFiles.length} config files:\n`));
// Validate each config
const errors: { file: string; error: Error }[] = [];
const validated: string[] = [];
for (const configFile of configFiles) {
const relativePath = configFile;
const absolutePath = path.join(projectDir, configFile);
try {
// Import the config module - this triggers validation
await import(absolutePath);
console.log(green(` ✓ ${relativePath}`));
validated.push(relativePath);
} catch (error) {
if (error instanceof Error) {
// Check if it's a disabled feature flag (not an error)
if (error.message.includes("isEnabled: false")) {
console.log(dim(` ○ ${relativePath} (feature disabled)`));
validated.push(relativePath);
} else {
console.log(red(` ✗ ${relativePath}`));
errors.push({ file: relativePath, error });
}
}
}
}
console.log("");
// Report validation errors
if (errors.length > 0) {
console.log(red(bold("Validation Errors:\n")));
for (const { file, error } of errors) {
console.log(red(` ${file}:`));
// Extract the actual error message
const message = error.message.split("\n").slice(0, 3).join("\n ");
console.log(red(` ${message}\n`));
}
}
// Find unused env variables (in .env files but not referenced by configs)
const envVarsInFiles = new Set<string>();
// Parse loaded env files to get all defined variables
for (const envFile of loadedEnvFiles) {
try {
const content = await Bun.file(envFile).text();
const lines = content.split("\n");
for (const line of lines) {
const trimmed = line.trim();
// Skip comments and empty lines
if (!trimmed || trimmed.startsWith("#")) continue;
// Extract variable name (before = sign)
const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)\s*=/);
if (match) {
envVarsInFiles.add(match[1]);
}
}
} catch {
// Ignore file read errors
}
}
// Common system/framework vars to ignore
const ignoredVars = new Set([
// System
"NODE_ENV",
"PATH",
"HOME",
"USER",
"SHELL",
"TERM",
"LANG",
"PWD",
"OLDPWD",
"HOSTNAME",
"LOGNAME",
"TMPDIR",
"XDG_CONFIG_HOME",
"XDG_DATA_HOME",
"XDG_CACHE_HOME",
"CI",
"TZ",
// Vercel
"VERCEL",
"VERCEL_ENV",
"VERCEL_URL",
"VERCEL_REGION",
"VERCEL_TARGET_ENV",
"VERCEL_GIT_COMMIT_SHA",
"VERCEL_GIT_COMMIT_MESSAGE",
"VERCEL_GIT_COMMIT_AUTHOR_LOGIN",
"VERCEL_GIT_COMMIT_AUTHOR_NAME",
"VERCEL_GIT_PREVIOUS_SHA",
"VERCEL_GIT_PROVIDER",
"VERCEL_GIT_REPO_ID",
"VERCEL_GIT_REPO_OWNER",
"VERCEL_GIT_REPO_SLUG",
"VERCEL_GIT_COMMIT_REF",
"VERCEL_GIT_PULL_REQUEST_ID",
// Build tools (Turbo, NX)
"TURBO_CACHE",
"TURBO_REMOTE_ONLY",
"TURBO_RUN_SUMMARY",
"TURBO_DOWNLOAD_LOCAL_ENABLED",
"NX_DAEMON",
]);
// Find vars in .env files but not referenced by configs
const unusedVars: { name: string; files: string[] }[] = [];
for (const envVar of envVarsInFiles) {
if (ignoredVars.has(envVar)) continue;
if (referencedEnvVars.has(envVar)) continue;
// Find which files define this var
const definingFiles: string[] = [];
for (const envFile of loadedEnvFiles) {
try {
const content = await Bun.file(envFile).text();
if (new RegExp(`^${envVar}\\s*=`, "m").test(content)) {
definingFiles.push(path.relative(projectDir, envFile));
}
} catch {
// Ignore
}
}
if (definingFiles.length > 0) {
unusedVars.push({ name: envVar, files: definingFiles });
}
}
// Report unused vars
if (unusedVars.length > 0) {
console.log(yellow(bold("Unused Environment Variables:\n")));
console.log(
dim(
" These variables are defined in .env files but not used by any config:\n",
),
);
for (const { name, files } of unusedVars.sort((a, b) =>
a.name.localeCompare(b.name),
)) {
console.log(yellow(` ⚠ ${name}`));
console.log(dim(` defined in: ${files.join(", ")}`));
}
console.log("");
}
// Summary
console.log(bold("Summary:\n"));
console.log(` Configs validated: ${green(String(validated.length))}`);
console.log(
` Validation errors: ${errors.length > 0 ? red(String(errors.length)) : green("0")}`,
);
console.log(
` Unused env vars: ${unusedVars.length > 0 ? yellow(String(unusedVars.length)) : green("0")}`,
);
console.log("");
// Exit with error code if validation failed
if (errors.length > 0) {
process.exit(1);
}
}
main().catch((error) => {
console.error(red("Unexpected error:"), error);
process.exit(1);
});
将验证脚本添加到 package.json:
{
"scripts": {
"prebuild": "bun run env:validate:prod",
"env:validate": "bun run scripts/validate-env.ts --environment=development",
"env:validate:prod": "bun run scripts/validate-env.ts --environment=production"
}
}
使用 env:validate 和 env:validate:prod 脚本,根据 .env 文件验证所有配置(src/lib/*/ 中的 config.ts 文件)。
(如上所述配置的)prebuild 脚本会在 build 之前自动运行,确保每次构建前(本地和 CI/Vercel 中)都验证环境变量。如果验证失败,构建会提前停止并显示清晰的错误。
该脚本会:
- 使用 Next.js 的
loadEnvConfig加载.env文件(遵循与 Next.js 相同的加载顺序) - 查找
src/lib/*/中的所有config.ts文件 - 导入每个配置以触发
configSchema验证 - 报告缺失或无效的环境变量
- 警告
.env文件中定义但未被任何配置使用的变量
验证出错时的输出示例:
🔍 Environment Configuration Validator
Environment: development
Loading environment files...
✓ .env.local
✓ .env.development
Found 5 config files:
✗ src/lib/resend/config.ts
✓ src/lib/sentry/config.ts
✓ src/lib/db/config.ts
✓ src/lib/ai/config.ts
✓ src/lib/auth/config.ts
Validation Errors:
src/lib/resend/config.ts:
Configuration validation error for Resend!
Did you correctly set all required environment variables in your .env* file?
- server.fromEmail (FROM_EMAIL) must be defined.
Summary:
Configs validated: 4
Validation errors: 1
Unused env vars: 0
存在未使用变量时的输出示例:
🔍 Environment Configuration Validator
Environment: development
Loading environment files...
✓ .env.local
✓ .env.development
Found 5 config files:
✓ src/lib/resend/config.ts
✓ src/lib/sentry/config.ts
✓ src/lib/db/config.ts
✓ src/lib/ai/config.ts
✓ src/lib/auth/config.ts
Unused Environment Variables:
These variables are defined in .env files but not used by any config:
⚠ OLD_API_KEY
defined in: .env.local
Summary:
Configs validated: 5
Validation errors: 0
Unused env vars: 1
如果发生任何验证错误,脚本会以代码 1 退出(这对 CI 很有用);未使用的变量只会触发警告,不会导致构建失败。
评论
支持 Markdown