From b3ebaa767fbe30b874cc029eae26568e390d2ca6 Mon Sep 17 00:00:00 2001 From: "qcqcqc@wsl" Date: Thu, 30 Oct 2025 11:24:57 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=B8=8D=E9=9C=80=E8=A6=81=E6=89=93?= =?UTF-8?q?=E5=8C=85=E8=BF=87=E5=A4=9A=E9=95=9C=E5=83=8F=EF=BC=8C=E8=80=8C?= =?UTF-8?q?=E6=98=AF=E5=9C=A8=E5=AE=B9=E5=99=A8=E5=90=AF=E5=8A=A8=E6=97=B6?= =?UTF-8?q?=E6=89=8B=E5=8A=A8=E4=BC=A0=E5=85=A5=E5=91=BD=E4=BB=A4=20feat:?= =?UTF-8?q?=20=E6=94=AF=E6=8C=81=E4=BA=86=E4=BE=9D=E8=B5=96=E5=85=B3?= =?UTF-8?q?=E7=B3=BB=E6=A3=80=E6=9F=A5=EF=BC=8C=E9=81=BF=E5=85=8D=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/cli.js | 706 ++++++++++++++++++++++++++++------------ src/cli.ts | 387 +++++++++------------- src/template/init.ts | 204 ++++++++++++ src/template/runtime.ts | 235 +++++++++++++ src/types.ts | 14 + 5 files changed, 1091 insertions(+), 455 deletions(-) create mode 100644 src/template/init.ts create mode 100644 src/template/runtime.ts create mode 100644 src/types.ts diff --git a/dist/cli.js b/dist/cli.js index f6c83b8..93e4074 100644 --- a/dist/cli.js +++ b/dist/cli.js @@ -2,9 +2,383 @@ // src/cli.ts import { execSync } from "child_process"; +import fs3 from "fs"; +import path3 from "path"; +import { parseArgs } from "util"; + +// src/template/runtime.ts import fs from "fs"; import path from "path"; -import { parseArgs } from "util"; +function generateDockerfile(pwd2, projectName, options, allProjects, projectPackageMap) { + const { extra, env, registry, port, nodeVersion } = options; + console.log("\n-> \u68C0\u67E5\u9879\u76EE\u4F9D\u8D56\u5173\u7CFB..."); + const dependencyGraph = {}; + for (const project of allProjects) { + dependencyGraph[project] = /* @__PURE__ */ new Set(); + } + for (const project of allProjects) { + console.log(`\u68C0\u67E5\u9879\u76EE: ${project}`); + const packageJsonPath = path.join(pwd2, project, "package.json"); + if (!fs.existsSync(packageJsonPath)) { + console.error(`\u627E\u4E0D\u5230\u9879\u76EE\u7684 package.json: ${packageJsonPath}`); + continue; + } + const packageJson = projectPackageMap[project]; + const dependencies = packageJson.dependencies || {}; + for (const dep in dependencies) { + if (dependencies[dep].startsWith("file:")) { + const depProjectPath = dependencies[dep].replace("file:", ""); + const targetPath = path.join(pwd2, project, depProjectPath); + const depProject = path.basename(targetPath); + console.log(` \u53D1\u73B0\u672C\u5730\u4F9D\u8D56: ${depProject} (\u6765\u81EA ${project})`); + if (!allProjects.includes(depProject)) { + console.error(`\u672A\u5305\u542B\u7684\u4F9D\u8D56\u9879\u76EE: ${depProject}\uFF0C\u8BF7\u901A\u8FC7 --extra \u53C2\u6570\u6DFB\u52A0`); + process.exit(1); + } + dependencyGraph[project].add(depProject); + } + } + } + function topoSort(graph) { + const inDegree = {}; + const result = []; + for (const p in graph) + inDegree[p] = 0; + for (const p in graph) { + for (const dep of graph[p]) { + inDegree[dep] = (inDegree[dep] || 0) + 1; + } + } + const queue = Object.keys(inDegree).filter((p) => inDegree[p] === 0); + while (queue.length > 0) { + const current = queue.shift(); + result.push(current); + for (const dep of graph[current]) { + inDegree[dep]--; + if (inDegree[dep] === 0) + queue.push(dep); + } + } + if (result.length !== Object.keys(graph).length) { + console.error("\u68C0\u6D4B\u5230\u5FAA\u73AF\u4F9D\u8D56\uFF0C\u8BF7\u68C0\u67E5\u9879\u76EE\u95F4\u7684 file: \u5F15\u7528"); + process.exit(1); + } + return result; + } + const sortedProjects = topoSort(dependencyGraph).reverse(); + console.log("\n\u6784\u5EFA\u987A\u5E8F\u5982\u4E0B\uFF1A"); + sortedProjects.forEach((p, i) => console.log(` ${i + 1}. ${p}`)); + const mainIndex = sortedProjects.indexOf(projectName); + if (mainIndex !== -1 && mainIndex !== sortedProjects.length - 1) { + sortedProjects.splice(mainIndex, 1); + sortedProjects.push(projectName); + } + 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 sortedProjects) { + dockerfile += `# \u590D\u5236 ${project} +`; + dockerfile += `COPY ./${project} ./${project} +`; + } + dockerfile += ` +# =========================== +# \u6784\u5EFA\u6240\u6709\u9879\u76EE +# =========================== +`; + for (const project of sortedProjects) { + dockerfile += ` +# \u6784\u5EFA ${project} +WORKDIR /app/${project} +RUN npm install +`; + const pkjson = projectPackageMap[project]; + if (project === projectName) { + if (pkjson.scripts?.["make:domain"]) + dockerfile += `RUN npm run make:domain +`; + if (pkjson.scripts?.["build"]) + dockerfile += `RUN npm run build +`; + if (pkjson.scripts?.["confuse"]) + dockerfile += `RUN npm run confuse +`; + } else { + dockerfile += `RUN npm run build || true +`; + } + } + dockerfile += ` +# =========================== +# \u6E05\u7406\u6784\u5EFA\u4EA7\u7269 +# =========================== +`; + for (const project of sortedProjects) { + dockerfile += ` +# \u6E05\u7406 ${project} +WORKDIR /app/${project} +RUN rm -rf src && rm -rf web && rm -rf wechatMp && rm -rf native && rm -rf */**/*.d.ts \\ + && rm -rf */**.ts && rm -rf .git && rm -rf .gitignore && rm -rf .husky && rm -rf configurations && rm -rf upgrade \\ + && 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 \\ + && find node_modules -type f \\( -name "*.map" \\) -delete \\ + && find node_modules -type f -name "LICENSE" -o -name "LICENSE.txt" -delete \\ + && find node_modules -type f -name "*.md" -o -name "*.markdown" -o -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.gif" -delete +`; + } + dockerfile += ` +# =========================== +# Stage 2: Runtime +# =========================== +FROM node:${nodeVersion}-alpine + +WORKDIR /app + +COPY --from=builder /app ./ + +WORKDIR /app/${projectName} + +RUN npm install -g @socket.io/pm2 +`; + for (const project of sortedProjects) { + 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 += ` +WORKDIR /app/${projectName} +EXPOSE ${port.join(" ")} +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 +`; +} + +// src/template/init.ts +import fs2 from "fs"; +import path2 from "path"; +function generateInitDockerfile(pwd2, 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..."); + const projectPackageMap = {}; + for (const project of allProjects) { + console.log(`\u68C0\u67E5\u9879\u76EE: ${project}`); + const packageJsonPath = path2.join(pwd2, project, "package.json"); + if (fs2.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf-8")); + projectPackageMap[project] = packageJson; + const dependencies = packageJson.dependencies || {}; + for (const dep in dependencies) { + if (dependencies[dep].startsWith("file:")) { + const depProjectPath = dependencies[dep].replace("file:", ""); + const targetPath = path2.join(pwd2, project, depProjectPath); + const depProject = path2.basename(targetPath); + console.log(` \u53D1\u73B0\u672C\u5730\u4F9D\u8D56: ${depProject} (\u6765\u81EA ${project})`); + 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) { + const pkjson = projectPackageMap[project]; + if (pkjson.scripts && pkjson.scripts["make:domain"]) { + dockerfile += `RUN npm run make:domain +`; + } + if (pkjson.scripts && pkjson.scripts["build"]) { + dockerfile += `RUN npm run build +`; + } + if (pkjson.scripts && pkjson.scripts["confuse"]) { + dockerfile += `RUN npm run confuse +`; + } + } 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 && rm -rf web && rm -rf wechatMp && rm -rf native && rm -rf */**/*.d.ts \\ + && rm -rf */**.ts && rm -rf .git && rm -rf .gitignore && rm -rf .husky && rm -rf configurations && rm -rf upgrade \\ + && 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 \\ + && find node_modules -type f \\( -name "*.map" \\) -delete \\ + && find node_modules -type f -name "LICENSE" -o -name "LICENSE.txt" -delete \\ + && find node_modules -type f -name "*.md" -o -name "*.markdown" -o -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.gif" -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} + +# \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 +`; + } + for (const project of allProjects) { + dockerfile += ` +# \u6E05\u7406 ${project} \u8FD0\u884C\u65F6\u4E0D\u9700\u8981\u7684\u6587\u4EF6\u5939\u548C\u6587\u4EF6 +WORKDIR /app/${project} +RUN set -eux; \\ + echo "Cleaning ${project} ..."; \\ + # \u5220\u9664\u4E00\u7EA7\u76EE\u5F55\uFF08\u4FDD\u7559 lib\u3001data\u3001node_modules\u3001scripts\u3001configuration\uFF09 + find . -mindepth 1 -maxdepth 1 -type d ! -name "lib" ! -name "data" ! -name "node_modules" ! -name "scripts" ! -name "configuration" -exec rm -rf {} +; \\ + # \u5220\u9664\u4E00\u7EA7\u6587\u4EF6\uFF08\u4FDD\u7559 package.json\uFF09 + find . -mindepth 1 -maxdepth 1 -type f ! -name "package.json" -exec rm -f {} +; \\ + # \u6E05\u7406 lib \u4E0B\u591A\u4F59\u76EE\u5F55\uFF08\u4FDD\u7559 oak-app-domain\u3001routines\u3001data\u3001context\u3001config\u3001configuration\u3001 utils\u3001types\uFF09 + if [ -d "lib" ]; then \\ + echo "pass"; \\ + fi +`; + } + dockerfile += ` +# \u5207\u6362\u56DE\u4E3B\u9879\u76EE\u76EE\u5F55 +WORKDIR /app/${projectName} +`; + return dockerfile; +} +function generateInitDockerignore() { + 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 +`; +} + +// src/cli.ts var pwd = process.cwd(); try { execSync("docker --version", { stdio: "ignore" }); @@ -12,6 +386,10 @@ try { 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); } +if (process.platform !== "linux" && process.platform !== "darwin") { + console.error("\u274C \u76EE\u524D\u4EC5\u652F\u6301 Linux \u548C macOS \u7CFB\u7EDF"); + process.exit(1); +} function parseCliArgs() { const { values, positionals } = parseArgs({ options: { @@ -82,14 +460,14 @@ function parseCliArgs() { console.error(" \u4F7F\u7528 --help \u67E5\u770B\u8BE6\u7EC6\u7528\u6CD5"); process.exit(1); } - const projectDir = path.resolve(positionals[0]); + const projectDir = path3.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); } - if (!["native", "wechatMp", "web", "backend"].includes(values["build-type"])) { + if (!["native", "wechatMp", "web", "backend", "init"].includes(values["build-type"])) { console.error(`\u274C \u65E0\u6548\u7684\u6784\u5EFA\u7C7B\u578B: ${values["build-type"]}`); console.error(" \u652F\u6301\u7684\u7C7B\u578B: native, wechatMp, web, backend"); process.exit(1); @@ -107,164 +485,6 @@ function parseCliArgs() { } }; } -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..."); - const projectPackageMap = {}; - 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")); - projectPackageMap[project] = packageJson; - 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) { - const pkjson = projectPackageMap[project]; - if (pkjson.scripts && pkjson.scripts["make:domain"]) { - dockerfile += `RUN npm run make:domain -`; - } - if (pkjson.scripts && pkjson.scripts["build"]) { - dockerfile += `RUN npm run build -`; - } - if (pkjson.scripts && pkjson.scripts["confuse"]) { - dockerfile += `RUN npm run confuse -`; - } - } 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 && rm -rf web && rm -rf wechatMp && rm -rf native \\ - && 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.join(" ")} - -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}...`); @@ -273,16 +493,61 @@ function buildDockerImage(imageBase, name, pwd2) { }); } function saveDockerImage(imageBase, name, pwd2) { - const distDir = path.join(pwd2, "dist"); - if (!fs.existsSync(distDir)) { - fs.mkdirSync(distDir); + const distDir = path3.join(pwd2, "dist"); + if (!fs3.existsSync(distDir)) { + fs3.mkdirSync(distDir); + } + const outputImagePath = path3.join(distDir, "server", imageBase, `${imageBase}-${name}.tar`); + if (!fs3.existsSync(path3.dirname(outputImagePath))) { + fs3.mkdirSync(path3.dirname(outputImagePath), { recursive: true }); } - 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 copyConfiguration(projectDir, imageBase, name, pwd2) { + const configDir = path3.join(projectDir, "configuration"); + if (!fs3.existsSync(configDir)) { + console.info( + `\u9879\u76EE\u76EE\u5F55\u4E2D\u6CA1\u6709 configurations \u6587\u4EF6\u5939\uFF1A${configDir}, \u8DF3\u8FC7\u914D\u7F6E\u6587\u4EF6\u590D\u5236\u6B65\u9AA4` + ); + return; + } + const distConfigDir = path3.join(pwd2, "dist", "server", imageBase, "configuration"); + if (!fs3.existsSync(distConfigDir)) { + fs3.mkdirSync(distConfigDir, { recursive: true }); + } + const configFiles = fs3.readdirSync(configDir).filter((file) => file.endsWith(".json")); + for (const file of configFiles) { + const srcPath = path3.join(configDir, file); + const destPath = path3.join(distConfigDir, file); + console.log(`\u590D\u5236\u914D\u7F6E\u6587\u4EF6 ${srcPath} \u5230 ${destPath}...`); + fs3.copyFileSync(srcPath, destPath); + } +} +function writeStartCommandScript(projectDir, imageBase, name, pwd2) { + const distDir = path3.join(pwd2, "dist", "server", imageBase); + if (!fs3.existsSync(distDir)) { + fs3.mkdirSync(distDir, { recursive: true }); + } + const scriptPath = path3.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 +`; + fs3.writeFileSync(scriptPath, scriptContent, { mode: 493 }); + console.log(`\u2705 \u5DF2\u751F\u6210\u542F\u52A8\u811A\u672C: ${scriptPath}`); +} function main() { const args = parseCliArgs(); if (!args) { @@ -300,7 +565,7 @@ function main() { 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)) { + if (!fs3.existsSync(projectDir)) { console.error(`\u274C \u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${projectDir}`); process.exit(1); } @@ -311,64 +576,51 @@ function main() { case "web": packageWeb(projectDir, options); break; + case "init": + packageInit(projectDir, options); + break; default: console.error(`\u274C \u4E0D\u652F\u6301\u7684\u6784\u5EFA\u7C7B\u578B: ${options.buildType}`); process.exit(1); } } function packageBackend(projectDir, options) { - 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 = path3.basename(projectDir); + const projectName = path3.basename(projectDir); + const projectPackageMap = {}; + const allProjects = [projectName]; + const { extra } = options; + allProjects.push(...extra); + for (const project of allProjects) { + const packageJsonPath = path3.join(pwd, project, "package.json"); + const packageJson = JSON.parse(fs3.readFileSync(packageJsonPath, "utf-8")); + projectPackageMap[project] = packageJson; } - const imageBase = path.basename(projectDir); - const projectName = path.basename(projectDir); - const dockerfileContent = generateDockerfile(projectName, options); + 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\u{1F4DD} \u5DF2\u751F\u6210 Dockerfile \u548C .dockerignore"); + fs3.writeFileSync(path3.join(pwd, "Dockerfile"), dockerfileContent); + fs3.writeFileSync(path3.join(pwd, ".dockerignore"), dockerignoreContent); + console.log("\n \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, pwd); - saveDockerImage(imageBase, name, pwd); - } - } else { - const name = `default-${options.env}`; - console.log(` -\u{1F527} \u6784\u5EFA\u9ED8\u8BA4\u955C\u50CF ${name}...`); - buildDockerImage(imageBase, name, pwd); - saveDockerImage(imageBase, name, pwd); - } - console.log("\n\u2705 \u6240\u6709\u955C\u50CF\u6784\u5EFA\u6210\u529F\uFF01"); + const name = projectPackageMap[projectName].version || "latest"; + console.log(` +\u6784\u5EFA\u955C\u50CF: ${projectName}:${name}`); + buildDockerImage(imageBase, name, pwd); + saveDockerImage(imageBase, name, pwd); + copyConfiguration(projectDir, imageBase, name, pwd); + writeStartCommandScript(projectDir, imageBase, name, pwd); + console.log("\n\u955C\u50CF\u6784\u5EFA\u6210\u529F\uFF01"); } finally { - fs.rmSync(path.join(projectDir, "configuration/mysql.json"), { - force: true - }); - fs.rmSync(path.join(pwd, "Dockerfile"), { force: true }); - fs.rmSync(path.join(pwd, ".dockerignore"), { force: true }); + fs3.rmSync(path3.join(pwd, "Dockerfile"), { force: true }); + fs3.rmSync(path3.join(pwd, ".dockerignore"), { force: true }); } } function packageWeb(projectDir, options) { console.log(` \u{1F527} \u6253\u5305 Web \u9879\u76EE...`); - const distDir = path.join(pwd, "dist"); - if (!fs.existsSync(distDir)) { - fs.mkdirSync(distDir); + const distDir = path3.join(pwd, "dist"); + if (!fs3.existsSync(distDir)) { + fs3.mkdirSync(distDir); } execSync(`npm install`, { stdio: "inherit", @@ -378,21 +630,41 @@ function packageWeb(projectDir, options) { stdio: "inherit", cwd: projectDir }); - const webDistDir = path.join(projectDir, options.webSource, "build"); - if (!fs.existsSync(webDistDir)) { + const webDistDir = path3.join(projectDir, options.webSource, "build"); + if (!fs3.existsSync(webDistDir)) { console.error(`\u274C \u6784\u5EFA\u5931\u8D25\uFF0C\u627E\u4E0D\u5230 Web \u6784\u5EFA\u4EA7\u7269\u76EE\u5F55: ${webDistDir}`); process.exit(1); } - const projectName = path.basename(projectDir); - const outputWebDir = path.join(distDir, "html", projectName, options.webSource); - if (!fs.existsSync(path.join(distDir, "html", projectName))) { - fs.mkdirSync(path.join(distDir, "html", projectName)); + const projectName = path3.basename(projectDir); + const outputWebDir = path3.join(distDir, "html", projectName, options.webSource); + if (!fs3.existsSync(path3.join(distDir, "html", projectName))) { + fs3.mkdirSync(path3.join(distDir, "html", projectName)); } - fs.rmSync(outputWebDir, { recursive: true, force: true }); - fs.mkdirSync(outputWebDir); + fs3.rmSync(outputWebDir, { recursive: true, force: true }); + fs3.mkdirSync(outputWebDir); execSync(`cp -r ${webDistDir}/* ${outputWebDir}/`, { stdio: "inherit" }); console.log(`\u2705 Web \u9879\u76EE\u6253\u5305\u6210\u529F\uFF0C\u4EA7\u7269\u4F4D\u4E8E: ${outputWebDir}`); } +function packageInit(projectDir, options) { + const imageBase = path3.basename(projectDir); + const projectName = path3.basename(projectDir); + const dockerfileContent = generateInitDockerfile(pwd, projectName, options); + const dockerignoreContent = generateInitDockerignore(); + fs3.writeFileSync(path3.join(pwd, "Dockerfile"), dockerfileContent); + fs3.writeFileSync(path3.join(pwd, ".dockerignore"), dockerignoreContent); + console.log("\n \u5DF2\u751F\u6210 Dockerfile \u548C .dockerignore"); + try { + const name = `init-${options.env}`; + console.log(` +\u{1F527} \u6784\u5EFA\u521D\u59CB\u5316\u955C\u50CF ${name}...`); + buildDockerImage(imageBase, name, pwd); + saveDockerImage(imageBase, name, pwd); + console.log("\n \u521D\u59CB\u5316\u955C\u50CF\u6784\u5EFA\u6210\u529F\uFF01"); + } finally { + fs3.rmSync(path3.join(pwd, "Dockerfile"), { force: true }); + fs3.rmSync(path3.join(pwd, ".dockerignore"), { force: true }); + } +} main(); diff --git a/src/cli.ts b/src/cli.ts index 75b6032..d0cfd6d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,23 +2,13 @@ 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(); -interface BuildOptions { - env: string; - extra: string[]; - registry: string; - port: string[]; - nodeVersion: string; - buildType: string; - webSource: string; -} -interface ParsedArgs { - projectDir: string; - options: BuildOptions; -} // 测试docker命令是否可用 try { @@ -28,6 +18,13 @@ try { process.exit(1); } + +// 目前只支持linux或者macOS系统 +if (process.platform !== "linux" && process.platform !== "darwin") { + console.error("❌ 目前仅支持 Linux 和 macOS 系统"); + process.exit(1); +} + /** * 解析命令行参数 */ @@ -116,7 +113,7 @@ function parseCliArgs(): ParsedArgs | null { process.exit(1); } - if (!["native", "wechatMp", "web", "backend"].includes(values["build-type"] as string)) { + 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); @@ -136,189 +133,6 @@ function parseCliArgs(): ParsedArgs | null { }; } -/** - * 生成 Dockerfile 内容 - */ -function generateDockerfile( - projectName: string, - options: BuildOptions -): string { - const { extra, env, registry, port, nodeVersion } = options; - - const allProjects = [projectName]; - allProjects.push(...extra); - - console.log("\n🔍 检查项目依赖关系..."); - // 需要遍历所有项目的package.json确定file:../这种,然后确保没有遗漏 - const projectPackageMap: { [key: string]: any } = {}; - - for (const project of allProjects) { - console.log(`检查项目: ${project}`); - const packageJsonPath = path.join(pwd, project, "package.json"); - if (fs.existsSync(packageJsonPath)) { - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); - projectPackageMap[project] = packageJson; - 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(`❌ 发现未包含的依赖项目: ${depProject},请在命令行中通过 --extra 参数添加`); - process.exit(1); - } - } - } - } else { - console.error(`❌ 找不到项目的 package.json: ${packageJsonPath}`); - } - } - - let dockerfile = `# =========================== -# Stage 1: Build -# =========================== -FROM node:${nodeVersion}-alpine AS builder - -WORKDIR /app - -# 设置镜像站 -RUN npm config set registry ${registry} - -`; - - // 复制所有项目 - for (const project of allProjects) { - dockerfile += `# 复制 ${project}\n`; - dockerfile += `COPY ./${project} ./${project}\n`; - } - - dockerfile += ` -# =========================== -# 构建所有项目 -# =========================== -`; - - // 为每个项目执行安装和构建 - for (const project of allProjects) { - dockerfile += ` -# 构建 ${project} -WORKDIR /app/${project} -RUN npm install -`; - - // 主项目需要执行 make:domain 和 build - if (project === projectName) { - const pkjson = projectPackageMap[project]; - if (pkjson.scripts && pkjson.scripts["make:domain"]) { - dockerfile += `RUN npm run make:domain\n`; - } - if (pkjson.scripts && pkjson.scripts["build"]) { - dockerfile += `RUN npm run build\n`; - } - if (pkjson.scripts && pkjson.scripts["confuse"]) { - dockerfile += `RUN npm run confuse\n`; - } - } else { - // 其他项目可能只需要 build,或者根据实际情况调整 - dockerfile += `RUN npm run build || true\n`; - } - } - - dockerfile += ` -# =========================== -# 清理构建产物 -# =========================== -`; - - // 清理所有项目的源码和测试文件 - for (const project of allProjects) { - dockerfile += ` -# 清理 ${project} -WORKDIR /app/${project} -RUN rm -rf src && rm -rf web && rm -rf wechatMp && rm -rf native \\ - && 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 - -# 从构建阶段复制所有内容 -COPY --from=builder /app ./ - -WORKDIR /app/${projectName} - -# 安装 PM2 -RUN npm install -g @socket.io/pm2 - -# 再次清理(确保运行时镜像最小化) -`; - - for (const project of allProjects) { - dockerfile += ` -# 清理 ${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" -o -name "*.map" \\) -delete \\ - && find . -type f -name "*.test.js" -delete -`; - } - - dockerfile += ` -# 切换回主项目目录 -WORKDIR /app/${projectName} - -EXPOSE ${port.join(" ")} - -CMD ["pm2-runtime", "start", "pm2.${env}.json"] -`; - - return dockerfile; -} - -/** - * 生成 .dockerignore 内容 - */ -function generateDockerignore(): string { - return `# Node modules -node_modules -npm-debug.log - -# 测试和文档 -test -tests -__tests__ -*.test.js -*.spec.js -docs -coverage - -# 开发文件 -.git -.gitignore -.env -.env.* -*.md -.vscode -.idea - -# 构建产物(在容器内构建) -dist -build - -# 临时文件 -*.log -*.tmp -.DS_Store -`; -} - /** * 构建 Docker 镜像 */ @@ -345,13 +159,76 @@ function saveDockerImage( if (!fs.existsSync(distDir)) { fs.mkdirSync(distDir); } - const outputImagePath = path.join(distDir, `${imageBase}-${name}.tar`); + 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}`); +} + /** * 主函数 */ @@ -387,71 +264,77 @@ function main(): void { 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 { - let skipConfigReplace = false; - const configDir = path.join(projectDir, "configurations"); - if (!fs.existsSync(configDir)) { - console.info( - `项目目录中没有 configurations 文件夹:${configDir}, 跳过配置替换步骤` - ); - skipConfigReplace = true; - } - 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(projectName, options); + 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"); + console.log("\n 已生成 Dockerfile 和 .dockerignore"); try { - if (!skipConfigReplace) { - // 循环打包每个配置 - const configFiles = fs - .readdirSync(configDir) - .filter((file) => file.endsWith(".json")); + // 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}`; + // for (const file of configFiles) { + // const configPath = path.join(configDir, file); + // const name = path.basename(file, ".json") + `-${options.env}`; - console.log(`\n🔧 使用配置 ${configPath} 构建镜像 ${name}...`); + // console.log(`\n🔧 使用配置 ${configPath} 构建镜像 ${name}...`); - // 拷贝 mysql.json - fs.copyFileSync( - configPath, - path.join(projectDir, "configuration/mysql.json") - ); + // // 拷贝 mysql.json + // fs.copyFileSync( + // configPath, + // path.join(projectDir, "configuration/mysql.json") + // ); - buildDockerImage(imageBase, name, pwd); - saveDockerImage(imageBase, name, pwd); - } - } else { - // 只构建一个默认镜像 - const name = `default-${options.env}`; - console.log(`\n🔧 构建默认镜像 ${name}...`); - buildDockerImage(imageBase, name, pwd); - saveDockerImage(imageBase, name, pwd); - } + // buildDockerImage(imageBase, name, pwd); + // saveDockerImage(imageBase, name, pwd); + // } + // } else { + // 只构建一个默认镜像 - console.log("\n✅ 所有镜像构建成功!"); + 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(projectDir, "configuration/mysql.json"), { - force: true, - }); fs.rmSync(path.join(pwd, "Dockerfile"), { force: true }); fs.rmSync(path.join(pwd, ".dockerignore"), { force: true }); } @@ -500,5 +383,33 @@ function packageWeb(projectDir: string, options: BuildOptions): void { 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(); diff --git a/src/template/init.ts b/src/template/init.ts new file mode 100644 index 0000000..23476c7 --- /dev/null +++ b/src/template/init.ts @@ -0,0 +1,204 @@ +import fs from "fs"; +import path from "path"; +import { BuildOptions } from "../types"; + +/** + * 生成 Dockerfile 内容 + */ +export function generateInitDockerfile( + pwd: string, + projectName: string, + options: BuildOptions +): string { + const { extra, env, registry, port, nodeVersion } = options; + + const allProjects = [projectName]; + allProjects.push(...extra); + + console.log("\n🔍 检查项目依赖关系..."); + // 需要遍历所有项目的package.json确定file:../这种,然后确保没有遗漏 + const projectPackageMap: { [key: string]: any } = {}; + + for (const project of allProjects) { + console.log(`检查项目: ${project}`); + const packageJsonPath = path.join(pwd, project, "package.json"); + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); + projectPackageMap[project] = packageJson; + const dependencies = packageJson.dependencies || {}; + for (const dep in dependencies) { + if (dependencies[dep].startsWith("file:")) { + const depProjectPath = dependencies[dep].replace("file:", ""); + const targetPath = path.join(pwd, project, depProjectPath); + const depProject = path.basename(targetPath); + console.log(` 发现本地依赖: ${depProject} (来自 ${project})`); + if (!allProjects.includes(depProject)) { + console.error(`❌ 发现未包含的依赖项目: ${depProject},请在命令行中通过 --extra 参数添加`); + process.exit(1); + } + } + } + } else { + console.error(`❌ 找不到项目的 package.json: ${packageJsonPath}`); + } + } + + let dockerfile = `# =========================== +# Stage 1: Build +# =========================== +FROM node:${nodeVersion}-alpine AS builder + +WORKDIR /app + +# 设置镜像站 +RUN npm config set registry ${registry} + +`; + // 复制所有项目 + for (const project of allProjects) { + dockerfile += `# 复制 ${project}\n`; + dockerfile += `COPY ./${project} ./${project}\n`; + } + + dockerfile += ` +# =========================== +# 构建所有项目 +# =========================== +`; + + // 为每个项目执行安装和构建 + for (const project of allProjects) { + dockerfile += ` +# 构建 ${project} +WORKDIR /app/${project} +RUN npm install +`; + + // 主项目需要执行 make:domain 和 build + if (project === projectName) { + const pkjson = projectPackageMap[project]; + if (pkjson.scripts && pkjson.scripts["make:domain"]) { + dockerfile += `RUN npm run make:domain\n`; + } + if (pkjson.scripts && pkjson.scripts["build"]) { + dockerfile += `RUN npm run build\n`; + } + if (pkjson.scripts && pkjson.scripts["confuse"]) { + dockerfile += `RUN npm run confuse\n`; + } + } else { + // 其他项目可能只需要 build,或者根据实际情况调整 + dockerfile += `RUN npm run build || true\n`; + } + } + + dockerfile += ` +# =========================== +# 清理构建产物 +# =========================== +`; + + // 清理所有项目的源码和测试文件 + for (const project of allProjects) { + dockerfile += ` +# 清理 ${project} +WORKDIR /app/${project} +RUN rm -rf src && rm -rf web && rm -rf wechatMp && rm -rf native && rm -rf */**/*.d.ts \\ + && rm -rf */**.ts && rm -rf .git && rm -rf .gitignore && rm -rf .husky && rm -rf configurations && rm -rf upgrade \\ + && 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 \\ + && find node_modules -type f \\( -name "*.map" \\) -delete \\ + && find node_modules -type f -name "LICENSE" -o -name "LICENSE.txt" -delete \\ + && find node_modules -type f -name "*.md" -o -name "*.markdown" -o -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.gif" -delete +`; + } + + dockerfile += ` +# =========================== +# Stage 2: Runtime +# =========================== +FROM node:${nodeVersion}-alpine + +WORKDIR /app + +# 从构建阶段复制所有内容 +COPY --from=builder /app ./ + +WORKDIR /app/${projectName} + +# 再次清理(确保运行时镜像最小化) +`; + + for (const project of allProjects) { + dockerfile += ` +# 清理 ${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" -o -name "*.map" \\) -delete \\ + && find . -type f -name "*.test.js" -delete +`; + } + + // 删除所有项目下除了 lib/data, script, package.json 和 node_modules 以外的文件夹与文件 + for (const project of allProjects) { + dockerfile += ` +# 清理 ${project} 运行时不需要的文件夹和文件 +WORKDIR /app/${project} +RUN set -eux; \\ + echo "Cleaning ${project} ..."; \\ + # 删除一级目录(保留 lib、data、node_modules、scripts、configuration) + find . -mindepth 1 -maxdepth 1 -type d ! -name "lib" ! -name "data" ! -name "node_modules" ! -name "scripts" ! -name "configuration" -exec rm -rf {} +; \\ + # 删除一级文件(保留 package.json) + find . -mindepth 1 -maxdepth 1 -type f ! -name "package.json" -exec rm -f {} +; \\ + # 清理 lib 下多余目录(保留 oak-app-domain、routines、data、context、config、configuration、 utils、types) + if [ -d "lib" ]; then \\ + echo "pass"; \\ + fi +`; + } + + dockerfile += ` +# 切换回主项目目录 +WORKDIR /app/${projectName} +`; + + return dockerfile; +} + +/** + * 生成 .dockerignore 内容 + */ +export function generateInitDockerignore(): string { + return `# Node modules +node_modules +npm-debug.log + +# 测试和文档 +test +tests +__tests__ +*.test.js +*.spec.js +docs +coverage + +# 开发文件 +.git +.gitignore +.env +.env.* +*.md +.vscode +.idea + +# 构建产物(在容器内构建) +dist +build + +# 临时文件 +*.log +*.tmp +.DS_Store +`; +} diff --git a/src/template/runtime.ts b/src/template/runtime.ts new file mode 100644 index 0000000..f9be26a --- /dev/null +++ b/src/template/runtime.ts @@ -0,0 +1,235 @@ +import fs from "fs"; +import path from "path"; +import { BuildOptions } from "../types"; + +/** + * 生成 Dockerfile 内容 + */ +/** + * 生成 Dockerfile 内容(自动根据依赖关系排序) + */ +export function generateDockerfile( + pwd: string, + projectName: string, + options: BuildOptions, + allProjects: string[], + projectPackageMap: { [key: string]: any } +): string { + const { extra, env, registry, port, nodeVersion } = options; + + console.log("\n-> 检查项目依赖关系..."); + + // 构建依赖图(有向图) + const dependencyGraph: Record> = {}; + for (const project of allProjects) { + dependencyGraph[project] = new Set(); + } + + // 遍历 package.json,记录依赖关系 + for (const project of allProjects) { + console.log(`检查项目: ${project}`); + const packageJsonPath = path.join(pwd, project, "package.json"); + if (!fs.existsSync(packageJsonPath)) { + console.error(`找不到项目的 package.json: ${packageJsonPath}`); + continue; + } + + const packageJson = projectPackageMap[project]; + const dependencies = packageJson.dependencies || {}; + for (const dep in dependencies) { + if (dependencies[dep].startsWith("file:")) { + const depProjectPath = dependencies[dep].replace("file:", ""); + const targetPath = path.join(pwd, project, depProjectPath); + const depProject = path.basename(targetPath); + + console.log(` 发现本地依赖: ${depProject} (来自 ${project})`); + if (!allProjects.includes(depProject)) { + console.error(`未包含的依赖项目: ${depProject},请通过 --extra 参数添加`); + process.exit(1); + } + + // 记录:project 依赖 depProject + dependencyGraph[project].add(depProject); + } + } + } + + // ===== 拓扑排序 ===== + function topoSort(graph: Record>): string[] { + const inDegree: Record = {}; + const result: string[] = []; + + // 初始化入度 + for (const p in graph) inDegree[p] = 0; + for (const p in graph) { + for (const dep of graph[p]) { + inDegree[dep] = (inDegree[dep] || 0) + 1; + } + } + + const queue: string[] = Object.keys(inDegree).filter(p => inDegree[p] === 0); + + while (queue.length > 0) { + const current = queue.shift()!; + result.push(current); + for (const dep of graph[current]) { + inDegree[dep]--; + if (inDegree[dep] === 0) queue.push(dep); + } + } + + if (result.length !== Object.keys(graph).length) { + console.error("检测到循环依赖,请检查项目间的 file: 引用"); + process.exit(1); + } + + return result; + } + + const sortedProjects = topoSort(dependencyGraph).reverse(); // 反转以确保依赖项在前; + + console.log("\n构建顺序如下:"); + sortedProjects.forEach((p, i) => console.log(` ${i + 1}. ${p}`)); + + // 确保主项目在最后(如果用户传入主项目) + const mainIndex = sortedProjects.indexOf(projectName); + if (mainIndex !== -1 && mainIndex !== sortedProjects.length - 1) { + sortedProjects.splice(mainIndex, 1); + sortedProjects.push(projectName); + } + + // ======================== + // 以下保持你的原始逻辑 + // ======================== + let dockerfile = `# =========================== +# Stage 1: Build +# =========================== +FROM node:${nodeVersion}-alpine AS builder + +WORKDIR /app + +# 设置镜像站 +RUN npm config set registry ${registry} + +`; + + // 复制所有项目 + for (const project of sortedProjects) { + dockerfile += `# 复制 ${project}\n`; + dockerfile += `COPY ./${project} ./${project}\n`; + } + + dockerfile += ` +# =========================== +# 构建所有项目 +# =========================== +`; + + for (const project of sortedProjects) { + dockerfile += ` +# 构建 ${project} +WORKDIR /app/${project} +RUN npm install +`; + const pkjson = projectPackageMap[project]; + if (project === projectName) { + if (pkjson.scripts?.["make:domain"]) dockerfile += `RUN npm run make:domain\n`; + if (pkjson.scripts?.["build"]) dockerfile += `RUN npm run build\n`; + if (pkjson.scripts?.["confuse"]) dockerfile += `RUN npm run confuse\n`; + } else { + dockerfile += `RUN npm run build || true\n`; + } + } + + dockerfile += ` +# =========================== +# 清理构建产物 +# =========================== +`; + + for (const project of sortedProjects) { + dockerfile += ` +# 清理 ${project} +WORKDIR /app/${project} +RUN rm -rf src && rm -rf web && rm -rf wechatMp && rm -rf native && rm -rf */**/*.d.ts \\ + && rm -rf */**.ts && rm -rf .git && rm -rf .gitignore && rm -rf .husky && rm -rf configurations && rm -rf upgrade \\ + && 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 \\ + && find node_modules -type f \\( -name "*.map" \\) -delete \\ + && find node_modules -type f -name "LICENSE" -o -name "LICENSE.txt" -delete \\ + && find node_modules -type f -name "*.md" -o -name "*.markdown" -o -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.gif" -delete +`; + } + + dockerfile += ` +# =========================== +# Stage 2: Runtime +# =========================== +FROM node:${nodeVersion}-alpine + +WORKDIR /app + +COPY --from=builder /app ./ + +WORKDIR /app/${projectName} + +RUN npm install -g @socket.io/pm2 +`; + + for (const project of sortedProjects) { + dockerfile += ` +# 清理 ${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" -o -name "*.map" \\) -delete \\ + && find . -type f -name "*.test.js" -delete +`; + } + + dockerfile += ` +WORKDIR /app/${projectName} +EXPOSE ${port.join(" ")} +CMD ["pm2-runtime", "start", "pm2.${env}.json"] +`; + + return dockerfile; +} + + +/** + * 生成 .dockerignore 内容 + */ +export function generateDockerignore(): string { + return `# Node modules +node_modules +npm-debug.log + +# 测试和文档 +test +tests +__tests__ +*.test.js +*.spec.js +docs +coverage + +# 开发文件 +.git +.gitignore +.env +.env.* +*.md +.vscode +.idea + +# 构建产物(在容器内构建) +dist +build + +# 临时文件 +*.log +*.tmp +.DS_Store +`; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..5f9cad6 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,14 @@ +export interface BuildOptions { + env: string; + extra: string[]; + registry: string; + port: string[]; + nodeVersion: string; + buildType: string; + webSource: string; +} + +export interface ParsedArgs { + projectDir: string; + options: BuildOptions; +} \ No newline at end of file