import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { spawn } from "node:child_process"; import { parse } from "dotenv"; const projectRoot = fileURLToPath(new URL("../", import.meta.url)); const directories = { development: ".next-dev", test: ".next-test", production: ".next", }; const commands = new Set(["dev", "build", "start"]); const readConfig = (file) => existsSync(file) ? parse(readFileSync(file)) : {}; // Normalize each layer before merging so a legacy process override still wins over a profile. const apiConfig = (values) => ({ ...values, ...(!Object.hasOwn(values, "UPSTREAM_API_URL") && Object.hasOwn(values, "API_BASE_URL") ? { UPSTREAM_API_URL: values.API_BASE_URL } : {}), }); /** APP_ENV selects the deployment; NODE_ENV keeps Next's development/production semantics. */ export function nextCommand( environment = "development", command = "dev", args = [], { root = projectRoot, variables = process.env } = {}, ) { if (!Object.hasOwn(directories, environment) || !commands.has(command)) throw new Error( "Usage: node scripts/next-env.mjs development|test|production dev|build|start", ); const base = resolve(root, "config", environment + ".env"); if (!existsSync(base)) throw new Error(`缺少环境配置:config/${environment}.env`); const env = { ...apiConfig(readConfig(base)), // Preserve the old local-development setup without leaking it into test/production. ...(environment === "development" ? apiConfig(readConfig(resolve(root, ".env.local"))) : {}), ...apiConfig( readConfig(resolve(root, "config", environment + ".local.env")), ), ...apiConfig( Object.fromEntries( Object.entries(variables).filter(([, value]) => value !== undefined), ), ), APP_ENV: environment, APP_DIST_DIR: directories[environment], NODE_ENV: command === "dev" ? "development" : "production", }; try { const value = (env.UPSTREAM_API_URL || "").trim(); if (value === "current") env.UPSTREAM_API_URL = value; else { const api = new URL(value); if ( !["http:", "https:"].includes(api.protocol) || api.username || api.password || api.search || api.hash ) throw new Error(); env.UPSTREAM_API_URL = api.href.replace(/\/+$/, ""); } env.API_BASE_URL = env.UPSTREAM_API_URL; } catch { throw new Error( `config/${environment}.env 的 UPSTREAM_API_URL(兼容 API_BASE_URL)必须为 current 或有效的 HTTP(S) 接口地址`, ); } const port = String(env.PORT || "21030"); if (!/^\d+$/.test(port) || Number(port) < 1 || Number(port) > 65535) throw new Error("PORT 必须在 1–65535 之间"); env.PORT = port; const hasOption = (names) => args.some((arg) => names.some((name) => arg === name || arg.startsWith(name + "=")), ); const options = command === "build" ? [] : [ ...(hasOption(["--hostname", "-H"]) ? [] : ["--hostname", "0.0.0.0"]), ...(hasOption(["--port", "-p"]) ? [] : ["--port", port]), ]; return { executable: process.execPath, args: [ resolve(root, "node_modules/next/dist/bin/next"), command, ...options, ...args, ], cwd: root, env, }; } export async function runNext(environment, command, args, options) { const spec = nextCommand(environment, command, args, options); const child = spawn(spec.executable, spec.args, { cwd: spec.cwd, env: spec.env, stdio: "inherit", }); const interrupt = () => child.kill("SIGINT"), terminate = () => child.kill("SIGTERM"); process.on("SIGINT", interrupt); process.on("SIGTERM", terminate); try { return await new Promise((resolve, reject) => { child.once("error", reject); child.once("exit", (code, signal) => resolve(code ?? (signal === "SIGINT" ? 130 : 143)), ); }); } finally { process.off("SIGINT", interrupt); process.off("SIGTERM", terminate); } } if ( process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) ) { try { const [environment = "development", command = "dev", ...args] = process.argv.slice(2); process.exitCode = await runNext(environment, command, args); } catch (error) { console.error(error instanceof Error ? error.message : "启动 Next.js 失败"); process.exitCode = 1; } }