UNPKG

vite-auto-deploy

Version:

用于配合 PHP 自动打包并上传打包后的文件到服务器指定目录的插件,此插件需要配合 php [项目地址](https://gitee.com/mxp131011/auto_deploy/)

1,118 lines (1,117 loc) 40.4 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); //#region \0rolldown/runtime.js var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion let formdata_node = require("formdata-node"); let node_fs = require("node:fs"); let path = require("path"); path = __toESM(path, 1); let ora = require("ora"); ora = __toESM(ora, 1); let chalk = require("chalk"); chalk = __toESM(chalk, 1); let os = require("os"); os = __toESM(os, 1); let fs = require("fs"); fs = __toESM(fs, 1); let crypto = require("crypto"); crypto = __toESM(crypto, 1); let node_machine_id = require("node-machine-id"); node_machine_id = __toESM(node_machine_id, 1); let jszip = require("jszip"); jszip = __toESM(jszip, 1); let node_fetch = require("node-fetch"); node_fetch = __toESM(node_fetch, 1); let https = require("https"); https = __toESM(https, 1); let socket_io_client = require("socket.io-client"); let node_zlib = require("node:zlib"); node_zlib = __toESM(node_zlib, 1); //#region src/file-util.ts /** * 读取文件 * @param {string} readPath - 文件路径 */ function readFile(readPath) { if (fs.default.existsSync(readPath)) return fs.default.readFileSync(readPath, "utf8"); else return null; } /** * 删除已存在的文件 (不能删除目录) * @param {string} removePath - 需要删除的完整路径 如:C:\Users\m1310\Desktop\vue-router\dist\dist.zip */ function deleteFile(removePath) { fs.default.existsSync(removePath) && fs.default.unlinkSync(removePath); } /** * 写入文件 * @param {string} filePath - 文件路径 * @param {string} data - 写入的数据 */ function writeFile(filePath, data) { const directory = path.default.dirname(filePath); fs.default.mkdirSync(directory, { recursive: true }); fs.default.writeFileSync(filePath, data); } //#endregion //#region src/auto-deploy/encrypt.ts /** * 得到保存或读取密匙的路径 * @param saveSecretKeyDirectory - 获取或生成密钥的路径 */ function getecretKeyPath(saveSecretKeyDirectory) { const rootDirectory = saveSecretKeyDirectory || path.default.join(os.default.homedir(), ".vite-auto-deploy"); return { publicKeyPath: path.default.join(rootDirectory, "ed25519_public.pem"), privateKeyPath: path.default.join(rootDirectory, "ed25519_private.pem"), keyPairIdPath: path.default.join(rootDirectory, ".keyPairId") }; } /** * 创建密钥 * @param passphrase - 私钥的密码 * @param pathInfo - 密匙相关的保存路径 */ function createSecretKey(passphrase, pathInfo) { const { publicKey, privateKey } = crypto.default.generateKeyPairSync("ed25519", { publicKeyEncoding: { type: "spki", format: "pem" }, privateKeyEncoding: { type: "pkcs8", format: "pem", passphrase, cipher: "aes-256-cbc" } }); let keyPairId = ""; try { keyPairId = node_machine_id.default.machineIdSync(); } catch (error) {} keyPairId = keyPairId ? keyPairId : calculateSHA256(crypto.default.randomUUID()); keyPairId = calculateSHA256(calculateSHA256(publicKey).substring(8, 16) + keyPairId + calculateSHA256(privateKey).substring(8, 16)); writeFile(pathInfo.publicKeyPath, publicKey); writeFile(pathInfo.privateKeyPath, privateKey); writeFile(pathInfo.keyPairIdPath, keyPairId); return { publicKey, privateKey, keyPairId }; } /** * 得到密匙 (不存在就创建) * @param passphrase - 私钥的密码 * @param pathInfo - 密匙相关的保存路径 * @param resetKey -是否重置密匙 */ function getSecretKey(passphrase, pathInfo, resetKey, debug) { resetKey && deleteFile(pathInfo.publicKeyPath); resetKey && deleteFile(pathInfo.privateKeyPath); resetKey && deleteFile(pathInfo.keyPairIdPath); let keys = { privateKey: readFile(pathInfo.privateKeyPath) || "", publicKey: readFile(pathInfo.publicKeyPath) || "", keyPairId: readFile(pathInfo.keyPairIdPath) || "" }; if (!keys.privateKey || !keys.publicKey || !keys.keyPairId) { keys = createSecretKey(passphrase, pathInfo); debug && console.log("日志: 生成或重置密匙成功"); } else debug && console.log("日志: 获取密匙成功"); return { publicKey: keys.publicKey, privateKey: keys.privateKey, keyPairId: keys.keyPairId }; } /** * Ed25519签名 * @param originalText - 原始文本 * @param privateKey - 私钥 * @param passphrase - 私钥的密码 */ function signEd25519(originalText, privateKey, passphrase) { return crypto.default.sign(null, Buffer.from(originalText), { key: privateKey, passphrase }).toString("base64"); } /** * 计算文件或字符串的 MD5 哈希值 * @param filePath - 文件路径 */ function calculateSHA256(fileContent) { const hash = crypto.default.createHash("sha256"); hash.update(fileContent); return hash.digest("hex"); } //#endregion //#region src/auto-deploy/file-zip.ts /** * 生成zip文件 * @param {string} outDir - 需要压缩的文件目录 如C:\Users\m1310\Desktop\vue-router\dist * @param {string} zipName - zip文件名 * @param {boolean} saveLocalZip - 是否保存zip文件 */ async function createZip(outDir, zipName, saveLocalZip) { try { const catalog = path.default.resolve(outDir); const storagePath = path.default.join(catalog, zipName); deleteFile(storagePath); const buf = await zipDir(catalog); if (saveLocalZip) { deleteFile(storagePath); fs.default.writeFileSync(storagePath, buf); } return Promise.resolve(buf); } catch (error) { return Promise.reject({ msg: error.message }); } } /** * 生成zip文件 * @param {string} catalog - 需要压缩的文件目录 如C:\Users\m1310\Desktop\vue-router\dist */ function zipDir(catalog) { const jszip$1 = new jszip.default(); readDir(jszip$1, catalog); return jszip$1.generateAsync({ type: "nodebuffer", compression: "DEFLATE", compressionOptions: { level: 6 } }); } /** * 读取每个文件为buffer并放到存到jszip中 * @param {JSZip} jszip - JSZip实例 * @param {string} catalog - 需要压缩的文件目录 如C:\Users\m1310\Desktop\vue-router\dist */ function readDir(jszip$2, catalog) { fs.default.readdirSync(catalog).forEach((fileName) => { const fillPath = path.default.join(catalog, "./", fileName); if (fs.default.statSync(fillPath).isDirectory()) { const dirZip = jszip$2.folder(fileName); dirZip && readDir(dirZip, fillPath); } else jszip$2.file(fileName, fs.default.readFileSync(fillPath)); }); } //#endregion //#region src/auto-deploy/index.ts /** * Git 拉取部署 socket 事件名 */ var GIT_DEPLOY_SOCKET_EVENTS = { /** 客户端发起部署 */ start: "git-deploy:start", /** 客户端恢复订阅 */ resume: "git-deploy:resume", /** 服务端推送进度 */ progress: "git-deploy:progress", /** 服务端返回结果 */ result: "git-deploy:result" }; /** * Git 部署 socket 自动重连次数 */ var GIT_DEPLOY_SOCKET_RECONNECTION_ATTEMPTS = 20; /** * Git 部署 socket 自动重连初始等待毫秒数 */ var GIT_DEPLOY_SOCKET_RECONNECTION_DELAY_MS = 1e3; /** * 自发布的部署服务包名 */ var SELF_DEPLOY_SERVICE_PACKAGE_NAME = "upload-code-service"; /** * 自发布提前判成功的启动阶段提示 */ var SELF_DEPLOY_EARLY_SUCCESS_MESSAGES = new Set([ "正在重启服务", "正在首次启动服务", "正在启动新进程" ]); /** * 已在终端输出过的部署异常 */ var HandledDeployError = class extends Error { /** * 构造函数 * @param message - 异常消息 */ constructor(message) { super(message); this.name = "HandledDeployError"; } }; /** * 兼容获取 `ora` 工厂函数 * @returns `ora` 实例创建函数 */ function getOraFactory$1() { /** 当前待解析的模块值 */ let currentOraModule = ora.default; while (currentOraModule && typeof currentOraModule === "object" && "default" in currentOraModule && currentOraModule.default !== currentOraModule) currentOraModule = currentOraModule.default; if (typeof currentOraModule !== "function") throw new Error("ora 模块加载失败"); return currentOraModule; } /** * 兼容获取 `chalk` 实例 * @returns `chalk` 实例 */ function getChalkInstance$1() { /** 当前待解析的模块值 */ let currentChalkModule = chalk.default; while (currentChalkModule && typeof currentChalkModule === "object" && "default" in currentChalkModule && currentChalkModule.default !== currentChalkModule) currentChalkModule = currentChalkModule.default; if (!currentChalkModule || typeof currentChalkModule !== "function") throw new Error("chalk 模块加载失败"); return currentChalkModule; } /** 兼容后的 `chalk` 实例 */ var colorChalk$1 = getChalkInstance$1(); /** * 自动化部署插件 * @param {object} config - 插件配置 */ function viteAutoDeployPlugin(config) { let viteConfig = void 0; return { name: "vite-auto-deploy", apply: "build", enforce: "post", configResolved(resolvedConfig) { viteConfig = resolvedConfig; }, closeBundle: { sequential: true, order: "post", async handler() { try { await viteAutoDeply(config, viteConfig); } catch (error) {} } } }; } /** * 自动部署 */ async function viteAutoDeply(config, viteConfig) { try { /** `ora` 加载后的工厂函数 */ const createOra = getOraFactory$1(); const newConfig = { uploadUrl: "", deployMode: "build-zip-upload", websocketUrl: "", websocketPath: "/socket.io", projectKey: "", zipName: "viteAutoDeploy", saveLocalZip: false, passphrase: "vite-auto-deploy", autoDeploy: true, root: void 0, outDir: void 0, headers: {}, description: void 0, resetKey: false, debug: false, ...config || {} }; if (newConfig.autoDeploy ?? true) { const spinner = createOra({ spinner: "dots" }); const uploadUrl = newConfig.uploadUrl || false; const websocketUrl = newConfig.websocketUrl || false; const websocketPath = newConfig.websocketPath || false; const deployMode = newConfig.deployMode || "build-zip-upload"; const projectKey = newConfig.projectKey || false; console.log(""); spinner.start(colorChalk$1.blue("开始自动部署...")); if (deployMode === "build-zip-upload" && !uploadUrl) spinner.fail(colorChalk$1.red("自动部署失败,请配置uploadUrl")); else if (deployMode === "git-pull-run" && (!websocketUrl || !websocketPath)) spinner.fail(colorChalk$1.red("自动部署失败,请同时配置 websocketUrl、websocketPath")); else if (!projectKey) spinner.fail(colorChalk$1.red("自动部署失败,请配置projectKey")); else { newConfig.root = newConfig.root || viteConfig?.root || process.cwd(); let outDir = newConfig.outDir || viteConfig?.build?.outDir || "dist"; outDir = path.default.isAbsolute(outDir) ? outDir : path.default.join(newConfig.root, outDir); outDir = path.default.resolve(outDir); newConfig.outDir = outDir; await autoDeply(newConfig, newConfig.outDir, spinner); typeof newConfig.success === "function" && newConfig.success(newConfig, viteConfig); } console.log(""); } return Promise.resolve(true); } catch (error) { if (error instanceof HandledDeployError) return Promise.reject(error); if (error instanceof Error) return Promise.reject(new HandledDeployError(error.message || "自动部署失败")); return Promise.reject(new HandledDeployError(String(error) || "自动部署失败")); } } /** * 自动部署 */ async function autoDeply(config, outDir, spinner) { const uploadUrl = config.uploadUrl || ""; const websocketUrl = config.websocketUrl || ""; const websocketPath = config.websocketPath || ""; const projectKey = config.projectKey || ""; const passphrase = config.passphrase || "vite-auto-deploy"; const deployMode = config.deployMode || "build-zip-upload"; const { publicKey, privateKey, keyPairId } = getSecretKey(passphrase, getecretKeyPath(config.saveSecretKeyDirectory), config.resetKey || false, config.debug || false); config.debug && console.log("日志: 公匙=======" + publicKey); config.debug && console.log("日志: 私匙=======" + privateKey); config.debug && console.log("日志: 密匙对ID==== " + keyPairId); try { if (deployMode === "git-pull-run") { if (!websocketUrl || !websocketPath) { spinner.fail(colorChalk$1.red("自动部署失败,请同时配置 websocketUrl、websocketPath")); return Promise.reject(new HandledDeployError("自动部署失败")); } await gitPullRunDeploy(config, websocketUrl, websocketPath, projectKey, keyPairId, privateKey, passphrase, spinner); } else { const zipName = `${config.zipName || "vite-auto-deploy"}.zip`; const zipBuffer = await createZip(outDir, zipName, config.saveLocalZip); const fileSHA256 = calculateSHA256(zipBuffer); const timestamp = (/* @__PURE__ */ new Date()).getTime(); const postData = { projectKey, keyPairId, sign: "", timestamp }; postData.sign = signEd25519(`${calculateSHA256(Math.floor(timestamp % 1e4 * 333.3).toString())}${fileSHA256}${keyPairId}${calculateSHA256(projectKey.toString())}`, privateKey, passphrase); await uploading(uploadUrl, zipBuffer, zipName, postData, config.headers || {}, config.rejectUnauthorized || false, config.debug || false); } spinner.succeed(colorChalk$1.bold.green(`自动部署完成!`)); return Promise.resolve(true); } catch (err) { const deployError = err; config.debug && console.log("日志: 错误信息2:", err); spinner.fail(colorChalk$1.bold.red(deployError.message || deployError.msg || "自动部署失败")); if (shouldPrintKeyPairInfo(deployError)) { spinner.info("公钥:"); spinner.info(colorChalk$1.green(" " + normalizePublicKeyForDisplay(publicKey))); spinner.info("密钥对ID:"); spinner.info(" " + colorChalk$1.green(keyPairId)); } console.log(""); return Promise.reject(new HandledDeployError(deployError.message || deployError.msg || "自动部署失败")); } } /** * Git 拉取并运行部署 * @param config - 插件配置 * @param websocketUrl - WebSocket 服务地址 * @param websocketPath - WebSocket 握手路径 * @param projectKey - 项目标识 * @param keyPairId - 密钥对 ID * @param privateKey - 私钥 * @param passphrase - 私钥密码 * @param spinner - 终端加载器 * @returns 部署结果 */ async function gitPullRunDeploy(config, websocketUrl, websocketPath, projectKey, keyPairId, privateKey, passphrase, spinner) { /** 时间戳 */ const timestamp = (/* @__PURE__ */ new Date()).getTime().toString(); /** 当前部署环境,默认使用 default */ const deployEnvironment = config.deployEnvironment || "default"; /** Git 签名原文 */ const signStr = buildGitDeploySignText(projectKey, keyPairId, Number(timestamp), deployEnvironment, config.description); await runGitDeploySocket(websocketUrl, websocketPath, { taskId: createGitDeployTaskId(projectKey), projectKey, keyPairId, sign: signEd25519(signStr, privateKey, passphrase), timestamp, deployEnvironment, description: config.description }, spinner, config.rejectUnauthorized || false, config.debug || false, config); } /** * 运行 Git 部署 socket 连接 * @param websocketUrl - WebSocket 服务地址 * @param websocketPath - WebSocket 握手路径 * @param payload - 部署参数 * @param spinner - 终端加载器 * @param rejectUnauthorized - 是否校验证书 * @param debug - 是否打印日志 * @returns 部署结果 */ async function runGitDeploySocket(websocketUrl, websocketPath, payload, spinner, rejectUnauthorized, debug, config) { /** 完整连接地址 */ const websocketConfig = buildSocketConfig(websocketUrl, websocketPath); /** 是否为部署服务自发布 */ const isSelfDeployService = isSelfDeployUploadCodeService(config, payload.projectKey); /** 部署任务 ID */ const taskId = payload.taskId || createGitDeployTaskId(payload.projectKey); payload.taskId = taskId; await new Promise((resolvePromise, rejectPromise) => { /** 是否已完成 */ let isFinished = false; /** 是否至少成功连接过一次 */ let hasConnectedOnce = false; /** 是否已发起过 start 事件 */ let hasSentStart = false; /** 已输出的进度事件键 */ const printedProgressEventKeys = /* @__PURE__ */ new Set(); /** 已输出的结果事件键 */ const printedResultEventKeys = /* @__PURE__ */ new Set(); /** socket 实例 */ const socket = (0, socket_io_client.io)(websocketConfig.url, { path: websocketConfig.path, transports: ["websocket"], reconnection: true, reconnectionAttempts: GIT_DEPLOY_SOCKET_RECONNECTION_ATTEMPTS, reconnectionDelay: GIT_DEPLOY_SOCKET_RECONNECTION_DELAY_MS, reconnectionDelayMax: GIT_DEPLOY_SOCKET_RECONNECTION_DELAY_MS * 3, rejectUnauthorized: !rejectUnauthorized }); /** * 结束连接 * @param error - 错误对象 * @returns void */ function finalize(error) { if (isFinished) return; isFinished = true; socket.removeAllListeners(); socket.io.removeAllListeners(); socket.disconnect(); if (error) { rejectPromise(error); return; } resolvePromise(); } /** * 输出部署进度 * @param event - 进度事件 */ function printProgressEvent(event) { const eventKey = buildGitDeployProgressEventKey(event); if (printedProgressEventKeys.has(eventKey)) return; printedProgressEventKeys.add(eventKey); spinner.text = colorChalk$1.blue(event.message); if (event.detail && event.status !== "failed") console.log(colorChalk$1.gray(event.detail)); } /** * 输出部署结果 * @param event - 结果事件 */ function printResultEvent(event) { const eventKey = buildGitDeployResultEventKey(event); if (printedResultEventKeys.has(eventKey)) return; printedResultEventKeys.add(eventKey); if (!event.data) return; console.log(colorChalk$1.green(`版本号: ${event.data.version}`)); console.log(colorChalk$1.green(`部署时间: ${event.data.deployTime}`)); console.log(colorChalk$1.green(`提交说明: ${event.data.commitMessage}`)); if (event.data.updateSummaries.length > 0) { console.log(colorChalk$1.green("更新内容:")); for (const summary of event.data.updateSummaries) console.log(colorChalk$1.green(summary)); } } socket.on("connect", () => { debug && console.log("日志: Git 部署 socket 已连接:", websocketConfig.url, "path:", websocketConfig.path); hasConnectedOnce = true; if (!hasSentStart) { spinner.text = colorChalk$1.blue("正在发起 Git 拉取部署..."); socket.emit(GIT_DEPLOY_SOCKET_EVENTS.start, payload); hasSentStart = true; return; } spinner.text = colorChalk$1.blue(`部署连接已恢复,正在同步任务进度... taskId=${taskId}`); const resumePayload = { taskId }; socket.emit(GIT_DEPLOY_SOCKET_EVENTS.resume, resumePayload); }); socket.on(GIT_DEPLOY_SOCKET_EVENTS.progress, (event) => { debug && console.log("日志: Git 部署进度:", event); if (event.taskId && event.taskId !== taskId) return; printProgressEvent(event); if (shouldFinalizeSelfDeployEarly(event, isSelfDeployService)) { spinner.info(colorChalk$1.yellow("检测到 upload-code-service 正在自发布,服务即将重启,提前结束等待并视为部署成功")); finalize(); } }); socket.on(GIT_DEPLOY_SOCKET_EVENTS.result, (event) => { debug && console.log("日志: Git 部署结果:", event); if (event.taskId && event.taskId !== taskId) return; if (!event.success) { finalize({ msg: event.message, code: event.errorCode ?? (event.failedStep === "auth" ? 427 : -201), message: event.message, reason: event.reason }); return; } printResultEvent(event); finalize(); }); socket.io.on("reconnect_attempt", (attempt) => { debug && console.log(`日志: Git 部署正在尝试重连,第 ${attempt} 次`); if (!isFinished) spinner.text = colorChalk$1.yellow(`部署连接已断开,正在尝试重连... 第 ${attempt} 次`); }); socket.io.on("reconnect_failed", () => { finalize({ msg: `部署连接重连失败,已达到最大重试次数,taskId=${taskId}`, message: `部署连接重连失败,已达到最大重试次数,taskId=${taskId}` }); }); socket.on("connect_error", (error) => { debug && console.log("日志: Git 部署连接失败:", error); if (!hasConnectedOnce) finalize(normalizeConnectionError(error, websocketConfig.url, "socket")); }); socket.on("disconnect", (reason) => { debug && console.log("日志: Git 部署连接已断开:", reason); if (!isFinished) { spinner.text = colorChalk$1.yellow(`部署连接已断开,正在等待重连... 原因: ${reason}`); if (reason === "io server disconnect") socket.connect(); } }); }); } /** * 判断当前是否为 upload-code-service 自发布 * @param config - 部署配置 * @param projectKey - 项目标识 * @returns 是否为自发布 */ function isSelfDeployUploadCodeService(config, projectKey) { const packageName = readProjectPackageName(config.root || process.cwd()); return packageName === SELF_DEPLOY_SERVICE_PACKAGE_NAME && packageName === projectKey; } /** * 读取项目 package.json 的 name * @param projectRoot - 项目根目录 * @returns 包名 */ function readProjectPackageName(projectRoot) { try { const packageJsonPath = path.default.join(projectRoot, "package.json"); if (!(0, node_fs.existsSync)(packageJsonPath)) return; const packageJsonContent = (0, node_fs.readFileSync)(packageJsonPath, "utf8"); const packageJson = JSON.parse(packageJsonContent); return typeof packageJson.name === "string" ? packageJson.name : void 0; } catch { return; } } /** * 判断是否需要在自发布重启前提前结束等待 * @param event - 进度事件 * @param isSelfDeployService - 是否为自发布 * @returns 是否提前判成功 */ function shouldFinalizeSelfDeployEarly(event, isSelfDeployService) { return isSelfDeployService && event.step === "start" && event.status === "running" && SELF_DEPLOY_EARLY_SUCCESS_MESSAGES.has(event.message); } /** * 创建 Git 部署任务 ID * @param projectKey - 项目标识 * @returns 部署任务 ID */ function createGitDeployTaskId(projectKey) { return `git-deploy-${projectKey}-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`; } /** * 构建部署进度事件唯一键 * @param event - 进度事件 * @returns 事件唯一键 */ function buildGitDeployProgressEventKey(event) { return [ event.taskId || "unknown", event.timestamp, event.step, event.status, event.message, event.detail || "" ].join("::"); } /** * 构建部署结果事件唯一键 * @param event - 结果事件 * @returns 事件唯一键 */ function buildGitDeployResultEventKey(event) { return [ event.taskId || "unknown", event.timestamp, event.success ? "success" : "failed", event.message ].join("::"); } /** * 构建 Git 部署 socket 配置 * @param websocketUrl - 原始 WebSocket 连接地址 * @param websocketPath - 原始 WebSocket 握手路径 * @returns 标准化后的 socket 配置 */ function buildSocketConfig(websocketUrl, websocketPath) { return { url: websocketUrl.replace(/\/+$/, ""), path: buildSocketPath(websocketPath) }; } /** * 构建 Git 部署 socket 握手路径 * @param websocketPath - 原始 WebSocket 握手路径 * @returns 标准化后的握手路径 */ function buildSocketPath(websocketPath) { return (websocketPath.startsWith("/") ? websocketPath : `/${websocketPath}`).replace(/\/+$/, "") || "/socket.io"; } /** * 构建 Git 部署签名原文 * @param projectKey - 项目标识 * @param keyPairId - 密钥对 ID * @param timestamp - 时间戳 * @param deployEnvironment - 部署环境 * @param description - 部署描述 * @returns 签名原文 */ function buildGitDeploySignText(projectKey, keyPairId, timestamp, deployEnvironment, description) { /** 时间戳扰动字符串 */ const srd = Math.floor(timestamp % 1e4 * 333.3).toString(); /** 项目哈希 */ const projectSHA256 = calculateSHA256(projectKey); /** 部署环境哈希 */ const deployEnvironmentSHA256 = calculateSHA256(deployEnvironment); /** 描述哈希 */ const descriptionSHA256 = calculateSHA256(description || ""); /** 模式哈希 */ const deployModeSHA256 = calculateSHA256("git-pull-run"); return `${calculateSHA256(srd)}${projectSHA256}${keyPairId}${deployEnvironmentSHA256}${descriptionSHA256}${deployModeSHA256}`; } /** * 上传 * @param urlStr - 上传的的地址 * @param buffer - 上传的文件Buffer * @param zipName - 件名称(带后缀) * @param postData - 上传携带的额外参数 * @param headers - 上传携带的headers * @param rejectUnauthorized - 是否检查ssl(如https自签证书) * @param debug - 是否打印日志 */ async function uploading(urlStr, buffer, zipName, postData, headers, rejectUnauthorized, debug) { try { const formData = new formdata_node.FormData(); const file = new formdata_node.File([buffer], zipName, { type: "application/zip" }); formData.append("file", file, zipName); for (const key in postData) formData.append(key, postData[key]); debug && console.log("日志: 源代码上传url:", urlStr); debug && console.log("日志: 源代码上传body:", postData); debug && console.log("日志: 源代码上传headers:", headers); const isHttps = urlStr.startsWith("https://"); const res = await (0, node_fetch.default)(urlStr, { method: "POST", body: formData, headers, agent: rejectUnauthorized && isHttps ? new https.default.Agent({ rejectUnauthorized: false }) : void 0 }); const result = safeParseJson(await res.text()); debug && console.log("日志: 返回结果", result); if (!res.ok) { if (isServerResult(result)) return Promise.reject({ msg: getServerResultMessage(result) || buildHttpErrorMessage(urlStr, res.status, res.statusText, result), message: getServerResultMessage(result) || buildHttpErrorMessage(urlStr, res.status, res.statusText, result), code: typeof result.code === "number" ? result.code : res.status, reason: extractAuthFailureReason(result) }); return Promise.reject({ msg: buildHttpErrorMessage(urlStr, res.status, res.statusText, result), code: res.status }); } if (isServerResult(result)) if (result.code === 200) return Promise.resolve("部署成功"); else return Promise.reject({ msg: getServerResultMessage(result) || "部署失败", message: getServerResultMessage(result) || "部署失败", code: typeof result.code === "number" ? result.code : -102, reason: extractAuthFailureReason(result) }); else return Promise.reject({ msg: `上传失败,服务器返回内容不是有效的 JSON,请检查接口实现:${urlStr},-BD003` }); } catch (error) { debug && console.log("日志: 错误信息1: ", error); return Promise.reject(normalizeConnectionError(error, urlStr, "http")); } } /** * 安全解析 JSON * @param value - 原始文本 * @returns 解析结果 */ function safeParseJson(value) { try { return JSON.parse(value); } catch (error) { return value; } } /** * 获取异常码 * @param error - 原始异常 * @returns 异常码 */ function getErrorCode(error) { if (!error || typeof error !== "object") return; const errorRecord = error; const directCode = errorRecord.code; if (typeof directCode === "string" || typeof directCode === "number") return directCode; const cause = errorRecord.cause; if (cause && typeof cause === "object") { const causeCode = cause.code; if (typeof causeCode === "string" || typeof causeCode === "number") return causeCode; } } /** * 获取异常消息 * @param error - 原始异常 * @returns 异常消息 */ function getErrorMessage(error) { if (error instanceof Error) return error.message; if (error && typeof error === "object") { const objectMessage = error.message; if (typeof objectMessage === "string") return objectMessage; } return String(error || "未知异常"); } /** * 规范化连接异常提示 * @param error - 原始异常 * @param targetUrl - 目标地址 * @param protocol - 协议类型 * @returns 标准异常 */ function normalizeConnectionError(error, targetUrl, protocol) { /** 异常码 */ const errorCode = getErrorCode(error); /** 原始异常消息 */ const errorMessage = getErrorMessage(error); /** 协议名称 */ const protocolName = protocol === "socket" ? "Socket" : "HTTP"; if (errorCode === "ECONNREFUSED") return { code: errorCode, msg: `${protocolName} 连接失败,服务器可能未启动或端口未监听:${targetUrl}`, message: `${protocolName} 连接失败,服务器可能未启动或端口未监听:${targetUrl}` }; if (errorCode === "ENOTFOUND") return { code: errorCode, msg: `无法解析服务器地址,请检查配置是否正确:${targetUrl}`, message: `无法解析服务器地址,请检查配置是否正确:${targetUrl}` }; if (errorCode === "ETIMEDOUT" || errorCode === "ESOCKETTIMEDOUT") return { code: errorCode, msg: `连接服务器超时,请检查网络或服务状态:${targetUrl}`, message: `连接服务器超时,请检查网络或服务状态:${targetUrl}` }; if (errorCode === "DEPTH_ZERO_SELF_SIGNED_CERT" || errorCode === "SELF_SIGNED_CERT_IN_CHAIN" || errorCode === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") return { code: errorCode, msg: `服务器证书校验失败:${targetUrl},如使用自签名证书可设置 rejectUnauthorized: true`, message: `服务器证书校验失败:${targetUrl},如使用自签名证书可设置 rejectUnauthorized: true` }; if (/socket hang up/i.test(errorMessage)) return { code: errorCode, msg: `服务器连接被中断,请检查服务端是否正常运行:${targetUrl}`, message: `服务器连接被中断,请检查服务端是否正常运行:${targetUrl}` }; return { code: errorCode, msg: `${protocolName} 请求失败:${targetUrl}${errorMessage}`, message: `${protocolName} 请求失败:${targetUrl}${errorMessage}` }; } /** * 构建 HTTP 状态异常提示 * @param urlStr - 请求地址 * @param status - HTTP 状态码 * @param statusText - HTTP 状态说明 * @param result - 服务端返回内容 * @returns 友好的错误消息 */ function buildHttpErrorMessage(urlStr, status, statusText, result) { if (isServerResult(result)) { const serverMessage = getServerResultMessage(result); if (serverMessage) return `上传失败,服务器返回 HTTP ${status} ${statusText}${serverMessage}`; } return `上传失败,服务器返回 HTTP ${status} ${statusText},请检查接口是否存在或服务是否正常:${urlStr}`; } /** * 判断是否为服务端标准返回 * @param result - 原始返回数据 * @returns 是否为标准返回 */ function isServerResult(result) { return !!result && typeof result === "object" && !Array.isArray(result) && ("code" in result || "msg" in result || "message" in result); } /** * 获取服务端返回消息 * @param result - 服务端返回结果 * @returns 消息文本 */ function getServerResultMessage(result) { if (typeof result.message === "string" && result.message) return result.message; if (typeof result.msg === "string" && result.msg) return result.msg; return ""; } /** * 提取鉴权失败原因 * @param result - 服务端返回结果 * @returns 失败原因 */ function extractAuthFailureReason(result) { if (typeof result.reason === "string") return result.reason; if (result.data && typeof result.data === "object" && !Array.isArray(result.data)) { const nestedReason = result.data.reason; if (typeof nestedReason === "string") return nestedReason; } } /** * 是否需要打印密钥信息 * @param deployError - 部署异常 * @returns 是否打印 */ function shouldPrintKeyPairInfo(deployError) { return deployError.reason === "key_pair_not_whitelisted" || deployError.reason === "signature_verification_failed" || deployError.code === 427 && !deployError.reason; } /** * 规范化公钥显示内容 * @param publicKey - 原始公钥 * @returns 去掉 PEM 包裹后的公钥主体 */ function normalizePublicKeyForDisplay(publicKey) { return publicKey.replace(/-----BEGIN PUBLIC KEY-----/g, "").replace(/-----END PUBLIC KEY-----/g, "").replace(/\s+/g, "").trim(); } //#endregion //#region src/compression/utils.ts /** * 判断是否为函数 */ function isFunction(arg) { return typeof arg === "function"; } /** * 判断是否为正则 */ function isRegExp(arg) { return Object.prototype.toString.call(arg) === "[object RegExp]"; } /** * 读取指定文件夹下的所有文件,通过规则过滤,返回文件路径数组 * @param root - 需要读取的文件夹路径 */ function readAllFile(root, reg) { let resultArr = []; if (fs.default.existsSync(root)) if (fs.default.lstatSync(root).isDirectory()) fs.default.readdirSync(root).forEach((file) => { const t = readAllFile(path.default.join(root, "/", file), reg); resultArr = resultArr.concat(t); }); else if (reg !== void 0) isFunction(reg.test) && reg.test(root) && resultArr.push(root); else resultArr.push(root); return resultArr; } //#endregion //#region src/compression/index.ts /** * 兼容获取 `ora` 工厂函数 * @returns `ora` 实例创建函数 */ function getOraFactory() { /** 当前待解析的模块值 */ let currentOraModule = ora.default; while (currentOraModule && typeof currentOraModule === "object" && "default" in currentOraModule && currentOraModule.default !== currentOraModule) currentOraModule = currentOraModule.default; if (typeof currentOraModule !== "function") throw new Error("ora 模块加载失败"); return currentOraModule; } /** * 兼容获取 `chalk` 实例 * @returns `chalk` 实例 */ function getChalkInstance() { /** 当前待解析的模块值 */ let currentChalkModule = chalk.default; while (currentChalkModule && typeof currentChalkModule === "object" && "default" in currentChalkModule && currentChalkModule.default !== currentChalkModule) currentChalkModule = currentChalkModule.default; if (!currentChalkModule || typeof currentChalkModule !== "function") throw new Error("chalk 模块加载失败"); return currentChalkModule; } /** 兼容后的 `chalk` 实例 */ var colorChalk = getChalkInstance(); /** * 代码压缩插件 * @param {object} config - 插件配置 */ function viteCompressionPlugin(config = {}) { let viteConfig = null; return { name: "vite-mxp-compression", apply: "build", enforce: "post", configResolved(resolvedConfig) { viteConfig = resolvedConfig; }, closeBundle() { viteCompression(config, viteConfig); } }; } /** * 压缩文件 */ function viteCompression(config, viteConfig) { /** `ora` 加载后的工厂函数 */ const createOra = getOraFactory(); const newConfig = { verbose: true, threshold: 1024, filter: void 0, disable: false, algorithm: "gzip", ext: ".gz", compressionOptions: void 0, deleteOriginFile: false, ...config || {} }; const spinner = createOra({ spinner: "dots2" }); try { const { disable = false, filter = /\.(js|mjs|json|css|html)$/i, verbose = true, algorithm = "gzip", threshold = 1025, compressionOptions = {}, deleteOriginFile = false, success } = newConfig; if (!disable) { newConfig.root = newConfig.root || viteConfig?.root || process.cwd(); let outDir = newConfig.outDir || viteConfig?.build?.outDir || "dist"; outDir = path.default.isAbsolute(outDir) ? outDir : path.default.join(newConfig.root, outDir); outDir = path.default.resolve(outDir); newConfig.outDir = outDir; const ext = newConfig.ext ? newConfig.ext : algorithm === "gzip" ? ".gz" : ".br"; let files = readAllFile(outDir) || []; if (!files.length) return; console.log(""); spinner.start(colorChalk.blue("正在压缩文件...")); files = filterFiles(files, filter); const compressOptions = getCompressionOptions(algorithm, compressionOptions); const compressInfoMap = /* @__PURE__ */ new Map(); files.forEach((filePath) => { const { size: oldSize } = fs.default.statSync(filePath); if (oldSize >= threshold) { let content = fs.default.readFileSync(filePath); deleteOriginFile && deleteFile(filePath); try { content = Buffer.from(compress(content, algorithm, compressOptions)); } catch (error) { spinner.fail(`vite-mxp-compression: ${colorChalk.bold.green(`压缩失败:${filePath}`)}`); } const size = content.byteLength; const fullPath = getOutputFileName(filePath, ext); compressInfoMap.set(filePath, { size: size / 1024, oldSize: oldSize / 1024, fullPath }); fs.default.writeFileSync(fullPath, content); } }); spinner.succeed(colorChalk.bold.green(`压缩完成, 压缩算法:${algorithm}${verbose ? ", 已压缩文件:" : ""}`)); verbose && handleOutputLogger(compressInfoMap, algorithm, outDir); typeof success === "function" && success(newConfig, viteConfig); } } catch (error) { console.log(`vite-mxp-compression: ${colorChalk.bold.red(error.message)}`); } } /** * 筛选文件 * @param files - 全部文件 * @param filter - 筛选器 */ function filterFiles(files, filter) { const isRe = isRegExp(filter); const isFn = isFunction(filter); if (isRe) return files.filter((file) => filter.test(file)); else if (isFn) return files.filter((file) => filter(file)); return files; } /** * 得到最终的压缩配置 * @param algorithm - 压缩算法 * @param compressionOptions - 压缩配置 */ function getCompressionOptions(algorithm, compressionOptions = {}) { return { ...{ gzip: { level: node_zlib.default.constants.Z_BEST_COMPRESSION }, deflate: { level: node_zlib.default.constants.Z_BEST_COMPRESSION }, deflateRaw: { level: node_zlib.default.constants.Z_BEST_COMPRESSION }, brotliCompress: { params: { [node_zlib.default.constants.BROTLI_PARAM_QUALITY]: node_zlib.default.constants.BROTLI_MAX_QUALITY, [node_zlib.default.constants.BROTLI_PARAM_MODE]: node_zlib.default.constants.BROTLI_MODE_TEXT } } }[algorithm], ...compressionOptions }; } /** * 开始压缩 * @param content - 要压缩的内容 * @param algorithm - 压缩算法 * @param options - 压缩配置 */ function compress(content, algorithm, options = {}) { switch (algorithm) { case "brotliCompress": return node_zlib.default.brotliCompressSync(content, options); case "deflate": return node_zlib.default.deflateSync(content, options); case "deflateRaw": return node_zlib.default.deflateRawSync(content, options); default: return node_zlib.default.gzipSync(content, options); } } /** * 得到压缩后的文件路径完整路径 * @param filepath - 原始文件路径 * @param ext - 压缩后文件扩展名 */ function getOutputFileName(filepath, ext) { return `${filepath}${ext.startsWith(".") ? ext : `.${ext}`}`; } /** * 输出压缩结果 * @param config - vite配置 * @param compressMap - 压缩结果 * @param algorithm - 压缩算法 */ function handleOutputLogger(compressInfoMap, algorithm, outDir) { const keyLengths = Array.from(compressInfoMap.keys(), (name) => name.length); const maxKeyLength = Math.max(...keyLengths); console.log(""); compressInfoMap.forEach((value, name) => { const { size, oldSize, fullPath } = value; const rName = fullPath.replace(outDir, ""); const sizeStr = `原始大小:${oldSize.toFixed(2)}kb / ${algorithm}压缩后: ${size.toFixed(2)}kb`; const spacings = " ".repeat(2 + maxKeyLength - name.length); const basename = path.default.basename(outDir); console.log(`${colorChalk.dim(basename)}${colorChalk.blueBright(rName)}${spacings} ${colorChalk.dim(sizeStr)}`); }); console.log(" "); } //#endregion exports.autoDeply = autoDeply; exports.viteAutoDeployPlugin = viteAutoDeployPlugin; exports.viteAutoDeply = viteAutoDeply; exports.viteCompressionPlugin = viteCompressionPlugin;