@@ -0,0 +1,142 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user