416 lines
13 KiB
TypeScript
416 lines
13 KiB
TypeScript
import { execSync } from "child_process";
|
||
import fs from "fs";
|
||
import path from "path";
|
||
import { parseArgs } from "util";
|
||
import { BuildOptions, ParsedArgs } from "./types";
|
||
import { generateDockerfile, generateDockerignore } from "./template/runtime";
|
||
import { generateInitDockerfile, generateInitDockerignore } from "./template/init";
|
||
|
||
const pwd = process.cwd();
|
||
|
||
|
||
|
||
// 测试docker命令是否可用
|
||
try {
|
||
execSync("docker --version", { stdio: "ignore" });
|
||
} catch (error) {
|
||
console.error("❌ Docker 命令不可用,请确保已安装并正确配置 Docker");
|
||
process.exit(1);
|
||
}
|
||
|
||
|
||
// 目前只支持linux或者macOS系统
|
||
if (process.platform !== "linux" && process.platform !== "darwin") {
|
||
console.error("❌ 目前仅支持 Linux 和 macOS 系统");
|
||
process.exit(1);
|
||
}
|
||
|
||
/**
|
||
* 解析命令行参数
|
||
*/
|
||
function parseCliArgs(): ParsedArgs | null {
|
||
const { values, positionals } = parseArgs({
|
||
options: {
|
||
env: {
|
||
type: "string",
|
||
default: "dev",
|
||
},
|
||
help: {
|
||
type: "boolean",
|
||
short: "h",
|
||
default: false,
|
||
},
|
||
extra: {
|
||
type: "string",
|
||
multiple: true,
|
||
default: [],
|
||
},
|
||
registry: {
|
||
type: "string",
|
||
default: "https://registry.npmmirror.com",
|
||
},
|
||
port: {
|
||
type: "string",
|
||
default: ["3001", "8080"],
|
||
multiple: true,
|
||
},
|
||
"node-version": {
|
||
type: "string",
|
||
default: "20",
|
||
},
|
||
// 打包web还是后端
|
||
"build-type": {
|
||
type: "string",
|
||
default: "backend",
|
||
},
|
||
"web-source": {
|
||
type: "string",
|
||
default: "web",
|
||
}
|
||
},
|
||
allowPositionals: true,
|
||
});
|
||
|
||
// 显示帮助信息
|
||
if (values.help) {
|
||
console.log(`
|
||
使用方法: docker-builder <项目目录> [选项]
|
||
|
||
参数:
|
||
<项目目录> 要打包的项目目录(必需)
|
||
|
||
选项:
|
||
--env=<environment> 环境类型 (dev|prod|staging),默认: dev
|
||
--extra=<model> 额外的依赖目录(可多次使用)
|
||
--registry=<url> npm 镜像站地址,默认: https://registry.npmmirror.com
|
||
--port=<port> 暴露的端口号,默认: 3001
|
||
--node-version=<ver> Node.js 版本,默认: 20
|
||
--build-type=<type> 构建类型 (native|wechatMp|web|backend),默认: backend
|
||
-h, --help 显示此帮助信息
|
||
|
||
示例:
|
||
docker-builder project
|
||
docker-builder project --extra=model1 --extra=model2
|
||
docker-builder project --env=prod --port=8080 --node-version=18
|
||
`);
|
||
return null;
|
||
}
|
||
|
||
// 获取项目目录
|
||
if (positionals.length === 0) {
|
||
console.error("❌ 请指定要打包的项目目录");
|
||
console.error(" 使用 --help 查看详细用法");
|
||
process.exit(1);
|
||
}
|
||
|
||
const projectDir = path.resolve(positionals[0]);
|
||
|
||
// 验证环境参数
|
||
const env = values.env as string;
|
||
if (!["dev", "prod", "staging"].includes(env)) {
|
||
console.error(`❌ 无效的环境类型: ${env}`);
|
||
console.error(" 支持的环境: dev, prod, staging");
|
||
process.exit(1);
|
||
}
|
||
|
||
if (!["native", "wechatMp", "web", "backend", "init"].includes(values["build-type"] as string)) {
|
||
console.error(`❌ 无效的构建类型: ${values["build-type"]}`);
|
||
console.error(" 支持的类型: native, wechatMp, web, backend");
|
||
process.exit(1);
|
||
}
|
||
|
||
return {
|
||
projectDir,
|
||
options: {
|
||
env,
|
||
extra: values.extra as string[],
|
||
registry: values.registry as string,
|
||
port: values.port as string[],
|
||
nodeVersion: values["node-version"] as string,
|
||
buildType: values["build-type"] as string,
|
||
webSource: values["web-source"] as string,
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 构建 Docker 镜像
|
||
*/
|
||
function buildDockerImage(
|
||
imageBase: string,
|
||
name: string,
|
||
pwd: string
|
||
): void {
|
||
console.log(`\n🔧 Building image for ${name}...`);
|
||
execSync(`docker build -t ${imageBase}:${name} ${pwd}`, {
|
||
stdio: "inherit",
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 保存 Docker 镜像
|
||
*/
|
||
function saveDockerImage(
|
||
imageBase: string,
|
||
name: string,
|
||
pwd: string
|
||
): void {
|
||
const distDir = path.join(pwd, "dist");
|
||
if (!fs.existsSync(distDir)) {
|
||
fs.mkdirSync(distDir);
|
||
}
|
||
const outputImagePath = path.join(distDir, "server", imageBase, `${imageBase}-${name}.tar`);
|
||
if (!fs.existsSync(path.dirname(outputImagePath))) {
|
||
fs.mkdirSync(path.dirname(outputImagePath), { recursive: true });
|
||
}
|
||
console.log(`📦 Saving image to ${outputImagePath}...`);
|
||
execSync(`docker save -o ${outputImagePath} ${imageBase}:${name}`, {
|
||
stdio: "inherit",
|
||
});
|
||
}
|
||
|
||
function copyConfiguration(
|
||
projectDir: string,
|
||
imageBase: string,
|
||
name: string,
|
||
pwd: string
|
||
): void {
|
||
const configDir = path.join(projectDir, "configuration");
|
||
if (!fs.existsSync(configDir)) {
|
||
console.info(
|
||
`项目目录中没有 configurations 文件夹:${configDir}, 跳过配置文件复制步骤`
|
||
);
|
||
return;
|
||
}
|
||
|
||
const distConfigDir = path.join(pwd, "dist", "server", imageBase, "configuration");
|
||
if (!fs.existsSync(distConfigDir)) {
|
||
fs.mkdirSync(distConfigDir, { recursive: true });
|
||
}
|
||
|
||
const configFiles = fs
|
||
.readdirSync(configDir)
|
||
.filter((file) => file.endsWith(".json"));
|
||
|
||
for (const file of configFiles) {
|
||
const srcPath = path.join(configDir, file);
|
||
const destPath = path.join(distConfigDir, file);
|
||
console.log(`复制配置文件 ${srcPath} 到 ${destPath}...`);
|
||
fs.copyFileSync(srcPath, destPath);
|
||
}
|
||
}
|
||
|
||
function writeStartCommandScript(
|
||
projectDir: string,
|
||
imageBase: string,
|
||
name: string,
|
||
pwd: string
|
||
): void {
|
||
const distDir = path.join(pwd, "dist", "server", imageBase);
|
||
if (!fs.existsSync(distDir)) {
|
||
fs.mkdirSync(distDir, { recursive: true });
|
||
}
|
||
|
||
const scriptPath = path.join(distDir, "start.sh");
|
||
const scriptContent = `#!/bin/sh
|
||
|
||
docker load -i ${imageBase}-${name}.tar
|
||
|
||
docker run -d \\
|
||
--name ${imageBase}-${name} \\
|
||
-v $(pwd)/configuration:/app/${imageBase}/configuration \\
|
||
-p 3001:3001 \\
|
||
-p 8080:8080 \\
|
||
--restart=always \\
|
||
${imageBase}:${name} \\
|
||
pm2-runtime start pm2.dev.json
|
||
`;
|
||
fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
|
||
console.log(`✅ 已生成启动脚本: ${scriptPath}`);
|
||
}
|
||
|
||
/**
|
||
* 主函数
|
||
*/
|
||
function main(): void {
|
||
const args = parseCliArgs();
|
||
if (!args) {
|
||
process.exit(0);
|
||
}
|
||
|
||
const { projectDir, options } = args;
|
||
const pwd = process.cwd();
|
||
|
||
console.log(`-> 配置信息:`);
|
||
console.log(` 项目目录: ${projectDir}`);
|
||
console.log(` 环境: ${options.env}`);
|
||
console.log(` 构建类型: ${options.buildType}`);
|
||
console.log(
|
||
` 额外依赖: ${options.extra.length > 0 ? options.extra.join(", ") : "无"}`
|
||
);
|
||
console.log(` npm 镜像站: ${options.registry}`);
|
||
console.log(` 端口: ${options.port}`);
|
||
console.log(` Node.js 版本: ${options.nodeVersion}`);
|
||
|
||
if (!fs.existsSync(projectDir)) {
|
||
console.error(`❌ 项目目录不存在:${projectDir}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
switch (options.buildType) {
|
||
case "backend":
|
||
packageBackend(projectDir, options);
|
||
break;
|
||
case "web":
|
||
packageWeb(projectDir, options);
|
||
break;
|
||
case "init":
|
||
packageInit(projectDir, options);
|
||
break;
|
||
default:
|
||
console.error(`❌ 不支持的构建类型: ${options.buildType}`);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
function packageBackend(projectDir: string, options: BuildOptions): void {
|
||
const imageBase = path.basename(projectDir);
|
||
const projectName = path.basename(projectDir);
|
||
|
||
|
||
const projectPackageMap: { [key: string]: any } = {};
|
||
|
||
|
||
const allProjects = [projectName];
|
||
const { extra } = options;
|
||
allProjects.push(...extra);
|
||
|
||
for (const project of allProjects) {
|
||
const packageJsonPath = path.join(pwd, project, "package.json");
|
||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
||
projectPackageMap[project] = packageJson;
|
||
}
|
||
|
||
// 生成 Dockerfile 和 .dockerignore
|
||
const dockerfileContent = generateDockerfile(pwd, projectName, options, allProjects, projectPackageMap);
|
||
const dockerignoreContent = generateDockerignore();
|
||
|
||
fs.writeFileSync(path.join(pwd, "Dockerfile"), dockerfileContent);
|
||
fs.writeFileSync(path.join(pwd, ".dockerignore"), dockerignoreContent);
|
||
|
||
console.log("\n 已生成 Dockerfile 和 .dockerignore");
|
||
|
||
try {
|
||
// if (!skipConfigReplace) {
|
||
// // 循环打包每个配置
|
||
// const configFiles = fs
|
||
// .readdirSync(configDir)
|
||
// .filter((file) => file.endsWith(".json"));
|
||
|
||
// for (const file of configFiles) {
|
||
// const configPath = path.join(configDir, file);
|
||
// const name = path.basename(file, ".json") + `-${options.env}`;
|
||
|
||
// console.log(`\n🔧 使用配置 ${configPath} 构建镜像 ${name}...`);
|
||
|
||
// // 拷贝 mysql.json
|
||
// fs.copyFileSync(
|
||
// configPath,
|
||
// path.join(projectDir, "configuration/mysql.json")
|
||
// );
|
||
|
||
// buildDockerImage(imageBase, name, pwd);
|
||
// saveDockerImage(imageBase, name, pwd);
|
||
// }
|
||
// } else {
|
||
// 只构建一个默认镜像
|
||
|
||
const name = projectPackageMap[projectName].version || "latest";
|
||
console.log(`\n构建镜像: ${projectName}:${name}`);
|
||
buildDockerImage(imageBase, name, pwd);
|
||
saveDockerImage(imageBase, name, pwd);
|
||
copyConfiguration(projectDir, imageBase, name, pwd);
|
||
writeStartCommandScript(projectDir, imageBase, name, pwd);
|
||
|
||
console.log("\n镜像构建成功!");
|
||
} finally {
|
||
// 清理临时文件
|
||
fs.rmSync(path.join(pwd, "Dockerfile"), { force: true });
|
||
fs.rmSync(path.join(pwd, ".dockerignore"), { force: true });
|
||
}
|
||
}
|
||
|
||
function packageWeb(projectDir: string, options: BuildOptions): void {
|
||
// Web 项目的打包逻辑:
|
||
// 1. 进入项目目录,执行 npm install 和 npm run build:web
|
||
// 2. 将web/build目录下的内容压缩后复制到pwd/dist目录下
|
||
console.log(`\n🔧 打包 Web 项目...`);
|
||
const distDir = path.join(pwd, "dist");
|
||
if (!fs.existsSync(distDir)) {
|
||
fs.mkdirSync(distDir);
|
||
}
|
||
|
||
// 进入项目目录执行构建命令
|
||
execSync(`npm install`, {
|
||
stdio: "inherit",
|
||
cwd: projectDir,
|
||
});
|
||
execSync(`npm run build:${options.webSource}${options.env === 'staging' ? ':staging' : ''}`, {
|
||
stdio: "inherit",
|
||
cwd: projectDir,
|
||
});
|
||
const webDistDir = path.join(projectDir, options.webSource, "build");
|
||
if (!fs.existsSync(webDistDir)) {
|
||
console.error(`❌ 构建失败,找不到 Web 构建产物目录: ${webDistDir}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
const projectName = path.basename(projectDir);
|
||
|
||
// 复制构建产物到 dist 目录
|
||
const outputWebDir = path.join(distDir, "html", projectName, options.webSource);
|
||
|
||
if (!fs.existsSync(path.join(distDir, "html", projectName))) {
|
||
fs.mkdirSync(path.join(distDir, "html", projectName));
|
||
}
|
||
|
||
fs.rmSync(outputWebDir, { recursive: true, force: true });
|
||
fs.mkdirSync(outputWebDir);
|
||
execSync(`cp -r ${webDistDir}/* ${outputWebDir}/`, {
|
||
stdio: "inherit",
|
||
});
|
||
|
||
console.log(`✅ Web 项目打包成功,产物位于: ${outputWebDir}`);
|
||
}
|
||
|
||
function packageInit(projectDir: string, options: BuildOptions): void {
|
||
// 打包初始化脚本,也是生成dockerfile,但是内容不一样
|
||
const imageBase = path.basename(projectDir);
|
||
const projectName = path.basename(projectDir);
|
||
|
||
// 生成 Dockerfile 和 .dockerignore
|
||
const dockerfileContent = generateInitDockerfile(pwd, projectName, options);
|
||
const dockerignoreContent = generateInitDockerignore();
|
||
|
||
fs.writeFileSync(path.join(pwd, "Dockerfile"), dockerfileContent);
|
||
fs.writeFileSync(path.join(pwd, ".dockerignore"), dockerignoreContent);
|
||
|
||
console.log("\n 已生成 Dockerfile 和 .dockerignore");
|
||
|
||
try {
|
||
const name = `init-${options.env}`;
|
||
console.log(`\n🔧 构建初始化镜像 ${name}...`);
|
||
buildDockerImage(imageBase, name, pwd);
|
||
saveDockerImage(imageBase, name, pwd);
|
||
|
||
console.log("\n 初始化镜像构建成功!");
|
||
} finally {
|
||
// 清理临时文件
|
||
fs.rmSync(path.join(pwd, "Dockerfile"), { force: true });
|
||
fs.rmSync(path.join(pwd, ".dockerignore"), { force: true });
|
||
}
|
||
}
|
||
|
||
// 执行主函数
|
||
main();
|