mcpField
E
env-validation

env-validation

github.com/andrelandgraf开发2397 次安装20 周前更新
代码仓库

关于 env-validation

这是 Fullstack Recipes 中用于在服务器启动和构建前检查环境变量的技能,帮助开发者及早发现缺失或无效配置。它通过配置校验和 Bun 脚本检查环境文件,并可加入构建流程。

主要功能

  1. 01服务器启动时校验配置
  2. 02构建前校验环境文件
  3. 03按 Next.js 顺序加载环境文件
  4. 04查找并检查 config.ts 文件
  5. 05提示未使用的变量
  6. 06配置无效时以错误退出

适用场景

  1. 01在服务器启动时发现缺失设置
  2. 02环境值无效时中止构建
  3. 03检查本地和 CI 构建配置
  4. 04找出配置未使用的环境变量

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/andrelandgraf/fullstackrecipes --skill env-validation

安装后在 Claude Code 中输入/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 中)都验证环境变量。如果验证失败,构建会提前停止并显示清晰的错误。

该脚本会:

  1. 使用 Next.js 的 loadEnvConfig 加载 .env 文件(遵循与 Next.js 相同的加载顺序)
  2. 查找 src/lib/*/ 中的所有 config.ts 文件
  3. 导入每个配置以触发 configSchema 验证
  4. 报告缺失或无效的环境变量
  5. 警告 .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