@infly/libs 2.0.48 → 2.0.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,866 +1,866 @@
1
- /**
2
- * 构建工具
3
- * @description
4
- * 工具实现根据配置自动切换分支,构建到配置的对应文件夹路径,并且执行自动提交推送,也可执行自动压缩提交到当前项目
5
- */
6
-
7
- const path = require("path");
8
- const fs = require("fs");
9
- const readline = require("readline");
10
-
11
- const { exec } = require("child_process");
12
-
13
- global.logColor = {
14
- success: "\n\x1b[32m%s\x1b[0m",
15
- error: "\n\x1b[31m%s\x1b[0m",
16
- warning: "\n\x1b[33m%s\x1b[0m",
17
- link: "\x1b[34m%s\x1b[0m",
18
- info: "\n\x1b[36m%s\x1b[0m"
19
- };
20
-
21
- const { zipSync } = require("./zip.js");
22
- const { buildList } = require("../../script");
23
- const { init: previewInit } = require("../../tools/project-preview.js");
24
- const { postVersionFileAndMsg } = require("../build-utils/webhook-single.js");
25
- const { scriptWrite, targetProjectFileResolve, currentProjectFileResolve } = require("../../tools/file-process.js");
26
- const { shouldManageBuildScripts } = require("./script-management.js");
27
- const {
28
- autoBranchProcess,
29
- autoGitProcess,
30
- getCurrentBranch,
31
- getLatestCommitInfo,
32
- getLastCommitByFile,
33
- checkAndUpdatePackages,
34
- checkSingleDirGitStatus,
35
- pullSingleDirFromBranch
36
- } = require("../../script/git-automation");
37
- const { resolveSiblingAppDeps } = require("../build-utils/deps");
38
-
39
- const scriptEvent = process.env.npm_lifecycle_event;
40
- const isBuilZipScript = process.env.npm_lifecycle_event;
41
- const testScript = "build:test";
42
- const versionFileName = "package.json";
43
- const oldVersionFileName = "version-config.json";
44
- const npmPackageConfigKey = "infly";
45
- const currentBranch = getCurrentBranch();
46
-
47
- const projectRoot = process.cwd();
48
-
49
- // 依赖包的配置
50
- const originConfigPath = currentProjectFileResolve("template-version-config.json"); // 模板项目文件
51
- const currentProjectPackageJSON = currentProjectFileResolve("../package.json"); // 模板项目文件
52
- const currentPackageJsonConfig = JSON.parse(fs.readFileSync(currentProjectPackageJSON, "utf-8"));
53
- const { infly: currentInflyConfig } = currentPackageJsonConfig || {};
54
-
55
- // 安装依赖的项目配置
56
- const targetProjectVersionConfigPath = targetProjectFileResolve(oldVersionFileName);
57
- const targetProjectVueConfigPath = targetProjectFileResolve("vue.config.js");
58
- const targetProjectPackageJSON = targetProjectFileResolve("package.json");
59
- const targetProjectSettingsJS = targetProjectFileResolve("src/settings.js");
60
- const targetPackageJsonConfig = JSON.parse(fs.readFileSync(targetProjectPackageJSON, "utf-8"));
61
- const { name: projectName, version: projectVersion = "", infly: targetInflyConfig } = targetPackageJsonConfig || {};
62
- const [major = "", minor = "", patch = ""] = projectVersion.split(".");
63
- const { title: projectTitle } = fs.existsSync(targetProjectSettingsJS) ? require(targetProjectSettingsJS) || {} : {};
64
-
65
- const isExistTargetProjectVersionConfig = fs.existsSync(targetProjectVersionConfigPath); // 安装依赖项目下是否存在旧版本控制文件
66
- const isExistVueConfigFile = fs.existsSync(targetProjectVueConfigPath); // 安装依赖项目下是否存在vue.config.js文件
67
- const hasNextConfig =
68
- !isExistVueConfigFile && // Next.js 项目检测
69
- (fs.existsSync(targetProjectFileResolve("next.config.js")) ||
70
- fs.existsSync(targetProjectFileResolve("next.config.mjs")) ||
71
- fs.existsSync(targetProjectFileResolve("next.config.ts")));
72
- const isMaster = currentBranch === "master"; // 是否在master分支
73
- const isPkg = versionFileName === "package.json";
74
-
75
- const targetProjectVueConfig = isExistVueConfigFile ? require(targetProjectVueConfigPath) : {};
76
- const { outputDir: vueOutputDir, configureWebpack } = targetProjectVueConfig || {};
77
- let targetProjectOutputDir = vueOutputDir;
78
-
79
- const { updateDevText = "测试环境" } = { master: { updateDevText: "正式环境" } }[currentBranch] || {}; // 更新环境和处理分支
80
-
81
- const configPath = path.resolve(projectRoot, versionFileName);
82
- let buildConfig = JSON.parse(fs.readFileSync(configPath, "utf-8")) || {};
83
- let tempPlatformName = "";
84
-
85
- buildConfig = normalizeBuildConfig(buildConfig, npmPackageConfigKey, currentBranch, projectTitle);
86
-
87
- // 准备构建信息
88
- const { buildConfigs, versionConfig } = buildConfig || {};
89
- const {
90
- buildFoldName: zipBuildFoldName, // 构建后文件名称
91
- outputPath, // 输出路径
92
- gitAuto, // 是否自动提交git
93
- gitAutoPushReposBranchMap, // 自动提交文件到对应仓库分支配置
94
- gitAutoPushRepos = gitAutoPushReposBranchMap, // 自动提交文件到对应仓库
95
- gitAutoCommitText = "", // git自动提交信息
96
- enablePreview, // 启用预览
97
- openExplorer, // 启用打开资源管理器
98
- webhookUrl, // 企业微信机器人链接
99
- webhookAtUser, // 企业微信机器人@用户
100
- enableClipboard, // 启用粘贴板
101
- disableUpdateVersionEnv // 禁止更新版本信息环境
102
- } = buildConfigs || {};
103
- // Next.js 项目:优先使用 infly.buildConfigs.outputPath
104
- if (!targetProjectOutputDir && outputPath) {
105
- targetProjectOutputDir = outputPath;
106
- }
107
- if (targetProjectOutputDir && !path.isAbsolute(targetProjectOutputDir)) {
108
- targetProjectOutputDir = path.resolve(projectRoot, targetProjectOutputDir);
109
- }
110
- const buildFoldName = zipBuildFoldName || targetProjectOutputDir || "dist";
111
- const { mainVersion: verMainVersion, baseName, versionList = [], links } = versionConfig || {};
112
- const mainVersion = verMainVersion || major; // 主版本号
113
- const [lastestVersionItem] = versionList || [];
114
- const { lastVersion = projectVersion || "1.0.0" } = lastestVersionItem || {};
115
-
116
- function getBuildContext() {
117
- try {
118
- return JSON.parse(process.env.INFLY_BUILD_CONTEXT || "{}");
119
- } catch {
120
- return {};
121
- }
122
- }
123
-
124
- const buildContext = getBuildContext();
125
-
126
- function hasArg(name) {
127
- return process.argv.includes(name);
128
- }
129
-
130
- function appendBatchWebhookRecord(publishText, extraParams = {}) {
131
- const batchWebhookFile = buildContext.batchWebhookFile || process.env.INFLY_BATCH_WEBHOOK_FILE;
132
-
133
- if (!batchWebhookFile) {
134
- return false;
135
- }
136
-
137
- fs.appendFileSync(batchWebhookFile, `${JSON.stringify({ publishText, extraParams })}\n`);
138
- return true;
139
- }
140
-
141
- function getArgValue(name) {
142
- const index = process.argv.indexOf(name);
143
- return index >= 0 ? process.argv[index + 1] : "";
144
- }
145
-
146
- function getNextStandaloneServerPath(distPath) {
147
- const rootServerPath = path.join(distPath, "server.js");
148
- const workspaceServerPath = path.join(distPath, "apps", projectName, "server.js");
149
-
150
- if (fs.existsSync(rootServerPath)) {
151
- return rootServerPath;
152
- }
153
-
154
- return workspaceServerPath;
155
- }
156
-
157
- function getBuildMode() {
158
- const modeArg = getArgValue("--mode");
159
- const lifecycleText = [process.env.npm_lifecycle_event, process.env.npm_lifecycle_script, ...process.argv]
160
- .filter(Boolean)
161
- .join(" ");
162
-
163
- if (modeArg === "staging" || lifecycleText.includes("build:stage") || lifecycleText.includes("--mode staging")) {
164
- return "stage";
165
- }
166
-
167
- if (
168
- modeArg === "production" ||
169
- modeArg === "prod" ||
170
- lifecycleText.includes("build:prod") ||
171
- lifecycleText.includes("build:pro")
172
- ) {
173
- return "prod";
174
- }
175
-
176
- return "";
177
- }
178
-
179
- function getStageBranch(branchMap = {}, branch = currentBranch) {
180
- if (branch !== "master" && branchMap[branch] && branchMap[branch] !== "master") {
181
- return branch;
182
- }
183
-
184
- if (branchMap.develop && branchMap.develop !== "master") {
185
- return "develop";
186
- }
187
-
188
- if (branchMap.release && branchMap.release !== "master") {
189
- return "release";
190
- }
191
-
192
- return Object.keys(branchMap).find((key) => branchMap[key] && branchMap[key] !== "master") || branch;
193
- }
194
-
195
- function getBuildTargetBranch(branchMap = {}) {
196
- const buildMode = getBuildMode();
197
-
198
- if (buildMode === "prod") {
199
- return "master";
200
- }
201
-
202
- if (buildMode === "stage") {
203
- return getStageBranch(branchMap);
204
- }
205
-
206
- return currentBranch;
207
- }
208
-
209
- async function init() {
210
- try {
211
- // validateConfig(buildConfig);
212
- validateFileExists(configPath, versionFileName);
213
-
214
- // 处理版本信息
215
- const zipFiles = handleZipFiles(`-${buildFoldName}-v`);
216
- const { deleteOldFile } = updateVersionList(zipFiles, {
217
- buildConfig,
218
- configPath
219
- });
220
-
221
- // 处理所需版本文件信息
222
- const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, ".");
223
- const baseFileName = `${tempPlatformName}-${buildFoldName}-${baseName}`;
224
- const newVersion = isMaster ? incrementVersion(lastVersion, mainVersion) : lastVersion; // 非正式环境构建不更新版本信息
225
- const newFileName = baseFileName.replace("{version}", newVersion).replace("{timestamp}", timestamp);
226
- const gitInfo = getLatestCommitInfo() || {};
227
- const { branch, commitMsg } = gitInfo || {};
228
- let tempLink = typeof links === "object" ? links[branch] : links || "";
229
- if (typeof tempLink === "object") {
230
- tempLink = tempLink[process.env.VUE_APP_PLATFORM] || tempLink["DEFAULT"] || JSON.stringify(tempLink);
231
- }
232
- const publishText = `${tempPlatformName}:\n更新到:${tempLink}\n更新时间:${timestamp}\n版本号:v${newVersion}\n分支:${branch}\n更新环境:${updateDevText}\n更新内容:\n${commitMsg}`;
233
- const newZipFileInfo = [
234
- {
235
- version: newVersion,
236
- description: "",
237
- timestamp,
238
- fileName: newFileName,
239
- ps: "【自动压缩命令生成的版本】",
240
- gitInfo
241
- }
242
- ];
243
-
244
- // 处理文件路径
245
- const outputDir = path.resolve(projectRoot, "./", outputPath);
246
- const outputPathFull = path.join(outputDir, newFileName);
247
- const distPath = gitAutoPushRepos ? targetProjectOutputDir : path.resolve(projectRoot, "./" + buildFoldName);
248
- // Next.js standalone 项目检查 server.js,Vue/SPA 项目检查 index.html
249
- const validatePath = hasNextConfig ? getNextStandaloneServerPath(distPath) : path.join(distPath, "index.html");
250
-
251
- // 校验构建文件是否存在,确保构建成功
252
- validateFileExists(distPath, "distPath");
253
- validateFileExists(validatePath, hasNextConfig ? "serverJsPath" : "indexHtmlPath");
254
-
255
- // 非自动推送到构建仓库,进行构建文件压缩和检查是否压缩成功
256
- if (!gitAutoPushRepos) {
257
- zipSync(distPath, outputPathFull, true);
258
- validateFileExists(outputPathFull, "outputPathFull");
259
- }
260
-
261
- updateVersionList(newZipFileInfo, { opt: "add", buildConfig, configPath });
262
-
263
- if (gitAuto || gitAutoPushRepos) {
264
- const checkGitStatusParams = {
265
- newVersion,
266
- deleteOldFile,
267
- versionFileName,
268
- gitAutoPushRepos,
269
- targetProjectOutputDir,
270
- projectName
271
- };
272
-
273
- autoGitProcess(buildFoldName, newFileName, {
274
- gitInfo,
275
- gitAutoCommitText,
276
- gitAutoPushReposBranchMap,
277
- gitLockDir: buildContext.gitLockDir,
278
- gitProcessOrder: buildContext.gitProcessOrder,
279
- ...checkGitStatusParams
280
- });
281
- }
282
-
283
- if (enableClipboard) {
284
- copyToClipboard(publishText);
285
- }
286
-
287
- if (openExplorer) {
288
- exec(`explorer ${outputDir}`);
289
- }
290
-
291
- if (webhookUrl) {
292
- const webhookParams = {
293
- projectName,
294
- newFileName,
295
- gitInfo,
296
- targetProjectOutputDir,
297
- repos: path.relative(__dirname, targetProjectOutputDir).replace(/^(\.\.\\){4}apps\\/, ""),
298
- ...buildConfigs
299
- };
300
-
301
- if (!appendBatchWebhookRecord(publishText, webhookParams)) {
302
- postVersionFileAndMsg(publishText, webhookParams);
303
- }
304
- }
305
-
306
- if (enablePreview && buildContext.skipPreview !== true && process.env.INFLY_SKIP_PREVIEW !== "1") {
307
- await previewInit(true);
308
- }
309
- } catch (error) {
310
- console.error(global.logColor.error, `❌ 自动构建脚本执行出错, 错误信息:\n${error}`);
311
- process.exit(1);
312
- }
313
- }
314
-
315
- /**
316
- * 规范化构建配置
317
- * @param {Object} config - 原始配置对象
318
- * @param {String} packageKey - 包配置键名
319
- * @param {String} branch - 当前分支
320
- * @param {String} title - 项目标题
321
- * @returns {Object} 规范化后的配置对象
322
- */
323
- function normalizeBuildConfig(config, packageKey, branch, title) {
324
- let normalizedConfig = config;
325
-
326
- // 提取嵌套的配置
327
- if (normalizedConfig[packageKey]) {
328
- normalizedConfig = normalizedConfig[packageKey];
329
- }
330
-
331
- // 自动启用 gitAutoPushRepos
332
- if (!normalizedConfig.gitAutoPushRepos && normalizedConfig.gitAutoPushReposBranchMap) {
333
- normalizedConfig.gitAutoPushRepos = true;
334
- }
335
-
336
- // 处理平台名称配置
337
- const platformName = normalizedConfig?.versionConfig?.platformName;
338
- if (typeof platformName === "object") {
339
- tempPlatformName = platformName[branch] || platformName["master"] || "构建平台";
340
- } else if (typeof platformName === "string" && platformName) {
341
- tempPlatformName = platformName;
342
- } else if (title) {
343
- // 使用项目标题作为默认平台名称
344
- tempPlatformName = title;
345
- }
346
-
347
- return normalizedConfig;
348
- }
349
-
350
- /**
351
- * 用户确认提示
352
- * @param {string} message - 提示信息
353
- * @param {boolean} defaultYes - 默认是否确认
354
- * @returns {Promise<boolean>}
355
- */
356
- function askUserConfirmation(message, defaultYes = false) {
357
- return new Promise((resolve) => {
358
- const rl = readline.createInterface({
359
- input: process.stdin,
360
- output: process.stdout
361
- });
362
-
363
- const prompt = defaultYes ? `${message} (Y/n): ` : `${message} (y/N): `;
364
-
365
- rl.question(prompt, (answer) => {
366
- rl.close();
367
-
368
- const input = answer.trim().toLowerCase();
369
- let confirmed;
370
-
371
- if (input === "") {
372
- confirmed = defaultYes;
373
- } else {
374
- confirmed = input === "y" || input === "yes";
375
- }
376
-
377
- resolve(confirmed);
378
- });
379
- });
380
- }
381
-
382
- /**
383
- * 校验配置文件
384
- * @param {Object} buildConfig
385
- */
386
- async function validateConfig(buildConfig, targetBranch = currentBranch) {
387
- if (!buildConfig) {
388
- console.error(global.logColor.error, `❌ ZIP压缩已禁用,版本控制文件${versionFileName}配置错误`);
389
- process.exit(1);
390
- }
391
-
392
- if (buildConfig?.buildConfigs?.enabled === false) {
393
- console.warn(
394
- global.logColor.error,
395
- `⚠️ ZIP压缩已禁用,版本控制文件${versionFileName}中的 enabled 属性设置为 false`
396
- );
397
- process.exit(0);
398
- }
399
-
400
- if (!buildConfig?.buildConfigs?.gitAutoPushReposBranchMap[targetBranch]) {
401
- if (![testScript].includes(scriptEvent)) {
402
- console.error(
403
- global.logColor.error,
404
- `❌ 目标分支 ${targetBranch} 非 gitAutoPushReposBranchMap 配置可构建分支,请检查`
405
- );
406
- process.exit(1);
407
- }
408
- }
409
-
410
- const buildMode = getBuildMode();
411
- const lifecycleEvent = process.env.npm_lifecycle_event || "";
412
- const isProdBuild = buildMode === "prod" || lifecycleEvent.includes("prod");
413
- const isStageBuild = buildMode === "stage" || lifecycleEvent.includes("stage");
414
-
415
- if (targetBranch === "master" && !isProdBuild) {
416
- console.error(global.logColor.error, `❌ 目标分支为 master 分支,请使用正式环境构建命令进行构建`);
417
- process.exit(1);
418
- }
419
-
420
- if (targetBranch !== "master" && !isStageBuild) {
421
- console.warn(global.logColor.warning, `⚠️ 目标分支为 ${targetBranch} 分支,建议使用测试环境构建命令`);
422
- console.log(global.logColor.info, `推荐命令: npm run infly:build:stage`);
423
-
424
- const shouldContinue = await askUserConfirmation("确定要继续当前构建吗?", false);
425
-
426
- if (!shouldContinue) {
427
- console.log(global.logColor.info, "构建已取消,请使用正确的构建命令");
428
- process.exit(1);
429
- }
430
- }
431
- }
432
- /**
433
- * 校验文件是否存在
434
- * @param {String} checkPath - 检查路径
435
- * @param {String} logKey - 校验文案键值
436
- */
437
- function validateFileExists(checkPath, logKey) {
438
- const { error, success, link } = {
439
- [versionFileName]: {
440
- error: `版本控制文件${versionFileName}不存在,请检查`
441
- },
442
- distPath: {
443
- error: "构建文件夹不存在,请检查构建配置"
444
- },
445
- indexHtmlPath: {
446
- error: "index.html文件不存在,请检查构建配置"
447
- },
448
- serverJsPath: {
449
- error: `server.js文件不存在,请检查 Next.js standalone 构建配置。支持 server.js 或 ${path.join(
450
- "apps",
451
- projectName,
452
- "server.js"
453
- )}`
454
- },
455
- outputPathFull: {
456
- error: "压缩文件没有成功创建,请检查构建配置",
457
- success: `压缩完成,已成功创建文件`,
458
- link: checkPath
459
- },
460
- configPath: {
461
- error: `新版本写入${versionFileName}失败,请检查版本文件是否存在`,
462
- success: `新版本写入${versionFileName}成功,请点击文件链接确认版本信息无误`,
463
- link: checkPath
464
- }
465
- }[logKey];
466
- if (!fs.existsSync(checkPath)) {
467
- console.error(global.logColor.error, `❌ ${error}`);
468
- process.exit(1);
469
- }
470
- if (success) {
471
- console.log(global.logColor.success, `✅ ${success}`);
472
- }
473
- if (link) {
474
- console.log(global.logColor.link, `${link}`);
475
- }
476
- }
477
-
478
- /**
479
- * 处理压缩文件
480
- * @param {String} checkFileName - 检查文件名
481
- * @param {Function} callback - 回调函数
482
- * @returns
483
- */
484
- function handleZipFiles(checkFileName) {
485
- const files = fs.readdirSync(projectRoot);
486
- const zipFiles = [];
487
-
488
- files.forEach((file) => {
489
- if (path.extname(file).toLowerCase() === ".zip" && path.basename(file).includes(checkFileName)) {
490
- const [platformName, versionTimestamp = ""] = file.split(checkFileName);
491
- const [version, timestamp] = versionTimestamp.split("-");
492
- zipFiles.push({
493
- version,
494
- description: "",
495
- timestamp,
496
- fileName: file
497
- });
498
- }
499
- });
500
- return zipFiles;
501
- }
502
-
503
- /**
504
- * 将版本数组转为对象,方便查找匹配
505
- * @param {Array} versionList
506
- * @returns
507
- */
508
- function createVersionMap(versionList = []) {
509
- return versionList.reduce((acc, item) => {
510
- acc[item.fileName] = item.version;
511
- return acc;
512
- }, {});
513
- }
514
-
515
- /**
516
- * 检查是否禁用更新版本文件环境
517
- * @param {Boolean} disableUpdateVersionEnv - 禁用环境配置
518
- */
519
- function checkIsDisableUpdateVersionEnv(disableUpdateVersionEnv = []) {
520
- const lifecycleText = [process.env.npm_lifecycle_event, process.env.npm_lifecycle_script, ...process.argv]
521
- .filter(Boolean)
522
- .join(" ");
523
- let isDisableUpdateVersionEnv = lifecycleText.includes(`--mode staging`); // 默认禁用测试环境更新版本
524
-
525
- if (Array.isArray(disableUpdateVersionEnv) && disableUpdateVersionEnv.length > 0) {
526
- disableUpdateVersionEnv.forEach((item) => {
527
- if (lifecycleText.includes(`--mode ${item}`)) {
528
- isDisableUpdateVersionEnv = disableUpdateVersionEnv.includes(item);
529
- }
530
- });
531
- }
532
- return isDisableUpdateVersionEnv;
533
- }
534
-
535
- /**
536
- * 更新版本文件列表
537
- * @param {Array} zipFiles - ZIP 文件列表
538
- * @param {Object} extraParams - 额外传参
539
- * @returns
540
- */
541
- function updateVersionList(zipFiles = [], extraParams) {
542
- const { opt = "update", buildConfig, configPath } = extraParams || {};
543
- let { versionConfig } = buildConfig || {};
544
- if (buildConfig && !versionConfig) {
545
- buildConfig.versionConfig = {};
546
- versionConfig = buildConfig.versionConfig;
547
- }
548
- if (versionConfig && !Array.isArray(versionConfig.versionList)) {
549
- versionConfig.versionList = [];
550
- }
551
- const { versionList = [] } = versionConfig || {};
552
- const { disableUpdateVersionEnv } = versionConfig || {};
553
- const isDisableUpdateVersionEnv = checkIsDisableUpdateVersionEnv(disableUpdateVersionEnv);
554
- const versionMap = createVersionMap(versionList);
555
- const deleteOldFile = [];
556
- const [newVersionItem] = zipFiles || [];
557
- const { version: newVersion } = newVersionItem || {};
558
- let allVersionExist = true; // 是否所有版本都存在于 version-config.json 中
559
- if (zipFiles.length === 0) {
560
- if (!isPkg) {
561
- console.warn(global.logColor.warning, "⚠️ 没有 ZIP 文件,跳过版本更新。");
562
- }
563
- } else {
564
- zipFiles.forEach((zipFile) => {
565
- const { version, fileName, ps = "【非压缩命令生成的版本】", gitInfo = getLastCommitByFile(fileName) } = zipFile;
566
- const singleVersionInfo = { ...zipFile, ps, gitInfo };
567
- // 如果存在没有写入版本配置文件的压缩文件,则进行写入操作
568
- if (!versionMap[fileName] && Array.isArray(versionList)) {
569
- allVersionExist = false;
570
- buildConfig.versionConfig.versionList.unshift(singleVersionInfo);
571
- const updateText = isDisableUpdateVersionEnv
572
- ? `✅ 新版本${version}已生成,当前环境跳过写入${versionFileName}`
573
- : `✅ 新版本${version}写入${versionFileName}文件更新成功`;
574
- console.log(global.logColor.success, updateText);
575
- }
576
- // 如果是更新,压缩脚本进行打包则删除全部旧文件
577
- if (isBuilZipScript && opt === "update") {
578
- deleteOldFile.push(fileName);
579
- fs.unlinkSync(path.join(projectRoot, fileName));
580
- console.log(global.logColor.success, `✅ 已删除 ZIP 文件: ${fileName}`);
581
- }
582
- });
583
- }
584
- if (versionList.length > 0) {
585
- sortVersion(buildConfig.versionConfig.versionList);
586
- }
587
- // 非禁止更新更新版本信息的环境正常更新版本信息(比如production)
588
- if (!isDisableUpdateVersionEnv) {
589
- let tempWriteConfig = buildConfig;
590
- if (targetPackageJsonConfig[npmPackageConfigKey]) {
591
- const { name: oldProjectName, version: oldVersion, ...otherPkgConfig } = targetPackageJsonConfig;
592
- if (buildConfig.versionConfig.versionList) {
593
- buildConfig.versionConfig.versionList = [];
594
- }
595
- targetPackageJsonConfig[npmPackageConfigKey] = buildConfig || {};
596
- tempWriteConfig = {
597
- name: projectName,
598
- version: newVersion,
599
- ...otherPkgConfig
600
- };
601
- }
602
- fs.writeFileSync(configPath, JSON.stringify(tempWriteConfig, null, 2));
603
- }
604
-
605
- if (allVersionExist) {
606
- if (!isPkg) {
607
- console.log(global.logColor.success, `✅ 所有文件版本已存在于${versionFileName}中,无需更新`);
608
- }
609
- } else if (!isBuilZipScript || opt !== "update") {
610
- // 如果是自动压缩的命令,只在完成压缩更新版本后统一输出,更新的时候不输出减少重复信息
611
- // validateFileExists(configPath, "configPath");
612
- }
613
- // 如果不是自动压缩的命令,在更新版本json文件后退出
614
- if (!isBuilZipScript) {
615
- console.warn(global.logColor.warning, "⚠️ 此次仅进行版本号更新,不进行压缩。");
616
- process.exit(0);
617
- }
618
- return { deleteOldFile };
619
- }
620
-
621
- /**
622
- * 进行版本大小排序
623
- * @param {Array} versionList
624
- * @returns
625
- */
626
- function sortVersion(versionList = []) {
627
- return versionList.sort((a, b) => {
628
- const aParts = a.version.split(".");
629
- const bParts = b.version.split(".");
630
- for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
631
- const aPart = parseInt(aParts[i] || "0", 10);
632
- const bPart = parseInt(bParts[i] || "0", 10);
633
- if (aPart !== bPart) {
634
- return bPart - aPart;
635
- }
636
- }
637
- return 0;
638
- });
639
- }
640
-
641
- /**
642
- * 递增版本号
643
- * @param {String} version - 版本号
644
- * @param {String} mainVersion - 主版本号
645
- * @returns
646
- */
647
- function incrementVersion(version = "", mainVersion = "") {
648
- const parts = version.split(".");
649
- for (let i = parts.length - 1; i >= 1; i--) {
650
- if (parseInt(parts[i]) < 9) {
651
- parts[i] = (parseInt(parts[i]) + 1).toString();
652
- break;
653
- } else {
654
- parts[i] = "0";
655
- parts[i - 1] = (parseInt(parts[i - 1]) + 1).toString();
656
- break;
657
- }
658
- }
659
- if (mainVersion) {
660
- parts[0] = mainVersion;
661
- }
662
- return parts.join(".");
663
- }
664
-
665
- /**
666
- * 将发版文案复制到剪贴板
667
- * @param {String} text - 要复制的文本
668
- */
669
- function copyToClipboard(text) {
670
- return new Promise((resolve, reject) => {
671
- let command;
672
- if (process.platform === "win32") {
673
- command = "clip";
674
- } else if (process.platform === "darwin") {
675
- command = "pbcopy";
676
- } else {
677
- // Linux/WSL
678
- // 优先用 xclip,其次 xsel
679
- command = "xclip -selection clipboard || xsel --clipboard";
680
- }
681
- const child = exec(command, (error) => {
682
- if (error) {
683
- console.error(
684
- global.logColor.error,
685
- `❌ 文本复制失败: ${error}\n请确保已安装 xclip 或 xsel(Linux),或手动复制:\n${text}`
686
- );
687
- reject(error);
688
- } else {
689
- console.log(global.logColor.success, `✅ 文本已复制到剪贴板\n${text}`);
690
- resolve(text);
691
- }
692
- });
693
- // 确保使用UTF-8编码写入
694
- if (process.platform === "win32") {
695
- child.stdin.write(Buffer.from(text, "utf8"));
696
- } else {
697
- child.stdin.write(text, "utf8");
698
- }
699
- child.stdin.end();
700
- });
701
- }
702
-
703
- /**
704
- * 脚本配置初始化
705
- * @returns
706
- */
707
- function scriptInit() {
708
- let tempInflyConfig = {};
709
- if ((!isExistVueConfigFile && !hasNextConfig) || !fs.existsSync(targetProjectPackageJSON)) {
710
- return;
711
- }
712
- if (shouldManageBuildScripts(targetInflyConfig)) {
713
- scriptWrite(targetProjectPackageJSON, buildList);
714
- }
715
-
716
- if (targetInflyConfig) {
717
- return;
718
- }
719
-
720
- // 如果存在以前的版本管理文件则进行配置迁移
721
- if (isExistTargetProjectVersionConfig) {
722
- tempInflyConfig = JSON.parse(fs.readFileSync(targetProjectVersionConfigPath, "utf-8"));
723
- const { versionConfig = {}, buildConfigs = {} } = tempInflyConfig || {};
724
- if (versionConfig.versionList) {
725
- tempInflyConfig.versionConfig.versionList = []; // 删除旧版本列表
726
- }
727
- if (buildConfigs.buildFoldName) {
728
- tempInflyConfig.buildConfigs.buildFoldName = "";
729
- }
730
- }
731
-
732
- tempInflyConfig = { ...currentInflyConfig, ...tempInflyConfig };
733
-
734
- scriptWrite(targetProjectPackageJSON, tempInflyConfig, npmPackageConfigKey);
735
-
736
- if (isExistTargetProjectVersionConfig) {
737
- console.log(global.logColor.success, `✅ ${versionFileName}配置迁移成功, 可手动删除旧版本控制文件`);
738
- }
739
- }
740
-
741
- /**
742
- * 复制版本管理文件模板到根目录
743
- * @returns
744
- */
745
- function copyVersionConfigFile() {
746
- if (!isExistVueConfigFile && !hasNextConfig) {
747
- return;
748
- }
749
- const { name: projectTitle } = configureWebpack || {};
750
- if (!isExistTargetProjectVersionConfig && fs.existsSync(originConfigPath)) {
751
- // 创建可读流和可写流
752
- const readStream = fs.createReadStream(originConfigPath);
753
- const writeStream = fs.createWriteStream(targetProjectVersionConfigPath);
754
-
755
- readStream.pipe(writeStream);
756
-
757
- writeStream.on("finish", () => {
758
- // 第二阶段:修改复制后的文件
759
- fs.readFile(targetProjectVersionConfigPath, "utf8", (err, data) => {
760
- if (!targetProjectOutputDir && !projectTitle) {
761
- return;
762
- }
763
-
764
- if (err) {
765
- console.error(global.logColor.error, `❌ 读取复制文件失败: ${err.message}`);
766
- return;
767
- }
768
-
769
- const tempVersionConfig = JSON.parse(data);
770
-
771
- if (targetProjectOutputDir) {
772
- tempVersionConfig.buildConfigs.buildFoldName = targetProjectOutputDir;
773
- }
774
-
775
- if (projectTitle && !tempVersionConfig.versionConfig?.platformName) {
776
- tempVersionConfig.versionConfig.platformName = projectTitle;
777
- }
778
-
779
- const tempVersionConfigStr = JSON.stringify(tempVersionConfig, null, 2);
780
-
781
- fs.writeFile(targetProjectVersionConfigPath, tempVersionConfigStr, (err) => {
782
- if (err) {
783
- console.error(global.logColor.error, `❌ 读取vue.config.js配置更新失败: ${err.message}`);
784
- } else {
785
- console.log(global.logColor.success, `✅ 读取vue.config.js配置更新成功`);
786
- }
787
- });
788
- });
789
- console.log(global.logColor.success, `✅ 版本JSON文件已成功复制到根目录:${targetProjectVersionConfigPath}`);
790
- });
791
-
792
- writeStream.on("error", (err) => {
793
- console.error(global.logColor.error, `❌ 复制版本JSON文件失败:${err.message}`);
794
- });
795
- }
796
- }
797
-
798
- /**
799
- * 构建前分支处理
800
- */
801
- async function beforeBuild() {
802
- const targetBranch = getBuildTargetBranch(gitAutoPushReposBranchMap);
803
-
804
- await validateConfig(buildConfig, targetBranch);
805
-
806
- if (
807
- hasArg("--check-only") ||
808
- buildContext.checkOnly === true ||
809
- (process.env.INFLY_SKIP_BRANCH_UPDATE === "1" && process.env.INFLY_SKIP_PACKAGES_UPDATE === "1")
810
- ) {
811
- return;
812
- }
813
-
814
- const currentProjectStatus = checkSingleDirGitStatus(projectRoot, projectName);
815
-
816
- if (currentProjectStatus.hasUncommitted) {
817
- console.log(global.logColor.error, `\n❌ 当前项目存在未提交的更改,请先提交或暂存后再继续构建`);
818
- process.exit(1);
819
- }
820
-
821
- const currentProjectUpdateResult = pullSingleDirFromBranch(projectRoot, projectName, targetBranch);
822
-
823
- if (!currentProjectUpdateResult.success) {
824
- console.log(global.logColor.error, `\n❌ 当前项目切换到 ${targetBranch} 分支失败,请检查`);
825
- process.exit(1);
826
- }
827
-
828
- await autoBranchProcess({
829
- gitAutoPushRepos,
830
- gitAutoPushReposBranchMap,
831
- currentBranch: targetBranch,
832
- targetProjectOutputDir,
833
- validateConfig: () => {}
834
- });
835
-
836
- if (process.env.INFLY_SKIP_PACKAGES_UPDATE !== "1") {
837
- await checkAndUpdatePackages(undefined, "master");
838
- }
839
-
840
- // 5. 检查并更新兄弟 app 依赖(通过 build config 中的 alias 推断,无需硬编码 app 名称)
841
- if (process.env.INFLY_SKIP_SIBLING_UPDATE !== "1") {
842
- const siblingDirs = resolveSiblingAppDeps(projectRoot);
843
-
844
- for (const siblingDir of siblingDirs) {
845
- const siblingName = path.basename(siblingDir);
846
- const siblingStatus = checkSingleDirGitStatus(siblingDir, siblingName);
847
-
848
- if (siblingStatus.hasUncommitted) {
849
- console.log(global.logColor.error, `\n❌ 兄弟项目 ${siblingName} 存在未提交的更改,请先提交或暂存后再继续构建`);
850
- process.exit(1);
851
- }
852
-
853
- const siblingUpdateResult = pullSingleDirFromBranch(siblingDir, siblingName, targetBranch);
854
-
855
- if (!siblingUpdateResult.success) {
856
- console.log(global.logColor.warning, `\n⚠️ 兄弟项目 ${siblingName} 更新失败,但可以继续执行`);
857
- }
858
- }
859
- }
860
- }
861
- module.exports = {
862
- init,
863
- scriptInit,
864
- copyVersionConfigFile,
865
- beforeBuild
866
- };
1
+ /**
2
+ * 构建工具
3
+ * @description
4
+ * 工具实现根据配置自动切换分支,构建到配置的对应文件夹路径,并且执行自动提交推送,也可执行自动压缩提交到当前项目
5
+ */
6
+
7
+ const path = require("path");
8
+ const fs = require("fs");
9
+ const readline = require("readline");
10
+
11
+ const { exec } = require("child_process");
12
+
13
+ global.logColor = {
14
+ success: "\n\x1b[32m%s\x1b[0m",
15
+ error: "\n\x1b[31m%s\x1b[0m",
16
+ warning: "\n\x1b[33m%s\x1b[0m",
17
+ link: "\x1b[34m%s\x1b[0m",
18
+ info: "\n\x1b[36m%s\x1b[0m"
19
+ };
20
+
21
+ const { zipSync } = require("./zip.js");
22
+ const { buildList } = require("../../script");
23
+ const { init: previewInit } = require("../../adapters/vue2/project-preview.js");
24
+ const { postVersionFileAndMsg } = require("../build-utils/webhook-single.js");
25
+ const { scriptWrite, targetProjectFileResolve, currentProjectFileResolve } = require("../project-files.js");
26
+ const { shouldManageBuildScripts } = require("./script-management.js");
27
+ const {
28
+ autoBranchProcess,
29
+ autoGitProcess,
30
+ getCurrentBranch,
31
+ getLatestCommitInfo,
32
+ getLastCommitByFile,
33
+ checkAndUpdatePackages,
34
+ checkSingleDirGitStatus,
35
+ pullSingleDirFromBranch
36
+ } = require("../../script/git-automation");
37
+ const { resolveSiblingAppDeps } = require("../build-utils/deps");
38
+
39
+ const scriptEvent = process.env.npm_lifecycle_event;
40
+ const isBuilZipScript = process.env.npm_lifecycle_event;
41
+ const testScript = "build:test";
42
+ const versionFileName = "package.json";
43
+ const oldVersionFileName = "version-config.json";
44
+ const npmPackageConfigKey = "infly";
45
+ const currentBranch = getCurrentBranch();
46
+
47
+ const projectRoot = process.cwd();
48
+
49
+ // 依赖包的配置
50
+ const originConfigPath = currentProjectFileResolve("template-version-config.json"); // 模板项目文件
51
+ const currentProjectPackageJSON = currentProjectFileResolve("../package.json"); // 模板项目文件
52
+ const currentPackageJsonConfig = JSON.parse(fs.readFileSync(currentProjectPackageJSON, "utf-8"));
53
+ const { infly: currentInflyConfig } = currentPackageJsonConfig || {};
54
+
55
+ // 安装依赖的项目配置
56
+ const targetProjectVersionConfigPath = targetProjectFileResolve(oldVersionFileName);
57
+ const targetProjectVueConfigPath = targetProjectFileResolve("vue.config.js");
58
+ const targetProjectPackageJSON = targetProjectFileResolve("package.json");
59
+ const targetProjectSettingsJS = targetProjectFileResolve("src/settings.js");
60
+ const targetPackageJsonConfig = JSON.parse(fs.readFileSync(targetProjectPackageJSON, "utf-8"));
61
+ const { name: projectName, version: projectVersion = "", infly: targetInflyConfig } = targetPackageJsonConfig || {};
62
+ const [major = "", minor = "", patch = ""] = projectVersion.split(".");
63
+ const { title: projectTitle } = fs.existsSync(targetProjectSettingsJS) ? require(targetProjectSettingsJS) || {} : {};
64
+
65
+ const isExistTargetProjectVersionConfig = fs.existsSync(targetProjectVersionConfigPath); // 安装依赖项目下是否存在旧版本控制文件
66
+ const isExistVueConfigFile = fs.existsSync(targetProjectVueConfigPath); // 安装依赖项目下是否存在vue.config.js文件
67
+ const hasNextConfig =
68
+ !isExistVueConfigFile && // Next.js 项目检测
69
+ (fs.existsSync(targetProjectFileResolve("next.config.js")) ||
70
+ fs.existsSync(targetProjectFileResolve("next.config.mjs")) ||
71
+ fs.existsSync(targetProjectFileResolve("next.config.ts")));
72
+ const isMaster = currentBranch === "master"; // 是否在master分支
73
+ const isPkg = versionFileName === "package.json";
74
+
75
+ const targetProjectVueConfig = isExistVueConfigFile ? require(targetProjectVueConfigPath) : {};
76
+ const { outputDir: vueOutputDir, configureWebpack } = targetProjectVueConfig || {};
77
+ let targetProjectOutputDir = vueOutputDir;
78
+
79
+ const { updateDevText = "测试环境" } = { master: { updateDevText: "正式环境" } }[currentBranch] || {}; // 更新环境和处理分支
80
+
81
+ const configPath = path.resolve(projectRoot, versionFileName);
82
+ let buildConfig = JSON.parse(fs.readFileSync(configPath, "utf-8")) || {};
83
+ let tempPlatformName = "";
84
+
85
+ buildConfig = normalizeBuildConfig(buildConfig, npmPackageConfigKey, currentBranch, projectTitle);
86
+
87
+ // 准备构建信息
88
+ const { buildConfigs, versionConfig } = buildConfig || {};
89
+ const {
90
+ buildFoldName: zipBuildFoldName, // 构建后文件名称
91
+ outputPath, // 输出路径
92
+ gitAuto, // 是否自动提交git
93
+ gitAutoPushReposBranchMap, // 自动提交文件到对应仓库分支配置
94
+ gitAutoPushRepos = gitAutoPushReposBranchMap, // 自动提交文件到对应仓库
95
+ gitAutoCommitText = "", // git自动提交信息
96
+ enablePreview, // 启用预览
97
+ openExplorer, // 启用打开资源管理器
98
+ webhookUrl, // 企业微信机器人链接
99
+ webhookAtUser, // 企业微信机器人@用户
100
+ enableClipboard, // 启用粘贴板
101
+ disableUpdateVersionEnv // 禁止更新版本信息环境
102
+ } = buildConfigs || {};
103
+ // Next.js 项目:优先使用 infly.buildConfigs.outputPath
104
+ if (!targetProjectOutputDir && outputPath) {
105
+ targetProjectOutputDir = outputPath;
106
+ }
107
+ if (targetProjectOutputDir && !path.isAbsolute(targetProjectOutputDir)) {
108
+ targetProjectOutputDir = path.resolve(projectRoot, targetProjectOutputDir);
109
+ }
110
+ const buildFoldName = zipBuildFoldName || targetProjectOutputDir || "dist";
111
+ const { mainVersion: verMainVersion, baseName, versionList = [], links } = versionConfig || {};
112
+ const mainVersion = verMainVersion || major; // 主版本号
113
+ const [lastestVersionItem] = versionList || [];
114
+ const { lastVersion = projectVersion || "1.0.0" } = lastestVersionItem || {};
115
+
116
+ function getBuildContext() {
117
+ try {
118
+ return JSON.parse(process.env.INFLY_BUILD_CONTEXT || "{}");
119
+ } catch {
120
+ return {};
121
+ }
122
+ }
123
+
124
+ const buildContext = getBuildContext();
125
+
126
+ function hasArg(name) {
127
+ return process.argv.includes(name);
128
+ }
129
+
130
+ function appendBatchWebhookRecord(publishText, extraParams = {}) {
131
+ const batchWebhookFile = buildContext.batchWebhookFile || process.env.INFLY_BATCH_WEBHOOK_FILE;
132
+
133
+ if (!batchWebhookFile) {
134
+ return false;
135
+ }
136
+
137
+ fs.appendFileSync(batchWebhookFile, `${JSON.stringify({ publishText, extraParams })}\n`);
138
+ return true;
139
+ }
140
+
141
+ function getArgValue(name) {
142
+ const index = process.argv.indexOf(name);
143
+ return index >= 0 ? process.argv[index + 1] : "";
144
+ }
145
+
146
+ function getNextStandaloneServerPath(distPath) {
147
+ const rootServerPath = path.join(distPath, "server.js");
148
+ const workspaceServerPath = path.join(distPath, "apps", projectName, "server.js");
149
+
150
+ if (fs.existsSync(rootServerPath)) {
151
+ return rootServerPath;
152
+ }
153
+
154
+ return workspaceServerPath;
155
+ }
156
+
157
+ function getBuildMode() {
158
+ const modeArg = getArgValue("--mode");
159
+ const lifecycleText = [process.env.npm_lifecycle_event, process.env.npm_lifecycle_script, ...process.argv]
160
+ .filter(Boolean)
161
+ .join(" ");
162
+
163
+ if (modeArg === "staging" || lifecycleText.includes("build:stage") || lifecycleText.includes("--mode staging")) {
164
+ return "stage";
165
+ }
166
+
167
+ if (
168
+ modeArg === "production" ||
169
+ modeArg === "prod" ||
170
+ lifecycleText.includes("build:prod") ||
171
+ lifecycleText.includes("build:pro")
172
+ ) {
173
+ return "prod";
174
+ }
175
+
176
+ return "";
177
+ }
178
+
179
+ function getStageBranch(branchMap = {}, branch = currentBranch) {
180
+ if (branch !== "master" && branchMap[branch] && branchMap[branch] !== "master") {
181
+ return branch;
182
+ }
183
+
184
+ if (branchMap.develop && branchMap.develop !== "master") {
185
+ return "develop";
186
+ }
187
+
188
+ if (branchMap.release && branchMap.release !== "master") {
189
+ return "release";
190
+ }
191
+
192
+ return Object.keys(branchMap).find((key) => branchMap[key] && branchMap[key] !== "master") || branch;
193
+ }
194
+
195
+ function getBuildTargetBranch(branchMap = {}) {
196
+ const buildMode = getBuildMode();
197
+
198
+ if (buildMode === "prod") {
199
+ return "master";
200
+ }
201
+
202
+ if (buildMode === "stage") {
203
+ return getStageBranch(branchMap);
204
+ }
205
+
206
+ return currentBranch;
207
+ }
208
+
209
+ async function init() {
210
+ try {
211
+ // validateConfig(buildConfig);
212
+ validateFileExists(configPath, versionFileName);
213
+
214
+ // 处理版本信息
215
+ const zipFiles = handleZipFiles(`-${buildFoldName}-v`);
216
+ const { deleteOldFile } = updateVersionList(zipFiles, {
217
+ buildConfig,
218
+ configPath
219
+ });
220
+
221
+ // 处理所需版本文件信息
222
+ const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, ".");
223
+ const baseFileName = `${tempPlatformName}-${buildFoldName}-${baseName}`;
224
+ const newVersion = isMaster ? incrementVersion(lastVersion, mainVersion) : lastVersion; // 非正式环境构建不更新版本信息
225
+ const newFileName = baseFileName.replace("{version}", newVersion).replace("{timestamp}", timestamp);
226
+ const gitInfo = getLatestCommitInfo() || {};
227
+ const { branch, commitMsg } = gitInfo || {};
228
+ let tempLink = typeof links === "object" ? links[branch] : links || "";
229
+ if (typeof tempLink === "object") {
230
+ tempLink = tempLink[process.env.VUE_APP_PLATFORM] || tempLink["DEFAULT"] || JSON.stringify(tempLink);
231
+ }
232
+ const publishText = `${tempPlatformName}:\n更新到:${tempLink}\n更新时间:${timestamp}\n版本号:v${newVersion}\n分支:${branch}\n更新环境:${updateDevText}\n更新内容:\n${commitMsg}`;
233
+ const newZipFileInfo = [
234
+ {
235
+ version: newVersion,
236
+ description: "",
237
+ timestamp,
238
+ fileName: newFileName,
239
+ ps: "【自动压缩命令生成的版本】",
240
+ gitInfo
241
+ }
242
+ ];
243
+
244
+ // 处理文件路径
245
+ const outputDir = path.resolve(projectRoot, "./", outputPath);
246
+ const outputPathFull = path.join(outputDir, newFileName);
247
+ const distPath = gitAutoPushRepos ? targetProjectOutputDir : path.resolve(projectRoot, "./" + buildFoldName);
248
+ // Next.js standalone 项目检查 server.js,Vue/SPA 项目检查 index.html
249
+ const validatePath = hasNextConfig ? getNextStandaloneServerPath(distPath) : path.join(distPath, "index.html");
250
+
251
+ // 校验构建文件是否存在,确保构建成功
252
+ validateFileExists(distPath, "distPath");
253
+ validateFileExists(validatePath, hasNextConfig ? "serverJsPath" : "indexHtmlPath");
254
+
255
+ // 非自动推送到构建仓库,进行构建文件压缩和检查是否压缩成功
256
+ if (!gitAutoPushRepos) {
257
+ zipSync(distPath, outputPathFull, true);
258
+ validateFileExists(outputPathFull, "outputPathFull");
259
+ }
260
+
261
+ updateVersionList(newZipFileInfo, { opt: "add", buildConfig, configPath });
262
+
263
+ if (gitAuto || gitAutoPushRepos) {
264
+ const checkGitStatusParams = {
265
+ newVersion,
266
+ deleteOldFile,
267
+ versionFileName,
268
+ gitAutoPushRepos,
269
+ targetProjectOutputDir,
270
+ projectName
271
+ };
272
+
273
+ autoGitProcess(buildFoldName, newFileName, {
274
+ gitInfo,
275
+ gitAutoCommitText,
276
+ gitAutoPushReposBranchMap,
277
+ gitLockDir: buildContext.gitLockDir,
278
+ gitProcessOrder: buildContext.gitProcessOrder,
279
+ ...checkGitStatusParams
280
+ });
281
+ }
282
+
283
+ if (enableClipboard) {
284
+ copyToClipboard(publishText);
285
+ }
286
+
287
+ if (openExplorer) {
288
+ exec(`explorer ${outputDir}`);
289
+ }
290
+
291
+ if (webhookUrl) {
292
+ const webhookParams = {
293
+ projectName,
294
+ newFileName,
295
+ gitInfo,
296
+ targetProjectOutputDir,
297
+ repos: path.relative(__dirname, targetProjectOutputDir).replace(/^(\.\.\\){4}apps\\/, ""),
298
+ ...buildConfigs
299
+ };
300
+
301
+ if (!appendBatchWebhookRecord(publishText, webhookParams)) {
302
+ postVersionFileAndMsg(publishText, webhookParams);
303
+ }
304
+ }
305
+
306
+ if (enablePreview && buildContext.skipPreview !== true && process.env.INFLY_SKIP_PREVIEW !== "1") {
307
+ await previewInit(true);
308
+ }
309
+ } catch (error) {
310
+ console.error(global.logColor.error, `❌ 自动构建脚本执行出错, 错误信息:\n${error}`);
311
+ process.exit(1);
312
+ }
313
+ }
314
+
315
+ /**
316
+ * 规范化构建配置
317
+ * @param {Object} config - 原始配置对象
318
+ * @param {String} packageKey - 包配置键名
319
+ * @param {String} branch - 当前分支
320
+ * @param {String} title - 项目标题
321
+ * @returns {Object} 规范化后的配置对象
322
+ */
323
+ function normalizeBuildConfig(config, packageKey, branch, title) {
324
+ let normalizedConfig = config;
325
+
326
+ // 提取嵌套的配置
327
+ if (normalizedConfig[packageKey]) {
328
+ normalizedConfig = normalizedConfig[packageKey];
329
+ }
330
+
331
+ // 自动启用 gitAutoPushRepos
332
+ if (!normalizedConfig.gitAutoPushRepos && normalizedConfig.gitAutoPushReposBranchMap) {
333
+ normalizedConfig.gitAutoPushRepos = true;
334
+ }
335
+
336
+ // 处理平台名称配置
337
+ const platformName = normalizedConfig?.versionConfig?.platformName;
338
+ if (typeof platformName === "object") {
339
+ tempPlatformName = platformName[branch] || platformName["master"] || "构建平台";
340
+ } else if (typeof platformName === "string" && platformName) {
341
+ tempPlatformName = platformName;
342
+ } else if (title) {
343
+ // 使用项目标题作为默认平台名称
344
+ tempPlatformName = title;
345
+ }
346
+
347
+ return normalizedConfig;
348
+ }
349
+
350
+ /**
351
+ * 用户确认提示
352
+ * @param {string} message - 提示信息
353
+ * @param {boolean} defaultYes - 默认是否确认
354
+ * @returns {Promise<boolean>}
355
+ */
356
+ function askUserConfirmation(message, defaultYes = false) {
357
+ return new Promise((resolve) => {
358
+ const rl = readline.createInterface({
359
+ input: process.stdin,
360
+ output: process.stdout
361
+ });
362
+
363
+ const prompt = defaultYes ? `${message} (Y/n): ` : `${message} (y/N): `;
364
+
365
+ rl.question(prompt, (answer) => {
366
+ rl.close();
367
+
368
+ const input = answer.trim().toLowerCase();
369
+ let confirmed;
370
+
371
+ if (input === "") {
372
+ confirmed = defaultYes;
373
+ } else {
374
+ confirmed = input === "y" || input === "yes";
375
+ }
376
+
377
+ resolve(confirmed);
378
+ });
379
+ });
380
+ }
381
+
382
+ /**
383
+ * 校验配置文件
384
+ * @param {Object} buildConfig
385
+ */
386
+ async function validateConfig(buildConfig, targetBranch = currentBranch) {
387
+ if (!buildConfig) {
388
+ console.error(global.logColor.error, `❌ ZIP压缩已禁用,版本控制文件${versionFileName}配置错误`);
389
+ process.exit(1);
390
+ }
391
+
392
+ if (buildConfig?.buildConfigs?.enabled === false) {
393
+ console.warn(
394
+ global.logColor.error,
395
+ `⚠️ ZIP压缩已禁用,版本控制文件${versionFileName}中的 enabled 属性设置为 false`
396
+ );
397
+ process.exit(0);
398
+ }
399
+
400
+ if (!buildConfig?.buildConfigs?.gitAutoPushReposBranchMap[targetBranch]) {
401
+ if (![testScript].includes(scriptEvent)) {
402
+ console.error(
403
+ global.logColor.error,
404
+ `❌ 目标分支 ${targetBranch} 非 gitAutoPushReposBranchMap 配置可构建分支,请检查`
405
+ );
406
+ process.exit(1);
407
+ }
408
+ }
409
+
410
+ const buildMode = getBuildMode();
411
+ const lifecycleEvent = process.env.npm_lifecycle_event || "";
412
+ const isProdBuild = buildMode === "prod" || lifecycleEvent.includes("prod");
413
+ const isStageBuild = buildMode === "stage" || lifecycleEvent.includes("stage");
414
+
415
+ if (targetBranch === "master" && !isProdBuild) {
416
+ console.error(global.logColor.error, `❌ 目标分支为 master 分支,请使用正式环境构建命令进行构建`);
417
+ process.exit(1);
418
+ }
419
+
420
+ if (targetBranch !== "master" && !isStageBuild) {
421
+ console.warn(global.logColor.warning, `⚠️ 目标分支为 ${targetBranch} 分支,建议使用测试环境构建命令`);
422
+ console.log(global.logColor.info, `推荐命令: npm run infly:build:stage`);
423
+
424
+ const shouldContinue = await askUserConfirmation("确定要继续当前构建吗?", false);
425
+
426
+ if (!shouldContinue) {
427
+ console.log(global.logColor.info, "构建已取消,请使用正确的构建命令");
428
+ process.exit(1);
429
+ }
430
+ }
431
+ }
432
+ /**
433
+ * 校验文件是否存在
434
+ * @param {String} checkPath - 检查路径
435
+ * @param {String} logKey - 校验文案键值
436
+ */
437
+ function validateFileExists(checkPath, logKey) {
438
+ const { error, success, link } = {
439
+ [versionFileName]: {
440
+ error: `版本控制文件${versionFileName}不存在,请检查`
441
+ },
442
+ distPath: {
443
+ error: "构建文件夹不存在,请检查构建配置"
444
+ },
445
+ indexHtmlPath: {
446
+ error: "index.html文件不存在,请检查构建配置"
447
+ },
448
+ serverJsPath: {
449
+ error: `server.js文件不存在,请检查 Next.js standalone 构建配置。支持 server.js 或 ${path.join(
450
+ "apps",
451
+ projectName,
452
+ "server.js"
453
+ )}`
454
+ },
455
+ outputPathFull: {
456
+ error: "压缩文件没有成功创建,请检查构建配置",
457
+ success: `压缩完成,已成功创建文件`,
458
+ link: checkPath
459
+ },
460
+ configPath: {
461
+ error: `新版本写入${versionFileName}失败,请检查版本文件是否存在`,
462
+ success: `新版本写入${versionFileName}成功,请点击文件链接确认版本信息无误`,
463
+ link: checkPath
464
+ }
465
+ }[logKey];
466
+ if (!fs.existsSync(checkPath)) {
467
+ console.error(global.logColor.error, `❌ ${error}`);
468
+ process.exit(1);
469
+ }
470
+ if (success) {
471
+ console.log(global.logColor.success, `✅ ${success}`);
472
+ }
473
+ if (link) {
474
+ console.log(global.logColor.link, `${link}`);
475
+ }
476
+ }
477
+
478
+ /**
479
+ * 处理压缩文件
480
+ * @param {String} checkFileName - 检查文件名
481
+ * @param {Function} callback - 回调函数
482
+ * @returns
483
+ */
484
+ function handleZipFiles(checkFileName) {
485
+ const files = fs.readdirSync(projectRoot);
486
+ const zipFiles = [];
487
+
488
+ files.forEach((file) => {
489
+ if (path.extname(file).toLowerCase() === ".zip" && path.basename(file).includes(checkFileName)) {
490
+ const [platformName, versionTimestamp = ""] = file.split(checkFileName);
491
+ const [version, timestamp] = versionTimestamp.split("-");
492
+ zipFiles.push({
493
+ version,
494
+ description: "",
495
+ timestamp,
496
+ fileName: file
497
+ });
498
+ }
499
+ });
500
+ return zipFiles;
501
+ }
502
+
503
+ /**
504
+ * 将版本数组转为对象,方便查找匹配
505
+ * @param {Array} versionList
506
+ * @returns
507
+ */
508
+ function createVersionMap(versionList = []) {
509
+ return versionList.reduce((acc, item) => {
510
+ acc[item.fileName] = item.version;
511
+ return acc;
512
+ }, {});
513
+ }
514
+
515
+ /**
516
+ * 检查是否禁用更新版本文件环境
517
+ * @param {Boolean} disableUpdateVersionEnv - 禁用环境配置
518
+ */
519
+ function checkIsDisableUpdateVersionEnv(disableUpdateVersionEnv = []) {
520
+ const lifecycleText = [process.env.npm_lifecycle_event, process.env.npm_lifecycle_script, ...process.argv]
521
+ .filter(Boolean)
522
+ .join(" ");
523
+ let isDisableUpdateVersionEnv = lifecycleText.includes(`--mode staging`); // 默认禁用测试环境更新版本
524
+
525
+ if (Array.isArray(disableUpdateVersionEnv) && disableUpdateVersionEnv.length > 0) {
526
+ disableUpdateVersionEnv.forEach((item) => {
527
+ if (lifecycleText.includes(`--mode ${item}`)) {
528
+ isDisableUpdateVersionEnv = disableUpdateVersionEnv.includes(item);
529
+ }
530
+ });
531
+ }
532
+ return isDisableUpdateVersionEnv;
533
+ }
534
+
535
+ /**
536
+ * 更新版本文件列表
537
+ * @param {Array} zipFiles - ZIP 文件列表
538
+ * @param {Object} extraParams - 额外传参
539
+ * @returns
540
+ */
541
+ function updateVersionList(zipFiles = [], extraParams) {
542
+ const { opt = "update", buildConfig, configPath } = extraParams || {};
543
+ let { versionConfig } = buildConfig || {};
544
+ if (buildConfig && !versionConfig) {
545
+ buildConfig.versionConfig = {};
546
+ versionConfig = buildConfig.versionConfig;
547
+ }
548
+ if (versionConfig && !Array.isArray(versionConfig.versionList)) {
549
+ versionConfig.versionList = [];
550
+ }
551
+ const { versionList = [] } = versionConfig || {};
552
+ const { disableUpdateVersionEnv } = versionConfig || {};
553
+ const isDisableUpdateVersionEnv = checkIsDisableUpdateVersionEnv(disableUpdateVersionEnv);
554
+ const versionMap = createVersionMap(versionList);
555
+ const deleteOldFile = [];
556
+ const [newVersionItem] = zipFiles || [];
557
+ const { version: newVersion } = newVersionItem || {};
558
+ let allVersionExist = true; // 是否所有版本都存在于 version-config.json 中
559
+ if (zipFiles.length === 0) {
560
+ if (!isPkg) {
561
+ console.warn(global.logColor.warning, "⚠️ 没有 ZIP 文件,跳过版本更新。");
562
+ }
563
+ } else {
564
+ zipFiles.forEach((zipFile) => {
565
+ const { version, fileName, ps = "【非压缩命令生成的版本】", gitInfo = getLastCommitByFile(fileName) } = zipFile;
566
+ const singleVersionInfo = { ...zipFile, ps, gitInfo };
567
+ // 如果存在没有写入版本配置文件的压缩文件,则进行写入操作
568
+ if (!versionMap[fileName] && Array.isArray(versionList)) {
569
+ allVersionExist = false;
570
+ buildConfig.versionConfig.versionList.unshift(singleVersionInfo);
571
+ const updateText = isDisableUpdateVersionEnv
572
+ ? `✅ 新版本${version}已生成,当前环境跳过写入${versionFileName}`
573
+ : `✅ 新版本${version}写入${versionFileName}文件更新成功`;
574
+ console.log(global.logColor.success, updateText);
575
+ }
576
+ // 如果是更新,压缩脚本进行打包则删除全部旧文件
577
+ if (isBuilZipScript && opt === "update") {
578
+ deleteOldFile.push(fileName);
579
+ fs.unlinkSync(path.join(projectRoot, fileName));
580
+ console.log(global.logColor.success, `✅ 已删除 ZIP 文件: ${fileName}`);
581
+ }
582
+ });
583
+ }
584
+ if (versionList.length > 0) {
585
+ sortVersion(buildConfig.versionConfig.versionList);
586
+ }
587
+ // 非禁止更新更新版本信息的环境正常更新版本信息(比如production)
588
+ if (!isDisableUpdateVersionEnv) {
589
+ let tempWriteConfig = buildConfig;
590
+ if (targetPackageJsonConfig[npmPackageConfigKey]) {
591
+ const { name: oldProjectName, version: oldVersion, ...otherPkgConfig } = targetPackageJsonConfig;
592
+ if (buildConfig.versionConfig.versionList) {
593
+ buildConfig.versionConfig.versionList = [];
594
+ }
595
+ targetPackageJsonConfig[npmPackageConfigKey] = buildConfig || {};
596
+ tempWriteConfig = {
597
+ name: projectName,
598
+ version: newVersion,
599
+ ...otherPkgConfig
600
+ };
601
+ }
602
+ fs.writeFileSync(configPath, JSON.stringify(tempWriteConfig, null, 2));
603
+ }
604
+
605
+ if (allVersionExist) {
606
+ if (!isPkg) {
607
+ console.log(global.logColor.success, `✅ 所有文件版本已存在于${versionFileName}中,无需更新`);
608
+ }
609
+ } else if (!isBuilZipScript || opt !== "update") {
610
+ // 如果是自动压缩的命令,只在完成压缩更新版本后统一输出,更新的时候不输出减少重复信息
611
+ // validateFileExists(configPath, "configPath");
612
+ }
613
+ // 如果不是自动压缩的命令,在更新版本json文件后退出
614
+ if (!isBuilZipScript) {
615
+ console.warn(global.logColor.warning, "⚠️ 此次仅进行版本号更新,不进行压缩。");
616
+ process.exit(0);
617
+ }
618
+ return { deleteOldFile };
619
+ }
620
+
621
+ /**
622
+ * 进行版本大小排序
623
+ * @param {Array} versionList
624
+ * @returns
625
+ */
626
+ function sortVersion(versionList = []) {
627
+ return versionList.sort((a, b) => {
628
+ const aParts = a.version.split(".");
629
+ const bParts = b.version.split(".");
630
+ for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
631
+ const aPart = parseInt(aParts[i] || "0", 10);
632
+ const bPart = parseInt(bParts[i] || "0", 10);
633
+ if (aPart !== bPart) {
634
+ return bPart - aPart;
635
+ }
636
+ }
637
+ return 0;
638
+ });
639
+ }
640
+
641
+ /**
642
+ * 递增版本号
643
+ * @param {String} version - 版本号
644
+ * @param {String} mainVersion - 主版本号
645
+ * @returns
646
+ */
647
+ function incrementVersion(version = "", mainVersion = "") {
648
+ const parts = version.split(".");
649
+ for (let i = parts.length - 1; i >= 1; i--) {
650
+ if (parseInt(parts[i]) < 9) {
651
+ parts[i] = (parseInt(parts[i]) + 1).toString();
652
+ break;
653
+ } else {
654
+ parts[i] = "0";
655
+ parts[i - 1] = (parseInt(parts[i - 1]) + 1).toString();
656
+ break;
657
+ }
658
+ }
659
+ if (mainVersion) {
660
+ parts[0] = mainVersion;
661
+ }
662
+ return parts.join(".");
663
+ }
664
+
665
+ /**
666
+ * 将发版文案复制到剪贴板
667
+ * @param {String} text - 要复制的文本
668
+ */
669
+ function copyToClipboard(text) {
670
+ return new Promise((resolve, reject) => {
671
+ let command;
672
+ if (process.platform === "win32") {
673
+ command = "clip";
674
+ } else if (process.platform === "darwin") {
675
+ command = "pbcopy";
676
+ } else {
677
+ // Linux/WSL
678
+ // 优先用 xclip,其次 xsel
679
+ command = "xclip -selection clipboard || xsel --clipboard";
680
+ }
681
+ const child = exec(command, (error) => {
682
+ if (error) {
683
+ console.error(
684
+ global.logColor.error,
685
+ `❌ 文本复制失败: ${error}\n请确保已安装 xclip 或 xsel(Linux),或手动复制:\n${text}`
686
+ );
687
+ reject(error);
688
+ } else {
689
+ console.log(global.logColor.success, `✅ 文本已复制到剪贴板\n${text}`);
690
+ resolve(text);
691
+ }
692
+ });
693
+ // 确保使用UTF-8编码写入
694
+ if (process.platform === "win32") {
695
+ child.stdin.write(Buffer.from(text, "utf8"));
696
+ } else {
697
+ child.stdin.write(text, "utf8");
698
+ }
699
+ child.stdin.end();
700
+ });
701
+ }
702
+
703
+ /**
704
+ * 脚本配置初始化
705
+ * @returns
706
+ */
707
+ function scriptInit() {
708
+ let tempInflyConfig = {};
709
+ if ((!isExistVueConfigFile && !hasNextConfig) || !fs.existsSync(targetProjectPackageJSON)) {
710
+ return;
711
+ }
712
+ if (shouldManageBuildScripts(targetInflyConfig)) {
713
+ scriptWrite(targetProjectPackageJSON, buildList);
714
+ }
715
+
716
+ if (targetInflyConfig) {
717
+ return;
718
+ }
719
+
720
+ // 如果存在以前的版本管理文件则进行配置迁移
721
+ if (isExistTargetProjectVersionConfig) {
722
+ tempInflyConfig = JSON.parse(fs.readFileSync(targetProjectVersionConfigPath, "utf-8"));
723
+ const { versionConfig = {}, buildConfigs = {} } = tempInflyConfig || {};
724
+ if (versionConfig.versionList) {
725
+ tempInflyConfig.versionConfig.versionList = []; // 删除旧版本列表
726
+ }
727
+ if (buildConfigs.buildFoldName) {
728
+ tempInflyConfig.buildConfigs.buildFoldName = "";
729
+ }
730
+ }
731
+
732
+ tempInflyConfig = { ...currentInflyConfig, ...tempInflyConfig };
733
+
734
+ scriptWrite(targetProjectPackageJSON, tempInflyConfig, npmPackageConfigKey);
735
+
736
+ if (isExistTargetProjectVersionConfig) {
737
+ console.log(global.logColor.success, `✅ ${versionFileName}配置迁移成功, 可手动删除旧版本控制文件`);
738
+ }
739
+ }
740
+
741
+ /**
742
+ * 复制版本管理文件模板到根目录
743
+ * @returns
744
+ */
745
+ function copyVersionConfigFile() {
746
+ if (!isExistVueConfigFile && !hasNextConfig) {
747
+ return;
748
+ }
749
+ const { name: projectTitle } = configureWebpack || {};
750
+ if (!isExistTargetProjectVersionConfig && fs.existsSync(originConfigPath)) {
751
+ // 创建可读流和可写流
752
+ const readStream = fs.createReadStream(originConfigPath);
753
+ const writeStream = fs.createWriteStream(targetProjectVersionConfigPath);
754
+
755
+ readStream.pipe(writeStream);
756
+
757
+ writeStream.on("finish", () => {
758
+ // 第二阶段:修改复制后的文件
759
+ fs.readFile(targetProjectVersionConfigPath, "utf8", (err, data) => {
760
+ if (!targetProjectOutputDir && !projectTitle) {
761
+ return;
762
+ }
763
+
764
+ if (err) {
765
+ console.error(global.logColor.error, `❌ 读取复制文件失败: ${err.message}`);
766
+ return;
767
+ }
768
+
769
+ const tempVersionConfig = JSON.parse(data);
770
+
771
+ if (targetProjectOutputDir) {
772
+ tempVersionConfig.buildConfigs.buildFoldName = targetProjectOutputDir;
773
+ }
774
+
775
+ if (projectTitle && !tempVersionConfig.versionConfig?.platformName) {
776
+ tempVersionConfig.versionConfig.platformName = projectTitle;
777
+ }
778
+
779
+ const tempVersionConfigStr = JSON.stringify(tempVersionConfig, null, 2);
780
+
781
+ fs.writeFile(targetProjectVersionConfigPath, tempVersionConfigStr, (err) => {
782
+ if (err) {
783
+ console.error(global.logColor.error, `❌ 读取vue.config.js配置更新失败: ${err.message}`);
784
+ } else {
785
+ console.log(global.logColor.success, `✅ 读取vue.config.js配置更新成功`);
786
+ }
787
+ });
788
+ });
789
+ console.log(global.logColor.success, `✅ 版本JSON文件已成功复制到根目录:${targetProjectVersionConfigPath}`);
790
+ });
791
+
792
+ writeStream.on("error", (err) => {
793
+ console.error(global.logColor.error, `❌ 复制版本JSON文件失败:${err.message}`);
794
+ });
795
+ }
796
+ }
797
+
798
+ /**
799
+ * 构建前分支处理
800
+ */
801
+ async function beforeBuild() {
802
+ const targetBranch = getBuildTargetBranch(gitAutoPushReposBranchMap);
803
+
804
+ await validateConfig(buildConfig, targetBranch);
805
+
806
+ if (
807
+ hasArg("--check-only") ||
808
+ buildContext.checkOnly === true ||
809
+ (process.env.INFLY_SKIP_BRANCH_UPDATE === "1" && process.env.INFLY_SKIP_PACKAGES_UPDATE === "1")
810
+ ) {
811
+ return;
812
+ }
813
+
814
+ const currentProjectStatus = checkSingleDirGitStatus(projectRoot, projectName);
815
+
816
+ if (currentProjectStatus.hasUncommitted) {
817
+ console.log(global.logColor.error, `\n❌ 当前项目存在未提交的更改,请先提交或暂存后再继续构建`);
818
+ process.exit(1);
819
+ }
820
+
821
+ const currentProjectUpdateResult = pullSingleDirFromBranch(projectRoot, projectName, targetBranch);
822
+
823
+ if (!currentProjectUpdateResult.success) {
824
+ console.log(global.logColor.error, `\n❌ 当前项目切换到 ${targetBranch} 分支失败,请检查`);
825
+ process.exit(1);
826
+ }
827
+
828
+ await autoBranchProcess({
829
+ gitAutoPushRepos,
830
+ gitAutoPushReposBranchMap,
831
+ currentBranch: targetBranch,
832
+ targetProjectOutputDir,
833
+ validateConfig: () => {}
834
+ });
835
+
836
+ if (process.env.INFLY_SKIP_PACKAGES_UPDATE !== "1") {
837
+ await checkAndUpdatePackages(undefined, "master");
838
+ }
839
+
840
+ // 5. 检查并更新兄弟 app 依赖(通过 build config 中的 alias 推断,无需硬编码 app 名称)
841
+ if (process.env.INFLY_SKIP_SIBLING_UPDATE !== "1") {
842
+ const siblingDirs = resolveSiblingAppDeps(projectRoot);
843
+
844
+ for (const siblingDir of siblingDirs) {
845
+ const siblingName = path.basename(siblingDir);
846
+ const siblingStatus = checkSingleDirGitStatus(siblingDir, siblingName);
847
+
848
+ if (siblingStatus.hasUncommitted) {
849
+ console.log(global.logColor.error, `\n❌ 兄弟项目 ${siblingName} 存在未提交的更改,请先提交或暂存后再继续构建`);
850
+ process.exit(1);
851
+ }
852
+
853
+ const siblingUpdateResult = pullSingleDirFromBranch(siblingDir, siblingName, targetBranch);
854
+
855
+ if (!siblingUpdateResult.success) {
856
+ console.log(global.logColor.warning, `\n⚠️ 兄弟项目 ${siblingName} 更新失败,但可以继续执行`);
857
+ }
858
+ }
859
+ }
860
+ }
861
+ module.exports = {
862
+ init,
863
+ scriptInit,
864
+ copyVersionConfigFile,
865
+ beforeBuild
866
+ };