infly-libs
Version:
工具组件库
585 lines (525 loc) • 20.9 kB
JavaScript
/**
* 构建工具
* @description
* 工具实现根据配置自动切换分支,构建到配置的对应文件夹路径,并且执行自动提交推送,也可执行自动压缩提交到当前项目
*/
const path = require("path");
const fs = require("fs");
const { exec } = require("child_process");
global.logColor = {
success: "\n\x1b[32m%s\x1b[0m",
error: "\n\x1b[31m%s\x1b[0m",
warning: "\n\x1b[33m%s\x1b[0m",
link: "\x1b[34m%s\x1b[0m"
};
const { zipSync } = require("./zip.js");
const { buildList } = require("../../script");
const { init: previewInit } = require("../../tools/project-preview.js");
const { postVersionFileAndMsg } = require("../../script/webhook/webhook.js");
const { scriptWrite, targetProjectFileResolve, currentProjectFileResolve } = require("../../tools/file-process.js");
const {
autoBranchProcess,
checkGitStatus,
autoGitProcess,
getCurrentBranch,
getLatestCommitInfo,
getLastCommitByFile
} = require("../../script/git-automation");
const scriptEvent = process.env.npm_lifecycle_event;
const isBuilZipScript = process.env.npm_lifecycle_event;
const testScript = "infly:build:test";
const versionFileName = "package.json";
const oldVersionFileName = "version-config.json";
const npmPackageConfigKey = "infly";
const currentBranch = getCurrentBranch();
const projectRoot = process.cwd();
// 依赖包的配置
const originConfigPath = currentProjectFileResolve("template-version-config.json"); // 模板项目文件
const currentProjectPackageJSON = currentProjectFileResolve("../package.json"); // 模板项目文件
const currentPackageJsonConfig = JSON.parse(fs.readFileSync(currentProjectPackageJSON, "utf-8"));
const { infly: currentInflyConfig } = currentPackageJsonConfig || {};
// 安装依赖的项目配置
const targetProjectVersionConfigPath = targetProjectFileResolve(oldVersionFileName);
const targetProjectVueConfigPath = targetProjectFileResolve("vue.config.js");
const targetProjectPackageJSON = targetProjectFileResolve("package.json");
const targetPackageJsonConfig = JSON.parse(fs.readFileSync(targetProjectPackageJSON, "utf-8"));
const { name: projectName, version: projectVersion = "", infly: targetInflyConfig } = targetPackageJsonConfig || {};
const [major = "", minor = "", patch = ""] = projectVersion.split(".");
const isExistTargetProjectVersionConfig = fs.existsSync(targetProjectVersionConfigPath); // 安装依赖项目下是否存在旧版本控制文件
const isExistVueConfigFile = fs.existsSync(targetProjectVueConfigPath); // 安装依赖项目下是否存在vue.config.js文件
const isMaster = currentBranch === "master"; // 是否在master分支
const dealBranch = isMaster ? "master" : "release";
const isPkg = versionFileName === "package.json";
const targetProjectVueConfig = isExistVueConfigFile ? require(targetProjectVueConfigPath) : {};
const { outputDir: targetProjectOutputDir, configureWebpack } = targetProjectVueConfig || {};
const { updateDevText } =
{ master: { updateDevText: "正式环境" }, release: { updateDevText: "测试环境" } }[dealBranch] || {}; // 更新环境和处理分支
const configPath = path.resolve(projectRoot, versionFileName);
let buildConfig = JSON.parse(fs.readFileSync(configPath, "utf-8")) || {};
if (buildConfig[npmPackageConfigKey]) {
buildConfig = buildConfig[npmPackageConfigKey];
}
// 准备构建信息
const { zipOptions, versionConfig } = buildConfig || {};
const {
buildFoldName: zipBuildFoldName, // 构建后文件名称
outputPath, // 输出路径
gitAuto, // 是否自动提交git
gitAutoPushRepos, // 自动提交文件到对应仓库
gitAutoPushReposBranchMap = {}, // 自动提交文件到对应仓库分支配置
gitAutoCommitText = "", // git自动提交信息
enablePreview, // 启用预览
openExplorer, // 启用打开资源管理器
webhookUrl, // 企业微信机器人链接
webhookAtUser, // 企业微信机器人@用户
enableClipboard, // 启用粘贴板
disableUpdateVersionEnv // 禁止更新版本信息环境
} = zipOptions || {};
const buildFoldName = zipBuildFoldName || targetProjectOutputDir || "dist";
const { mainVersion: verMainVersion, platformName, baseName, versionList = [], links } = versionConfig || {};
const mainVersion = verMainVersion || major; // 主版本号
const [lastestVersionItem] = versionList || [];
const { lastVersion = projectVersion || "1.0.0" } = lastestVersionItem || {};
function init() {
try {
validateConfig(buildConfig);
validateFileExists(configPath, versionFileName);
// 处理版本信息
const zipFiles = handleZipFiles(`-${buildFoldName}-v`);
const { deleteOldFile } = updateVersionList(zipFiles, {
buildConfig,
configPath
});
// 处理所需版本文件信息
const timestamp = new Date()
.toISOString()
.slice(0, 10)
.replace(/-/g, ".");
const baseFileName = `${platformName}-${buildFoldName}-${baseName}`;
const newVersion = isMaster ? incrementVersion(lastVersion, mainVersion) : lastVersion; // 非正式环境构建不更新版本信息
const newFileName = baseFileName.replace("{version}", newVersion).replace("{timestamp}", timestamp);
const gitInfo = getLatestCommitInfo() || {};
const { branch, commitMsg } = gitInfo || {};
const tempLink = typeof links === "object" ? links[dealBranch] : links || "";
const publishText = `${platformName}:\n更新到:${tempLink}\n更新时间:${timestamp}\n版本号:v${newVersion}\n分支:${branch}\n更新环境:${updateDevText}\n更新内容:\n${commitMsg}`;
const newZipFileInfo = [
{
version: newVersion,
description: "",
timestamp,
fileName: newFileName,
ps: "【自动压缩命令生成的版本】",
gitInfo
}
];
// 处理文件路径
const outputDir = path.resolve(projectRoot, "./", outputPath);
const outputPathFull = path.join(outputDir, newFileName);
const distPath = gitAutoPushRepos ? targetProjectOutputDir : path.resolve(projectRoot, "./" + buildFoldName);
const indexHtmlPath = path.join(distPath, "index.html");
// 校验构建文件是否存在,确保构建成功
validateFileExists(distPath, "distPath");
validateFileExists(indexHtmlPath, "indexHtmlPath");
// 非自动推送到构建仓库,进行构建文件压缩和检查是否压缩成功
if (!gitAutoPushRepos) {
zipSync(distPath, outputPathFull, true);
validateFileExists(outputPathFull, "outputPathFull");
}
updateVersionList(newZipFileInfo, { opt: "add", buildConfig, configPath });
if (gitAuto || gitAutoPushRepos) {
const checkGitStatusParams = {
newVersion,
deleteOldFile,
versionFileName,
gitAutoPushRepos,
targetProjectOutputDir
};
checkGitStatus(buildFoldName, newFileName, checkGitStatusParams);
autoGitProcess(buildFoldName, newFileName, {
gitInfo,
gitAutoCommitText,
gitAutoPushReposBranchMap,
...checkGitStatusParams
});
}
if (enableClipboard) {
copyToClipboard(publishText);
}
if (enablePreview) {
previewInit(true);
}
if (openExplorer) {
exec(`explorer ${outputDir}`);
}
if (webhookUrl) {
postVersionFileAndMsg(publishText, {
projectName,
newFileName,
gitInfo,
targetProjectOutputDir,
...zipOptions
});
}
} catch (error) {
console.error(global.logColor.error, `❌ 自动构建脚本执行出错, 错误信息:\n${error}`);
process.exit(1);
}
}
/**
* 校验配置文件
* @param {Object} buildConfig
*/
function validateConfig(buildConfig) {
if (!buildConfig) {
console.error(global.logColor.error, `❌ ZIP压缩已禁用,版本控制文件${versionFileName}配置错误`);
process.exit(1);
}
if (buildConfig?.zipOptions?.enabled === false) {
console.warn(global.logColor.error, `⚠️ ZIP压缩已禁用,版本控制文件${versionFileName}中的enabled属性设置为false`);
process.exit(0);
}
if (!/^release/.test(currentBranch) && currentBranch !== "master") {
if (![testScript].includes(scriptEvent)) {
console.error(global.logColor.error, `❌ 当前分支非release、release/xx、master等可构建分支,请检查`);
process.exit(1);
}
}
if (currentBranch !== "master") {
console.warn(global.logColor.warning, `⚠️ 当前分支为 ${currentBranch},非测试需求请在 master/release/release/xxx 等可构建分支上执行此脚本`);
}
}
/**
* 校验文件是否存在
* @param {String} checkPath - 检查路径
* @param {String} logKey - 校验文案键值
*/
function validateFileExists(checkPath, logKey) {
const { error, success, link } = {
[versionFileName]: {
error: `版本控制文件${versionFileName}不存在,请检查`
},
distPath: {
error: "构建文件夹不存在,请检查构建配置"
},
indexHtmlPath: {
error: "index.html文件不存在,请检查构建配置"
},
outputPathFull: {
error: "压缩文件没有成功创建,请检查构建配置",
success: `压缩完成,已成功创建文件`,
link: checkPath
},
configPath: {
error: `新版本写入${versionFileName}失败,请检查版本文件是否存在`,
success: `新版本写入${versionFileName}成功,请点击文件链接确认版本信息无误`,
link: checkPath
}
}[logKey];
if (!fs.existsSync(checkPath)) {
console.error(global.logColor.error, `❌ ${error}`);
process.exit(1);
}
if (success) {
console.log(global.logColor.success, `✅ ${success}`);
}
if (link) {
console.log(global.logColor.link, `${link}`);
}
}
/**
* 处理压缩文件
* @param {String} checkFileName - 检查文件名
* @param {Function} callback - 回调函数
* @returns
*/
function handleZipFiles(checkFileName) {
const files = fs.readdirSync(projectRoot);
const zipFiles = [];
files.forEach(file => {
if (path.extname(file).toLowerCase() === ".zip" && path.basename(file).includes(checkFileName)) {
const [platformName, versionTimestamp = ""] = file.split(checkFileName);
const [version, timestamp] = versionTimestamp.split("-");
zipFiles.push({
version,
description: "",
timestamp,
fileName: file
});
}
});
return zipFiles;
}
/**
* 将版本数组转为对象,方便查找匹配
* @param {Array} versionList
* @returns
*/
function createVersionMap(versionList = []) {
return versionList.reduce((acc, item) => {
acc[item.fileName] = item.version;
return acc;
}, {});
}
/**
* 检查是否禁用更新版本文件环境
* @param {Boolean} disableUpdateVersionEnv - 禁用环境配置
*/
function checkIsDisableUpdateVersionEnv(disableUpdateVersionEnv = []) {
let isDisableUpdateVersionEnv = process.env.npm_lifecycle_script.includes(`--mode staging`); // 默认禁用测试环境更新版本
if (Array.isArray(disableUpdateVersionEnv) && disableUpdateVersionEnv.length > 0) {
disableUpdateVersionEnv.forEach(item => {
if (process.env.npm_lifecycle_script.includes(`--mode ${item}`)) {
isDisableUpdateVersionEnv = disableUpdateVersionEnv.includes(item);
}
});
}
return isDisableUpdateVersionEnv;
}
/**
* 更新版本文件列表
* @param {Array} zipFiles - ZIP 文件列表
* @param {Object} extraParams - 额外传参
* @returns
*/
function updateVersionList(zipFiles = [], extraParams) {
const { opt = "update", buildConfig, configPath } = extraParams || {};
const { versionConfig, zipOptions } = buildConfig || {};
const { versionList = [] } = versionConfig || {};
const { disableUpdateVersionEnv } = versionConfig || {};
const isDisableUpdateVersionEnv = checkIsDisableUpdateVersionEnv(disableUpdateVersionEnv);
const versionMap = createVersionMap(versionList);
const deleteOldFile = [];
const [newVersionItem] = zipFiles || [];
const { version: newVersion } = newVersionItem || {};
let allVersionExist = true; // 是否所有版本都存在于 version-config.json 中
if (zipFiles.length === 0) {
if (!isPkg) {
console.warn(global.logColor.warning, "⚠️ 没有 ZIP 文件,跳过版本更新。");
}
} else {
zipFiles.forEach(zipFile => {
const { version, fileName, ps = "【非压缩命令生成的版本】", gitInfo = getLastCommitByFile(fileName) } = zipFile;
const singleVersionInfo = { ...zipFile, ps, gitInfo };
// 如果存在没有写入版本配置文件的压缩文件,则进行写入操作
if (!versionMap[fileName] && Array.isArray(versionList)) {
allVersionExist = false;
buildConfig.versionConfig.versionList.unshift(singleVersionInfo);
console.log(global.logColor.success, `✅ 新版本${version}写入${versionFileName}文件更新成功`);
}
// 如果是更新,压缩脚本进行打包则删除全部旧文件
if (isBuilZipScript && opt === "update") {
deleteOldFile.push(fileName);
fs.unlinkSync(path.join(projectRoot, fileName));
console.log(global.logColor.success, `✅ 已删除 ZIP 文件: ${fileName}`);
}
});
}
if (versionList.length > 0) {
sortVersion(buildConfig.versionConfig.versionList);
}
// 非禁止更新更新版本信息的环境正常更新版本信息(比如production)
if (!isDisableUpdateVersionEnv) {
let tempWriteConfig = buildConfig;
if (targetPackageJsonConfig[npmPackageConfigKey]) {
const { name: oldProjectName, version: oldVersion, ...otherPkgConfig } = targetPackageJsonConfig;
if (buildConfig.versionConfig.versionList) {
buildConfig.versionConfig.versionList = [];
}
targetPackageJsonConfig[npmPackageConfigKey] = buildConfig || {};
tempWriteConfig = {
name: projectName,
version: newVersion,
...otherPkgConfig
};
}
fs.writeFileSync(configPath, JSON.stringify(tempWriteConfig, null, 2));
}
if (allVersionExist) {
if (!isPkg) {
console.log(global.logColor.success, `✅ 所有文件版本已存在于${versionFileName}中,无需更新`);
}
} else if (!isBuilZipScript || opt !== "update") {
// 如果是自动压缩的命令,只在完成压缩更新版本后统一输出,更新的时候不输出减少重复信息
validateFileExists(configPath, "configPath");
}
// 如果不是自动压缩的命令,在更新版本json文件后退出
if (!isBuilZipScript) {
console.warn(global.logColor.warning, "⚠️ 此次仅进行版本号更新,不进行压缩。");
process.exit(0);
}
return { deleteOldFile };
}
/**
* 进行版本大小排序
* @param {Array} versionList
* @returns
*/
function sortVersion(versionList = []) {
return versionList.sort((a, b) => {
const aParts = a.version.split(".");
const bParts = b.version.split(".");
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
const aPart = parseInt(aParts[i] || "0", 10);
const bPart = parseInt(bParts[i] || "0", 10);
if (aPart !== bPart) {
return bPart - aPart;
}
}
return 0;
});
}
/**
* 递增版本号
* @param {String} version - 版本号
* @param {String} mainVersion - 主版本号
* @returns
*/
function incrementVersion(version = "", mainVersion = "") {
const parts = version.split(".");
for (let i = parts.length - 1; i >= 1; i--) {
if (parseInt(parts[i]) < 9) {
parts[i] = (parseInt(parts[i]) + 1).toString();
break;
} else {
parts[i] = "0";
parts[i - 1] = (parseInt(parts[i - 1]) + 1).toString();
break;
}
}
if (mainVersion) {
parts[0] = mainVersion;
}
return parts.join(".");
}
/**
* 将发版文案复制到剪贴板
* @param {String} text - 要复制的文本
*/
function copyToClipboard(text) {
return new Promise((resolve, reject) => {
let command;
if (process.platform === "win32") {
command = "clip";
} else if (process.platform === "darwin") {
command = "pbcopy";
} else {
// Linux/WSL
// 优先用 xclip,其次 xsel
command = "xclip -selection clipboard || xsel --clipboard";
}
const child = exec(command, error => {
if (error) {
console.error(
global.logColor.error,
`❌ 文本复制失败: ${error}\n请确保已安装 xclip 或 xsel(Linux),或手动复制:\n${text}`
);
reject(error);
} else {
console.log(global.logColor.success, `✅ 文本已复制到剪贴板\n${text}`);
resolve(text);
}
});
// 确保使用UTF-8编码写入
if (process.platform === "win32") {
child.stdin.write(Buffer.from(text, "utf8"));
} else {
child.stdin.write(text, "utf8");
}
child.stdin.end();
});
}
/**
* 脚本配置初始化
* @returns
*/
function scriptInit() {
let tempInflyConfig = {};
if (!isExistVueConfigFile || !fs.existsSync(targetProjectPackageJSON)) {
return;
}
scriptWrite(targetProjectPackageJSON, buildList);
if (targetInflyConfig) {
return;
}
// 如果存在以前的版本管理文件则进行配置迁移
if (isExistTargetProjectVersionConfig) {
tempInflyConfig = JSON.parse(fs.readFileSync(targetProjectVersionConfigPath, "utf-8"));
const { versionConfig = {}, zipOptions = {} } = tempInflyConfig || {};
if (versionConfig.versionList) {
tempInflyConfig.versionConfig.versionList = []; // 删除旧版本列表
}
if (zipOptions.buildFoldName) {
tempInflyConfig.zipOptions.buildFoldName = "";
}
}
tempInflyConfig = { ...currentInflyConfig, ...tempInflyConfig };
scriptWrite(targetProjectPackageJSON, tempInflyConfig, npmPackageConfigKey);
if (isExistTargetProjectVersionConfig) {
console.log(global.logColor.success, `✅ ${versionFileName}配置迁移成功, 可手动删除旧版本控制文件`);
}
}
/**
* 复制版本管理文件模板到根目录
* @returns
*/
function copyVersionConfigFile() {
if (!isExistVueConfigFile) {
return;
}
const { name: projectTitle } = configureWebpack || {};
if (!isExistTargetProjectVersionConfig && fs.existsSync(originConfigPath)) {
// 创建可读流和可写流
const readStream = fs.createReadStream(originConfigPath);
const writeStream = fs.createWriteStream(targetProjectVersionConfigPath);
readStream.pipe(writeStream);
writeStream.on("finish", () => {
// 第二阶段:修改复制后的文件
fs.readFile(targetProjectVersionConfigPath, "utf8", (err, data) => {
if (!targetProjectOutputDir && !projectTitle) {
return;
}
if (err) {
console.error(global.logColor.error, `❌ 读取复制文件失败: ${err.message}`);
return;
}
const tempVersionConfig = JSON.parse(data);
if (targetProjectOutputDir) {
tempVersionConfig.zipOptions.buildFoldName = targetProjectOutputDir;
}
if (projectTitle) {
tempVersionConfig.versionConfig.platformName = projectTitle;
}
const tempVersionConfigStr = JSON.stringify(tempVersionConfig, null, 2);
fs.writeFile(targetProjectVersionConfigPath, tempVersionConfigStr, err => {
if (err) {
console.error(global.logColor.error, `❌ 读取vue.config.js配置更新失败: ${err.message}`);
} else {
console.log(global.logColor.success, `✅ 读取vue.config.js配置更新成功`);
}
});
});
console.log(global.logColor.success, `✅ 版本JSON文件已成功复制到根目录:${targetProjectVersionConfigPath}`);
});
writeStream.on("error", err => {
console.error(global.logColor.error, `❌ 复制版本JSON文件失败:${err.message}`);
});
}
}
/**
* 构建前分支处理
*/
function beforeBuild() {
autoBranchProcess({
gitAutoPushRepos,
gitAutoPushReposBranchMap,
currentBranch,
targetProjectOutputDir,
validateConfig: () => validateConfig(buildConfig)
});
}
module.exports = {
init,
scriptInit,
copyVersionConfigFile,
beforeBuild
};