143 lines
4.4 KiB
JavaScript
143 lines
4.4 KiB
JavaScript
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;
|
||
}
|
||
}
|