#!/usr/bin/env node // src/cli.ts import { execSync } from "child_process"; import fs from "fs"; import path from "path"; import { parseArgs } from "util"; var pwd = process.cwd(); try { execSync("docker --version", { stdio: "ignore" }); } catch (error) { console.error("\u274C Docker \u547D\u4EE4\u4E0D\u53EF\u7528\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5\u5E76\u6B63\u786E\u914D\u7F6E Docker"); process.exit(1); } function parseCliArgs() { 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" }, "node-version": { type: "string", default: "20" } }, allowPositionals: true }); if (values.help) { console.log(` \u4F7F\u7528\u65B9\u6CD5: docker-builder <\u9879\u76EE\u76EE\u5F55> [\u9009\u9879] \u53C2\u6570: <\u9879\u76EE\u76EE\u5F55> \u8981\u6253\u5305\u7684\u9879\u76EE\u76EE\u5F55\uFF08\u5FC5\u9700\uFF09 \u9009\u9879: --env= \u73AF\u5883\u7C7B\u578B (dev|prod|staging)\uFF0C\u9ED8\u8BA4: dev --extra= \u989D\u5916\u7684\u6A21\u578B\u76EE\u5F55\uFF08\u53EF\u591A\u6B21\u4F7F\u7528\uFF09 --registry= npm \u955C\u50CF\u7AD9\u5730\u5740\uFF0C\u9ED8\u8BA4: https://registry.npmmirror.com --port= \u66B4\u9732\u7684\u7AEF\u53E3\u53F7\uFF0C\u9ED8\u8BA4: 3001 --node-version= Node.js \u7248\u672C\uFF0C\u9ED8\u8BA4: 20 -h, --help \u663E\u793A\u6B64\u5E2E\u52A9\u4FE1\u606F \u793A\u4F8B: 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("\u274C \u8BF7\u6307\u5B9A\u8981\u6253\u5305\u7684\u9879\u76EE\u76EE\u5F55"); console.error(" \u4F7F\u7528 --help \u67E5\u770B\u8BE6\u7EC6\u7528\u6CD5"); process.exit(1); } const projectDir = path.resolve(positionals[0]); const env = values.env; if (!["dev", "prod", "staging"].includes(env)) { console.error(`\u274C \u65E0\u6548\u7684\u73AF\u5883\u7C7B\u578B: ${env}`); console.error(" \u652F\u6301\u7684\u73AF\u5883: dev, prod, staging"); process.exit(1); } return { projectDir, options: { env, extra: values.extra, registry: values.registry, port: values.port, nodeVersion: values["node-version"] } }; } function generateDockerfile(projectName, options) { const { extra, env, registry, port, nodeVersion } = options; const allProjects = [projectName]; allProjects.push(...extra); console.log("\n\u{1F50D} \u68C0\u67E5\u9879\u76EE\u4F9D\u8D56\u5173\u7CFB..."); for (const project of allProjects) { console.log(`\u68C0\u67E5\u9879\u76EE: ${project}`); const packageJsonPath = path.join(pwd, project, "package.json"); if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); const dependencies = packageJson.dependencies || {}; for (const dep in dependencies) { if (dependencies[dep].startsWith("file:../")) { const depProject = dependencies[dep].replace("file:../", ""); if (!allProjects.includes(depProject)) { console.error(`\u274C \u53D1\u73B0\u672A\u5305\u542B\u7684\u4F9D\u8D56\u9879\u76EE: ${depProject}\uFF0C\u8BF7\u5728\u547D\u4EE4\u884C\u4E2D\u901A\u8FC7 --extra \u53C2\u6570\u6DFB\u52A0`); process.exit(1); } } } } else { console.error(`\u274C \u627E\u4E0D\u5230\u9879\u76EE\u7684 package.json: ${packageJsonPath}`); } } let dockerfile = `# =========================== # Stage 1: Build # =========================== FROM node:${nodeVersion}-alpine AS builder WORKDIR /app # \u8BBE\u7F6E\u955C\u50CF\u7AD9 RUN npm config set registry ${registry} `; for (const project of allProjects) { dockerfile += `# \u590D\u5236 ${project} `; dockerfile += `COPY ./${project} ./${project} `; } dockerfile += ` # =========================== # \u6784\u5EFA\u6240\u6709\u9879\u76EE # =========================== `; for (const project of allProjects) { dockerfile += ` # \u6784\u5EFA ${project} WORKDIR /app/${project} RUN npm install `; if (project === projectName) { dockerfile += `RUN npm run make:domain && npm run build `; } else { dockerfile += `RUN npm run build || true `; } } dockerfile += ` # =========================== # \u6E05\u7406\u6784\u5EFA\u4EA7\u7269 # =========================== `; for (const project of allProjects) { dockerfile += ` # \u6E05\u7406 ${project} WORKDIR /app/${project} RUN rm -rf src \\ && find node_modules -type d -name "test" -o -name "__tests__" -exec rm -rf {} + \\ && find node_modules -type f \\( -name "*.ts" -o -name "*.d.ts" \\) -delete `; } dockerfile += ` # =========================== # Stage 2: Runtime # =========================== FROM node:${nodeVersion}-alpine WORKDIR /app # \u4ECE\u6784\u5EFA\u9636\u6BB5\u590D\u5236\u6240\u6709\u5185\u5BB9 COPY --from=builder /app ./ WORKDIR /app/${projectName} # \u5B89\u88C5 PM2 RUN npm install -g @socket.io/pm2 # \u518D\u6B21\u6E05\u7406\uFF08\u786E\u4FDD\u8FD0\u884C\u65F6\u955C\u50CF\u6700\u5C0F\u5316\uFF09 `; for (const project of allProjects) { dockerfile += ` # \u6E05\u7406 ${project} \u8FD0\u884C\u65F6\u4E0D\u9700\u8981\u7684\u6587\u4EF6 WORKDIR /app/${project} RUN rm -rf src \\ && find node_modules -type d -name "test" -o -name "__tests__" -exec rm -rf {} + \\ && find node_modules -type f \\( -name "*.ts" -o -name "*.d.ts" -o -name "*.map" \\) -delete \\ && find . -type f -name "*.test.js" -delete `; } dockerfile += ` # \u5207\u6362\u56DE\u4E3B\u9879\u76EE\u76EE\u5F55 WORKDIR /app/${projectName} EXPOSE ${port} CMD ["pm2-runtime", "start", "pm2.${env}.json"] `; return dockerfile; } function generateDockerignore() { return `# Node modules node_modules npm-debug.log # \u6D4B\u8BD5\u548C\u6587\u6863 test tests __tests__ *.test.js *.spec.js docs coverage # \u5F00\u53D1\u6587\u4EF6 .git .gitignore .env .env.* *.md .vscode .idea # \u6784\u5EFA\u4EA7\u7269\uFF08\u5728\u5BB9\u5668\u5185\u6784\u5EFA\uFF09 dist build # \u4E34\u65F6\u6587\u4EF6 *.log *.tmp .DS_Store `; } function buildDockerImage(imageBase, name, pwd2) { console.log(` \u{1F527} Building image for ${name}...`); execSync(`docker build -t ${imageBase}:${name} ${pwd2}`, { stdio: "inherit" }); } function saveDockerImage(imageBase, name, pwd2) { const distDir = path.join(pwd2, "dist"); if (!fs.existsSync(distDir)) { fs.mkdirSync(distDir); } const outputImagePath = path.join(distDir, `${imageBase}-${name}.tar`); console.log(`\u{1F4E6} Saving image to ${outputImagePath}...`); execSync(`docker save -o ${outputImagePath} ${imageBase}:${name}`, { stdio: "inherit" }); } function main() { const args = parseCliArgs(); if (!args) { process.exit(0); } const { projectDir, options } = args; const pwd2 = process.cwd(); console.log(`-> \u914D\u7F6E\u4FE1\u606F:`); console.log(` \u9879\u76EE\u76EE\u5F55: ${projectDir}`); console.log(` \u73AF\u5883: ${options.env}`); console.log( ` \u989D\u5916\u6A21\u578B: ${options.extra.length > 0 ? options.extra.join(", ") : "\u65E0"}` ); console.log(` npm \u955C\u50CF\u7AD9: ${options.registry}`); console.log(` \u7AEF\u53E3: ${options.port}`); console.log(` Node.js \u7248\u672C: ${options.nodeVersion}`); if (!fs.existsSync(projectDir)) { console.error(`\u274C \u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${projectDir}`); process.exit(1); } let skipConfigReplace = false; const configDir = path.join(projectDir, "configurations"); if (!fs.existsSync(configDir)) { console.info( `\u9879\u76EE\u76EE\u5F55\u4E2D\u6CA1\u6709 configurations \u6587\u4EF6\u5939\uFF1A${configDir}, \u8DF3\u8FC7\u914D\u7F6E\u66FF\u6362\u6B65\u9AA4` ); skipConfigReplace = true; } const imageBase = path.basename(projectDir); const projectName = path.basename(projectDir); const dockerfileContent = generateDockerfile(projectName, options); const dockerignoreContent = generateDockerignore(); fs.writeFileSync(path.join(pwd2, "Dockerfile"), dockerfileContent); fs.writeFileSync(path.join(pwd2, ".dockerignore"), dockerignoreContent); console.log("\n\u{1F4DD} \u5DF2\u751F\u6210 Dockerfile \u548C .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(` \u{1F527} \u4F7F\u7528\u914D\u7F6E ${configPath} \u6784\u5EFA\u955C\u50CF ${name}...`); fs.copyFileSync( configPath, path.join(projectDir, "configuration/mysql.json") ); buildDockerImage(imageBase, name, pwd2); saveDockerImage(imageBase, name, pwd2); } } else { const name = `default-${options.env}`; console.log(` \u{1F527} \u6784\u5EFA\u9ED8\u8BA4\u955C\u50CF ${name}...`); buildDockerImage(imageBase, name, pwd2); saveDockerImage(imageBase, name, pwd2); } console.log("\n\u2705 \u6240\u6709\u955C\u50CF\u6784\u5EFA\u6210\u529F\uFF01"); } finally { fs.rmSync(path.join(projectDir, "configuration/mysql.json"), { force: true }); fs.rmSync(path.join(pwd2, "Dockerfile"), { force: true }); fs.rmSync(path.join(pwd2, ".dockerignore"), { force: true }); } } main();