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= 环境类型 (dev|prod|staging),默认: dev --extra= 额外的依赖目录(可多次使用) --registry= npm 镜像站地址,默认: https://registry.npmmirror.com --port= 暴露的端口号,默认: 3001 --node-version= Node.js 版本,默认: 20 --build-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();