parent
08c093f3fc
commit
b3ebaa767f
|
|
@ -2,9 +2,383 @@
|
||||||
|
|
||||||
// src/cli.ts
|
// src/cli.ts
|
||||||
import { execSync } from "child_process";
|
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 fs from "fs";
|
||||||
import path from "path";
|
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();
|
var pwd = process.cwd();
|
||||||
try {
|
try {
|
||||||
execSync("docker --version", { stdio: "ignore" });
|
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");
|
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);
|
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() {
|
function parseCliArgs() {
|
||||||
const { values, positionals } = parseArgs({
|
const { values, positionals } = parseArgs({
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -82,14 +460,14 @@ function parseCliArgs() {
|
||||||
console.error(" \u4F7F\u7528 --help \u67E5\u770B\u8BE6\u7EC6\u7528\u6CD5");
|
console.error(" \u4F7F\u7528 --help \u67E5\u770B\u8BE6\u7EC6\u7528\u6CD5");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const projectDir = path.resolve(positionals[0]);
|
const projectDir = path3.resolve(positionals[0]);
|
||||||
const env = values.env;
|
const env = values.env;
|
||||||
if (!["dev", "prod", "staging"].includes(env)) {
|
if (!["dev", "prod", "staging"].includes(env)) {
|
||||||
console.error(`\u274C \u65E0\u6548\u7684\u73AF\u5883\u7C7B\u578B: ${env}`);
|
console.error(`\u274C \u65E0\u6548\u7684\u73AF\u5883\u7C7B\u578B: ${env}`);
|
||||||
console.error(" \u652F\u6301\u7684\u73AF\u5883: dev, prod, staging");
|
console.error(" \u652F\u6301\u7684\u73AF\u5883: dev, prod, staging");
|
||||||
process.exit(1);
|
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(`\u274C \u65E0\u6548\u7684\u6784\u5EFA\u7C7B\u578B: ${values["build-type"]}`);
|
||||||
console.error(" \u652F\u6301\u7684\u7C7B\u578B: native, wechatMp, web, backend");
|
console.error(" \u652F\u6301\u7684\u7C7B\u578B: native, wechatMp, web, backend");
|
||||||
process.exit(1);
|
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) {
|
function buildDockerImage(imageBase, name, pwd2) {
|
||||||
console.log(`
|
console.log(`
|
||||||
\u{1F527} Building image for ${name}...`);
|
\u{1F527} Building image for ${name}...`);
|
||||||
|
|
@ -273,16 +493,61 @@ function buildDockerImage(imageBase, name, pwd2) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function saveDockerImage(imageBase, name, pwd2) {
|
function saveDockerImage(imageBase, name, pwd2) {
|
||||||
const distDir = path.join(pwd2, "dist");
|
const distDir = path3.join(pwd2, "dist");
|
||||||
if (!fs.existsSync(distDir)) {
|
if (!fs3.existsSync(distDir)) {
|
||||||
fs.mkdirSync(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}...`);
|
console.log(`\u{1F4E6} Saving image to ${outputImagePath}...`);
|
||||||
execSync(`docker save -o ${outputImagePath} ${imageBase}:${name}`, {
|
execSync(`docker save -o ${outputImagePath} ${imageBase}:${name}`, {
|
||||||
stdio: "inherit"
|
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() {
|
function main() {
|
||||||
const args = parseCliArgs();
|
const args = parseCliArgs();
|
||||||
if (!args) {
|
if (!args) {
|
||||||
|
|
@ -300,7 +565,7 @@ function main() {
|
||||||
console.log(` npm \u955C\u50CF\u7AD9: ${options.registry}`);
|
console.log(` npm \u955C\u50CF\u7AD9: ${options.registry}`);
|
||||||
console.log(` \u7AEF\u53E3: ${options.port}`);
|
console.log(` \u7AEF\u53E3: ${options.port}`);
|
||||||
console.log(` Node.js \u7248\u672C: ${options.nodeVersion}`);
|
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}`);
|
console.error(`\u274C \u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${projectDir}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
@ -311,64 +576,51 @@ function main() {
|
||||||
case "web":
|
case "web":
|
||||||
packageWeb(projectDir, options);
|
packageWeb(projectDir, options);
|
||||||
break;
|
break;
|
||||||
|
case "init":
|
||||||
|
packageInit(projectDir, options);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.error(`\u274C \u4E0D\u652F\u6301\u7684\u6784\u5EFA\u7C7B\u578B: ${options.buildType}`);
|
console.error(`\u274C \u4E0D\u652F\u6301\u7684\u6784\u5EFA\u7C7B\u578B: ${options.buildType}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function packageBackend(projectDir, options) {
|
function packageBackend(projectDir, options) {
|
||||||
let skipConfigReplace = false;
|
const imageBase = path3.basename(projectDir);
|
||||||
const configDir = path.join(projectDir, "configurations");
|
const projectName = path3.basename(projectDir);
|
||||||
if (!fs.existsSync(configDir)) {
|
const projectPackageMap = {};
|
||||||
console.info(
|
const allProjects = [projectName];
|
||||||
`\u9879\u76EE\u76EE\u5F55\u4E2D\u6CA1\u6709 configurations \u6587\u4EF6\u5939\uFF1A${configDir}, \u8DF3\u8FC7\u914D\u7F6E\u66FF\u6362\u6B65\u9AA4`
|
const { extra } = options;
|
||||||
);
|
allProjects.push(...extra);
|
||||||
skipConfigReplace = true;
|
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 dockerfileContent = generateDockerfile(pwd, projectName, options, allProjects, projectPackageMap);
|
||||||
const projectName = path.basename(projectDir);
|
|
||||||
const dockerfileContent = generateDockerfile(projectName, options);
|
|
||||||
const dockerignoreContent = generateDockerignore();
|
const dockerignoreContent = generateDockerignore();
|
||||||
fs.writeFileSync(path.join(pwd, "Dockerfile"), dockerfileContent);
|
fs3.writeFileSync(path3.join(pwd, "Dockerfile"), dockerfileContent);
|
||||||
fs.writeFileSync(path.join(pwd, ".dockerignore"), dockerignoreContent);
|
fs3.writeFileSync(path3.join(pwd, ".dockerignore"), dockerignoreContent);
|
||||||
console.log("\n\u{1F4DD} \u5DF2\u751F\u6210 Dockerfile \u548C .dockerignore");
|
console.log("\n \u5DF2\u751F\u6210 Dockerfile \u548C .dockerignore");
|
||||||
try {
|
try {
|
||||||
if (!skipConfigReplace) {
|
const name = projectPackageMap[projectName].version || "latest";
|
||||||
const configFiles = fs.readdirSync(configDir).filter((file) => file.endsWith(".json"));
|
console.log(`
|
||||||
for (const file of configFiles) {
|
\u6784\u5EFA\u955C\u50CF: ${projectName}:${name}`);
|
||||||
const configPath = path.join(configDir, file);
|
buildDockerImage(imageBase, name, pwd);
|
||||||
const name = path.basename(file, ".json") + `-${options.env}`;
|
saveDockerImage(imageBase, name, pwd);
|
||||||
console.log(`
|
copyConfiguration(projectDir, imageBase, name, pwd);
|
||||||
\u{1F527} \u4F7F\u7528\u914D\u7F6E ${configPath} \u6784\u5EFA\u955C\u50CF ${name}...`);
|
writeStartCommandScript(projectDir, imageBase, name, pwd);
|
||||||
fs.copyFileSync(
|
console.log("\n\u955C\u50CF\u6784\u5EFA\u6210\u529F\uFF01");
|
||||||
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");
|
|
||||||
} finally {
|
} finally {
|
||||||
fs.rmSync(path.join(projectDir, "configuration/mysql.json"), {
|
fs3.rmSync(path3.join(pwd, "Dockerfile"), { force: true });
|
||||||
force: true
|
fs3.rmSync(path3.join(pwd, ".dockerignore"), { force: true });
|
||||||
});
|
|
||||||
fs.rmSync(path.join(pwd, "Dockerfile"), { force: true });
|
|
||||||
fs.rmSync(path.join(pwd, ".dockerignore"), { force: true });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function packageWeb(projectDir, options) {
|
function packageWeb(projectDir, options) {
|
||||||
console.log(`
|
console.log(`
|
||||||
\u{1F527} \u6253\u5305 Web \u9879\u76EE...`);
|
\u{1F527} \u6253\u5305 Web \u9879\u76EE...`);
|
||||||
const distDir = path.join(pwd, "dist");
|
const distDir = path3.join(pwd, "dist");
|
||||||
if (!fs.existsSync(distDir)) {
|
if (!fs3.existsSync(distDir)) {
|
||||||
fs.mkdirSync(distDir);
|
fs3.mkdirSync(distDir);
|
||||||
}
|
}
|
||||||
execSync(`npm install`, {
|
execSync(`npm install`, {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
|
|
@ -378,21 +630,41 @@ function packageWeb(projectDir, options) {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
cwd: projectDir
|
cwd: projectDir
|
||||||
});
|
});
|
||||||
const webDistDir = path.join(projectDir, options.webSource, "build");
|
const webDistDir = path3.join(projectDir, options.webSource, "build");
|
||||||
if (!fs.existsSync(webDistDir)) {
|
if (!fs3.existsSync(webDistDir)) {
|
||||||
console.error(`\u274C \u6784\u5EFA\u5931\u8D25\uFF0C\u627E\u4E0D\u5230 Web \u6784\u5EFA\u4EA7\u7269\u76EE\u5F55: ${webDistDir}`);
|
console.error(`\u274C \u6784\u5EFA\u5931\u8D25\uFF0C\u627E\u4E0D\u5230 Web \u6784\u5EFA\u4EA7\u7269\u76EE\u5F55: ${webDistDir}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const projectName = path.basename(projectDir);
|
const projectName = path3.basename(projectDir);
|
||||||
const outputWebDir = path.join(distDir, "html", projectName, options.webSource);
|
const outputWebDir = path3.join(distDir, "html", projectName, options.webSource);
|
||||||
if (!fs.existsSync(path.join(distDir, "html", projectName))) {
|
if (!fs3.existsSync(path3.join(distDir, "html", projectName))) {
|
||||||
fs.mkdirSync(path.join(distDir, "html", projectName));
|
fs3.mkdirSync(path3.join(distDir, "html", projectName));
|
||||||
}
|
}
|
||||||
fs.rmSync(outputWebDir, { recursive: true, force: true });
|
fs3.rmSync(outputWebDir, { recursive: true, force: true });
|
||||||
fs.mkdirSync(outputWebDir);
|
fs3.mkdirSync(outputWebDir);
|
||||||
execSync(`cp -r ${webDistDir}/* ${outputWebDir}/`, {
|
execSync(`cp -r ${webDistDir}/* ${outputWebDir}/`, {
|
||||||
stdio: "inherit"
|
stdio: "inherit"
|
||||||
});
|
});
|
||||||
console.log(`\u2705 Web \u9879\u76EE\u6253\u5305\u6210\u529F\uFF0C\u4EA7\u7269\u4F4D\u4E8E: ${outputWebDir}`);
|
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();
|
main();
|
||||||
|
|
|
||||||
387
src/cli.ts
387
src/cli.ts
|
|
@ -2,23 +2,13 @@ import { execSync } from "child_process";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { parseArgs } from "util";
|
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();
|
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命令是否可用
|
// 测试docker命令是否可用
|
||||||
try {
|
try {
|
||||||
|
|
@ -28,6 +18,13 @@ try {
|
||||||
process.exit(1);
|
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);
|
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(`❌ 无效的构建类型: ${values["build-type"]}`);
|
||||||
console.error(" 支持的类型: native, wechatMp, web, backend");
|
console.error(" 支持的类型: native, wechatMp, web, backend");
|
||||||
process.exit(1);
|
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 镜像
|
* 构建 Docker 镜像
|
||||||
*/
|
*/
|
||||||
|
|
@ -345,13 +159,76 @@ function saveDockerImage(
|
||||||
if (!fs.existsSync(distDir)) {
|
if (!fs.existsSync(distDir)) {
|
||||||
fs.mkdirSync(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}...`);
|
console.log(`📦 Saving image to ${outputImagePath}...`);
|
||||||
execSync(`docker save -o ${outputImagePath} ${imageBase}:${name}`, {
|
execSync(`docker save -o ${outputImagePath} ${imageBase}:${name}`, {
|
||||||
stdio: "inherit",
|
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":
|
case "web":
|
||||||
packageWeb(projectDir, options);
|
packageWeb(projectDir, options);
|
||||||
break;
|
break;
|
||||||
|
case "init":
|
||||||
|
packageInit(projectDir, options);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.error(`❌ 不支持的构建类型: ${options.buildType}`);
|
console.error(`❌ 不支持的构建类型: ${options.buildType}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function packageBackend(projectDir: string, options: BuildOptions): void {
|
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 imageBase = path.basename(projectDir);
|
||||||
const projectName = 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
|
// 生成 Dockerfile 和 .dockerignore
|
||||||
const dockerfileContent = generateDockerfile(projectName, options);
|
const dockerfileContent = generateDockerfile(pwd, projectName, options, allProjects, projectPackageMap);
|
||||||
const dockerignoreContent = generateDockerignore();
|
const dockerignoreContent = generateDockerignore();
|
||||||
|
|
||||||
fs.writeFileSync(path.join(pwd, "Dockerfile"), dockerfileContent);
|
fs.writeFileSync(path.join(pwd, "Dockerfile"), dockerfileContent);
|
||||||
fs.writeFileSync(path.join(pwd, ".dockerignore"), dockerignoreContent);
|
fs.writeFileSync(path.join(pwd, ".dockerignore"), dockerignoreContent);
|
||||||
|
|
||||||
console.log("\n📝 已生成 Dockerfile 和 .dockerignore");
|
console.log("\n 已生成 Dockerfile 和 .dockerignore");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!skipConfigReplace) {
|
// if (!skipConfigReplace) {
|
||||||
// 循环打包每个配置
|
// // 循环打包每个配置
|
||||||
const configFiles = fs
|
// const configFiles = fs
|
||||||
.readdirSync(configDir)
|
// .readdirSync(configDir)
|
||||||
.filter((file) => file.endsWith(".json"));
|
// .filter((file) => file.endsWith(".json"));
|
||||||
|
|
||||||
for (const file of configFiles) {
|
// for (const file of configFiles) {
|
||||||
const configPath = path.join(configDir, file);
|
// const configPath = path.join(configDir, file);
|
||||||
const name = path.basename(file, ".json") + `-${options.env}`;
|
// const name = path.basename(file, ".json") + `-${options.env}`;
|
||||||
|
|
||||||
console.log(`\n🔧 使用配置 ${configPath} 构建镜像 ${name}...`);
|
// console.log(`\n🔧 使用配置 ${configPath} 构建镜像 ${name}...`);
|
||||||
|
|
||||||
// 拷贝 mysql.json
|
// // 拷贝 mysql.json
|
||||||
fs.copyFileSync(
|
// fs.copyFileSync(
|
||||||
configPath,
|
// configPath,
|
||||||
path.join(projectDir, "configuration/mysql.json")
|
// path.join(projectDir, "configuration/mysql.json")
|
||||||
);
|
// );
|
||||||
|
|
||||||
buildDockerImage(imageBase, name, pwd);
|
// buildDockerImage(imageBase, name, pwd);
|
||||||
saveDockerImage(imageBase, name, pwd);
|
// saveDockerImage(imageBase, name, pwd);
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
// 只构建一个默认镜像
|
// 只构建一个默认镜像
|
||||||
const name = `default-${options.env}`;
|
|
||||||
console.log(`\n🔧 构建默认镜像 ${name}...`);
|
|
||||||
buildDockerImage(imageBase, name, pwd);
|
|
||||||
saveDockerImage(imageBase, name, pwd);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
} finally {
|
||||||
// 清理临时文件
|
// 清理临时文件
|
||||||
fs.rmSync(path.join(projectDir, "configuration/mysql.json"), {
|
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
fs.rmSync(path.join(pwd, "Dockerfile"), { force: true });
|
fs.rmSync(path.join(pwd, "Dockerfile"), { force: true });
|
||||||
fs.rmSync(path.join(pwd, ".dockerignore"), { 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}`);
|
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();
|
main();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
@ -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<string, Set<string>> = {};
|
||||||
|
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, Set<string>>): string[] {
|
||||||
|
const inDegree: Record<string, number> = {};
|
||||||
|
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
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue