vite-auto-deploy
Version:
用于配合 PHP 自动打包并上传打包后的文件到服务器指定目录的插件,此插件需要配合 php [项目地址](https://gitee.com/mxp131011/auto_deploy/)
1,118 lines (1,117 loc) • 40.4 kB
JavaScript
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;