oak-docker-tools/src/cli.ts

416 lines
13 KiB
TypeScript
Raw 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 { 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();