Files
web_hgdja_h5/scripts/package-standalone.ts
谢宇宁andClaude Opus 5.5 6bd5cc665a Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 20:30:26 +07:00

247 lines
7.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createRequire } from "node:module";
import {
cp,
lstat,
mkdir,
readFile,
readdir,
realpath,
rename,
rm,
writeFile,
} from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { parse } from "dotenv";
const environment = process.argv[2] || "production";
if (!["development", "test", "production"].includes(environment)) {
throw new Error("Unknown environment");
}
/** 本地构建目录(test/dev 分目录,避免互相覆盖) */
const buildDirectory =
environment === "production" ? ".next" : environment === "test" ? ".next-test" : ".next-dev";
/** 测试包保留 .next-test,与 1047 的 run.sh 一致;正式包统一为 .next */
const runtimeDirectory = environment === "test" ? buildDirectory : ".next";
const destination = resolve("dist", environment);
const require = createRequire(import.meta.url);
/** 将包内 .next-test / .next-dev 路径改写为 .next(不含 node_modules) */
async function rewriteRuntimeDistDir(root: string, from: string, to: string) {
if (from === to) return;
const skip = new Set(["node_modules"]);
const walk = async (dir: string) => {
for (const entry of await readdir(dir, { withFileTypes: true })) {
if (skip.has(entry.name)) continue;
const path = join(dir, entry.name);
if (entry.isDirectory()) {
await walk(path);
continue;
}
if (!/\.(js|mjs|cjs|json|map)$/.test(entry.name)) continue;
const text = await readFile(path, "utf8");
if (!text.includes(from)) continue;
await writeFile(path, text.split(from).join(to));
}
};
await walk(root);
}
const readConfig = async (file: string) =>
parse(
await readFile(file).catch((error: NodeJS.ErrnoException) => {
if (error.code === "ENOENT") return Buffer.alloc(0);
throw error;
}),
);
const config = {
...(await readConfig(resolve("config", `${environment}.env`))),
...(await readConfig(resolve("config", `${environment}.local.env`))),
...process.env,
};
const runtimePort = (config.PORT || "3000").trim() || "3000";
for (const directory of [resolve("dist"), destination]) {
const info = await lstat(directory).catch((error: NodeJS.ErrnoException) => {
if (error.code === "ENOENT") return null;
throw error;
});
if (
info &&
(!info.isDirectory() || info.isSymbolicLink() || (await realpath(directory)) !== directory)
) {
throw new Error("Standalone output must be an ordinary build directory");
}
}
const buildId = (await readFile(resolve(buildDirectory, "BUILD_ID"), "utf8")).trim();
if (!buildId) throw new Error(`${buildDirectory}/BUILD_ID 为空,请先 pnpm build`);
await rm(destination, { recursive: true, force: true });
await mkdir(destination, { recursive: true });
await cp(resolve(buildDirectory, "standalone"), destination, { recursive: true });
await cp(resolve("public"), resolve(destination, "public"), { recursive: true });
await cp(
resolve(buildDirectory, "static"),
resolve(destination, buildDirectory, "static"),
{ recursive: true },
);
// 测试/开发构建:把包内目录规范成 .next,并改写 server.js / chunks 中的 distDir
if (buildDirectory !== runtimeDirectory) {
const fromDir = resolve(destination, buildDirectory);
const toDir = resolve(destination, runtimeDirectory);
await rm(toDir, { recursive: true, force: true });
await rename(fromDir, toDir);
await rewriteRuntimeDistDir(destination, buildDirectory, runtimeDirectory);
}
// 将 standalone server.js 默认端口改为当前环境 PORT(Next 生成时写死 3000)
{
const serverJsPath = resolve(destination, "server.js");
const serverJs = await readFile(serverJsPath, "utf8");
const nextDefaultPort =
/const currentPort = parseInt\(process\.env\.PORT, 10\) \|\| 3000/;
if (!nextDefaultPort.test(serverJs)) {
throw new Error(
"server.js 未找到默认端口写法 `parseInt(process.env.PORT, 10) || 3000`,请检查 Next standalone 产物",
);
}
await writeFile(
serverJsPath,
serverJs.replace(
nextDefaultPort,
`const currentPort = parseInt(process.env.PORT, 10) || ${runtimePort}`,
),
);
}
await mkdir(resolve(destination, "config"), { recursive: true });
await cp(
resolve("config", `${environment}.env`),
resolve(destination, "config", `${environment}.env`),
);
if (environment !== "test") {
const dotenvRoot = dirname(require.resolve("dotenv/package.json"));
await mkdir(resolve(destination, "node_modules"), { recursive: true });
await cp(dotenvRoot, resolve(destination, "node_modules/dotenv"), {
recursive: true,
});
}
// 运行包 package.json:禁止 next start,必须走 standalone server.js
await writeFile(
resolve(destination, "package.json"),
`${JSON.stringify(
{
name: "jha-1048-hddj-web",
version: "1.0.0",
private: true,
type: "module",
scripts: {
start: "node ./start.mjs",
},
engines: { node: ">=20.9.0" },
},
null,
2,
)}\n`,
);
await writeFile(
resolve(destination, "start.mjs"),
`import { readFileSync, existsSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parse } from 'dotenv';
const root = dirname(fileURLToPath(import.meta.url));
process.chdir(root);
if (existsSync(resolve('static'))) {
throw new Error('Standalone must run in an isolated directory without a legacy static/ folder');
}
const buildIdFile = resolve(${JSON.stringify(runtimeDirectory)}, 'BUILD_ID');
if (!existsSync(buildIdFile)) {
throw new Error(
'缺少 ' + ${JSON.stringify(runtimeDirectory)} + '/BUILD_ID。请使用打包产物并用 node start.mjs 启动,不要使用 next start。',
);
}
const environment = ${JSON.stringify(environment)};
const read = (file) => (existsSync(file) ? parse(readFileSync(file)) : {});
const config = {
...read(resolve('config', environment + '.env')),
...read(resolve('config', environment + '.local.env')),
...process.env,
APP_ENV: environment,
NODE_ENV: 'production',
};
const api = (config.API_PROXY_TARGET || config.NEXT_PUBLIC_API_BASE_URL || '').trim();
if (!api) throw new Error('API_PROXY_TARGET 或 NEXT_PUBLIC_API_BASE_URL 必填');
Object.assign(process.env, config);
// standalone 入口(不是 next start)
await import('./server.js');
`,
);
if (environment === "test") {
await cp(resolve("package.json"), resolve(destination, "package.json"));
await cp(resolve("package-lock.json"), resolve(destination, "package-lock.json"));
await mkdir(resolve(destination, "scripts"), { recursive: true });
await cp(resolve("scripts/next-env.mjs"), resolve(destination, "scripts/next-env.mjs"));
await cp(
resolve("scripts/check-health.mjs"),
resolve(destination, "scripts/check-health.mjs"),
);
}
await writeFile(
resolve(destination, "ecosystem.config.cjs"),
`/** PM2:必须 node start.mjs,禁止 next start */
module.exports = {
apps: [
{
name: "hddj_h5",
cwd: __dirname,
script: "start.mjs",
interpreter: "node",
instances: 1,
exec_mode: "fork",
env: {
NODE_ENV: "production",
APP_ENV: ${JSON.stringify(environment)},
HOSTNAME: "0.0.0.0",
PORT: ${JSON.stringify(runtimePort)},
},
},
],
};
`,
);
const packagedBuildId = (
await readFile(resolve(destination, runtimeDirectory, "BUILD_ID"), "utf8")
).trim();
if (packagedBuildId !== buildId) {
throw new Error(`打包后 BUILD_ID 不一致:build=${buildId} packaged=${packagedBuildId}`);
}
const api =
config.API_PROXY_TARGET?.trim() || config.NEXT_PUBLIC_API_BASE_URL?.trim();
if (!api) {
console.warn(
`[package-standalone] 警告:当前 ${environment} 未配置 API_PROXY_TARGET / NEXT_PUBLIC_API_BASE_URL`,
);
}
console.log(`Standalone package ready: ${destination}`);
console.log(`BUILD_ID=${packagedBuildId}`);
console.log(`PORT=${runtimePort}`);
console.log(`启动:cd ${destination} && node start.mjs`);
console.log(`或:pm2 start ${destination}/ecosystem.config.cjs`);