@taole/deploy-helper 1.0.3 → 1.0.5-beta.1
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.
- package/index.mjs +13 -43
- package/lib/git.mjs +102 -0
- package/lib/lightDeploy.mjs +102 -0
- package/lib/project.mjs +1 -1
- package/lib/util.mjs +0 -40
- package/package.json +1 -3
- package/lib/offlinePkg.mjs +0 -333
- package/lib/upload.js +0 -49
package/index.mjs
CHANGED
|
@@ -4,14 +4,14 @@ import { Client } from 'node-scp'
|
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import { join, basename, dirname } from "path";
|
|
6
6
|
import { simpleGit } from 'simple-git';
|
|
7
|
-
import { homedir } from 'os'
|
|
8
7
|
import { runPipeline, checkYunxiaoToken, triggerPipeline } from './lib/pipelineApi.mjs';
|
|
9
|
-
import { setDebug, log
|
|
8
|
+
import { setDebug, log } from './lib/util.mjs';
|
|
10
9
|
import path from 'path';
|
|
11
10
|
import { fileURLToPath } from 'url';
|
|
12
|
-
import { checkOfflinePkg, syncApi as syncOfflinePkgApi } from './lib/offlinePkg.mjs';
|
|
13
11
|
import { cmdWhoami, cmdLogout, cmdLogin } from './lib/login.mjs';
|
|
14
12
|
import { cmdProjectCreate, cmdProjectPublish, cmdProjectPull } from './lib/project.mjs';
|
|
13
|
+
import { cmdCommit } from './lib/git.mjs';
|
|
14
|
+
import { lightDeploy } from './lib/lightDeploy.mjs';
|
|
15
15
|
|
|
16
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
17
|
const __dirname = path.dirname(__filename);
|
|
@@ -108,10 +108,10 @@ async function getScpClient() {
|
|
|
108
108
|
return scpClient;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
function getPackageJson() {
|
|
112
112
|
const packageJsonPath = join(__dirname, "package.json");
|
|
113
113
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
114
|
-
return packageJson
|
|
114
|
+
return packageJson;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
const commandMap = {};
|
|
@@ -130,6 +130,7 @@ function doRegisterCommands() {
|
|
|
130
130
|
registerCommand("create", cmdProjectCreate, "创建项目");
|
|
131
131
|
registerCommand("publish", cmdProjectPublish, "更新项目到H5平台");
|
|
132
132
|
registerCommand("pull", cmdProjectPull, "拉取已有项目到本地");
|
|
133
|
+
registerCommand("commit", cmdCommit, "按任务号提交(参数为说明,如 feat: xxx)");
|
|
133
134
|
registerCommand("whoami", cmdWhoami, "查看当前登录用户信息");
|
|
134
135
|
registerCommand("logout", cmdLogout, "退出登录");
|
|
135
136
|
registerCommand("login", cmdLogin, "登录");
|
|
@@ -138,7 +139,8 @@ function doRegisterCommands() {
|
|
|
138
139
|
doRegisterCommands();
|
|
139
140
|
|
|
140
141
|
async function main() {
|
|
141
|
-
const
|
|
142
|
+
const packageJson = getPackageJson();
|
|
143
|
+
const version = packageJson.version;
|
|
142
144
|
|
|
143
145
|
// process.argv.includes("--debug") ||
|
|
144
146
|
// 默认开启debug debug目前只是打印日志的时候额外打印时间
|
|
@@ -155,7 +157,7 @@ async function main() {
|
|
|
155
157
|
const registeredCommand = commandMap[command];
|
|
156
158
|
if(registeredCommand){
|
|
157
159
|
// pass
|
|
158
|
-
} else if (!["init", "prod", "test", "scp", "scpevt", "pipeline"
|
|
160
|
+
} else if (!["init", "prod", "test", "scp", "scpevt", "pipeline"].includes(command)) {
|
|
159
161
|
command = "help";
|
|
160
162
|
}
|
|
161
163
|
|
|
@@ -182,7 +184,6 @@ async function main() {
|
|
|
182
184
|
console.log(`command: scpevt {file} 复制文件{file}到events测试服务器{file}`);
|
|
183
185
|
console.log(`command: scpevt {file} {dest} 复制文件{file}到events测试服务器{dest}`);
|
|
184
186
|
console.log(`command: pipeline {pipelineName|pipelineId} [branch] 触发流水线{pipelineName|pipelineId}, 指定分支(可省略)`);
|
|
185
|
-
console.log(`command: offlinepkgrm {name} {platform} [mode] 删除离线包{name}, 指定平台{platform}, 指定模式(可省略,默认test)`);
|
|
186
187
|
Object.keys(commandMap).forEach(command => {
|
|
187
188
|
const cmdObj = commandMap[command];
|
|
188
189
|
console.log(`command: ${command} ${cmdObj.help}`);
|
|
@@ -256,29 +257,6 @@ async function main() {
|
|
|
256
257
|
process.exit(1);
|
|
257
258
|
}
|
|
258
259
|
|
|
259
|
-
} else if (command === "offlinepkgrm") {
|
|
260
|
-
const name = process.argv[3];
|
|
261
|
-
if (!name) {
|
|
262
|
-
log(`name参数不能为空`);
|
|
263
|
-
process.exit(1);
|
|
264
|
-
}
|
|
265
|
-
const platform = process.argv[4];
|
|
266
|
-
if (!platform) {
|
|
267
|
-
log(`platform参数不能为空`);
|
|
268
|
-
process.exit(1);
|
|
269
|
-
}
|
|
270
|
-
const mode = process.argv[5] || "test";
|
|
271
|
-
if (!["prod", "test"].includes(mode)) {
|
|
272
|
-
log(`mode参数只能是prod或test`);
|
|
273
|
-
process.exit(1);
|
|
274
|
-
}
|
|
275
|
-
const userDeployHelperConfig = getUserDeployHelperConfig();
|
|
276
|
-
if (!userDeployHelperConfig || !userDeployHelperConfig.offlineApi || !userDeployHelperConfig.offlineApi.get || !userDeployHelperConfig.offlineApi.set) {
|
|
277
|
-
log(`配置文件未配置离线包api接口, 请先配置`);
|
|
278
|
-
process.exit(1);
|
|
279
|
-
}
|
|
280
|
-
await syncOfflinePkgApi(userDeployHelperConfig, mode, { name, remove: true, platform }, null);
|
|
281
|
-
process.exit(0);
|
|
282
260
|
}
|
|
283
261
|
try {
|
|
284
262
|
const workDir = process.cwd(); // 当前项目根目录
|
|
@@ -309,7 +287,10 @@ async function main() {
|
|
|
309
287
|
process.exit(1);
|
|
310
288
|
}
|
|
311
289
|
|
|
312
|
-
|
|
290
|
+
const needHandleLight = config.type === "ddfe:cdn";
|
|
291
|
+
if(needHandleLight){
|
|
292
|
+
await lightDeploy(config, mode);
|
|
293
|
+
}
|
|
313
294
|
const needHandleAssets = config.assets && config.assets.dest;
|
|
314
295
|
|
|
315
296
|
// 需要处理entry
|
|
@@ -322,17 +303,6 @@ async function main() {
|
|
|
322
303
|
|
|
323
304
|
// 检查流水线配置
|
|
324
305
|
checkYunxiaoToken(config, mode);
|
|
325
|
-
const offlinePkgResult = checkOfflinePkg({
|
|
326
|
-
workDir: workDir,
|
|
327
|
-
mode,
|
|
328
|
-
});
|
|
329
|
-
if (!offlinePkgResult.canBuild && offlinePkgResult.errorMsg) {
|
|
330
|
-
log(`${offlinePkgResult.errorMsg}`);
|
|
331
|
-
process.exit(1);
|
|
332
|
-
}
|
|
333
|
-
if (offlinePkgResult.canBuild) {
|
|
334
|
-
await offlinePkgResult.hookPostBuild();
|
|
335
|
-
}
|
|
336
306
|
|
|
337
307
|
// 检查项目
|
|
338
308
|
// 1. 检查项目是否存在
|
package/lib/git.mjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Git 辅助操作(基于 simple-git)
|
|
2
|
+
import { simpleGit } from "simple-git";
|
|
3
|
+
import { log } from "./util.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} branch
|
|
7
|
+
* @param {string} latestCommitMsg
|
|
8
|
+
*/
|
|
9
|
+
function extractTaskId(branch, latestCommitMsg) {
|
|
10
|
+
const fromBranch = branch.match(/#([A-Z]+-\d+)$/);
|
|
11
|
+
if (fromBranch) return fromBranch[1];
|
|
12
|
+
const fromCommit = latestCommitMsg.match(/-#([A-Z]+-\d+)/);
|
|
13
|
+
if (fromCommit) return fromCommit[1];
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getUserMessage() {
|
|
18
|
+
const argvMsg = process.argv.slice(3).join(" ").trim();
|
|
19
|
+
if (argvMsg) return argvMsg;
|
|
20
|
+
const envMsg = (
|
|
21
|
+
process.env.npm_config_message ||
|
|
22
|
+
process.env.npm_config_msg ||
|
|
23
|
+
""
|
|
24
|
+
).trim();
|
|
25
|
+
return envMsg;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} input
|
|
30
|
+
* @returns {{ full: string, typePart: null } | { full: null, typePart: string } | null}
|
|
31
|
+
*/
|
|
32
|
+
/** 参数是否已自带 `-#PROJ-123` 形式任务前缀(无需再从分支/历史解析) */
|
|
33
|
+
function hasExplicitTaskPrefix(msg) {
|
|
34
|
+
return /^-#[A-Z]+-\d+/.test(msg.trim());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeMessage(input) {
|
|
38
|
+
const msg = input.trim();
|
|
39
|
+
if (!msg) return null;
|
|
40
|
+
if (msg.startsWith("-#")) {
|
|
41
|
+
return { full: msg, typePart: null };
|
|
42
|
+
}
|
|
43
|
+
const match = msg.match(/^([a-z]+):\s*(.+)$/i);
|
|
44
|
+
if (!match) return null;
|
|
45
|
+
return { full: null, typePart: `${match[1]}: ${match[2]}` };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function getLatestSubject(git) {
|
|
49
|
+
try {
|
|
50
|
+
const logResult = await git.log({ maxCount: 1 });
|
|
51
|
+
if (!logResult.latest) return "";
|
|
52
|
+
return logResult.latest.message.split("\n")[0].trim();
|
|
53
|
+
} catch {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 按任务号规范提交:默认从分支名(末尾 #任务号)或最新提交中提取任务号;若参数已含 `-#DDKH-4169` 形式前缀则直接使用该说明提交。
|
|
60
|
+
* 用法: dh commit "feat: 说明" 或 dh commit "-#DDKH-4169 feat: 完整信息"
|
|
61
|
+
*/
|
|
62
|
+
export async function cmdCommit() {
|
|
63
|
+
const git = simpleGit(process.cwd());
|
|
64
|
+
const rawMsg = getUserMessage();
|
|
65
|
+
|
|
66
|
+
let commitMsg;
|
|
67
|
+
|
|
68
|
+
if (rawMsg.trim() && hasExplicitTaskPrefix(rawMsg)) {
|
|
69
|
+
const parsed = normalizeMessage(rawMsg);
|
|
70
|
+
if (!parsed) {
|
|
71
|
+
log('请传入提交说明,例如: dh commit "feat:我的提交信息"');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
commitMsg = parsed.full;
|
|
75
|
+
} else {
|
|
76
|
+
const branch = (await git.status()).current;
|
|
77
|
+
const latestCommitMsg = await getLatestSubject(git);
|
|
78
|
+
const taskId = extractTaskId(branch, latestCommitMsg);
|
|
79
|
+
if (!taskId) {
|
|
80
|
+
log("未能从分支名或最新提交中提取任务号(如 DDKH-4169)");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const parsed = normalizeMessage(rawMsg);
|
|
85
|
+
if (!parsed) {
|
|
86
|
+
log('请传入提交说明,例如: dh commit "feat:我的提交信息"');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
commitMsg = parsed.full || `-#${taskId} ${parsed.typePart}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const st = await git.status();
|
|
94
|
+
if (st.isClean()) {
|
|
95
|
+
log("没有可提交的改动");
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
await git.add(".");
|
|
99
|
+
await git.commit(commitMsg);
|
|
100
|
+
log(`commit 完成: ${commitMsg}`);
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { log } from "./util.mjs";
|
|
4
|
+
import { getCache as getLoginCache } from "./login.mjs";
|
|
5
|
+
import { genArchive } from "./project.mjs";
|
|
6
|
+
|
|
7
|
+
const isDev = false;
|
|
8
|
+
const apiHost = isDev ? "http://localhost:9000" : "https://fapi.tuwan.com";
|
|
9
|
+
|
|
10
|
+
function getErrorMessage(resJson, fallback) {
|
|
11
|
+
const message = resJson?.message;
|
|
12
|
+
if (Array.isArray(message)) {
|
|
13
|
+
return message.join(", ");
|
|
14
|
+
}
|
|
15
|
+
if (message) {
|
|
16
|
+
return String(message);
|
|
17
|
+
}
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function apiCdnUpload({ distZipPath, prefix, mode, includeHtml, entryHtmlMap }, userCache) {
|
|
22
|
+
const formData = new FormData();
|
|
23
|
+
formData.append("prefix", prefix);
|
|
24
|
+
formData.append("mode", mode);
|
|
25
|
+
formData.append("includeHtml", includeHtml ? "true" : "false");
|
|
26
|
+
if (entryHtmlMap) {
|
|
27
|
+
const entryHtmlMapStr =
|
|
28
|
+
typeof entryHtmlMap === "string" ? entryHtmlMap : JSON.stringify(entryHtmlMap);
|
|
29
|
+
formData.append("entryHtmlMap", entryHtmlMapStr);
|
|
30
|
+
}
|
|
31
|
+
formData.append(
|
|
32
|
+
"file",
|
|
33
|
+
new Blob([fs.readFileSync(distZipPath)], { type: "application/zip" }),
|
|
34
|
+
"dist.zip"
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const res = await fetch(`${apiHost}/cdn/upload`, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
body: formData,
|
|
40
|
+
headers: {
|
|
41
|
+
Cookie: `Tuwan_Passport=${userCache.Tuwan_Passport}`,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const resJson = await res.json();
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
throw new Error(getErrorMessage(resJson, "CDN 上传失败"));
|
|
47
|
+
}
|
|
48
|
+
return resJson;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function lightDeploy(config, mode) {
|
|
52
|
+
log(`light模式, 开始部署`, { mode, prefix: config.prefix, includeHtml: config.includeHtml });
|
|
53
|
+
|
|
54
|
+
if (!mode || (mode !== "test" && mode !== "prod")) {
|
|
55
|
+
throw new Error("部署环境无效,仅支持 test 或 prod");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const prefix = config.prefix;
|
|
59
|
+
if (!prefix) {
|
|
60
|
+
throw new Error("配置缺少 prefix");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const userCache = await getLoginCache();
|
|
64
|
+
if (!userCache?.userInfo?.uid) {
|
|
65
|
+
throw new Error("请先登录");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const workDir = process.cwd();
|
|
69
|
+
const distDir = config.distDir || "dist";
|
|
70
|
+
const distPath = path.join(workDir, distDir);
|
|
71
|
+
if (!fs.existsSync(distPath)) {
|
|
72
|
+
throw new Error(`${distPath} 不存在,请先执行构建`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const distZipPath = path.join(workDir, "dist.zip");
|
|
76
|
+
if (fs.existsSync(distZipPath)) {
|
|
77
|
+
fs.unlinkSync(distZipPath);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
log(`开始打包 ${distPath}`);
|
|
81
|
+
await genArchive(distZipPath, distPath);
|
|
82
|
+
log(`打包完成: ${distZipPath}`);
|
|
83
|
+
|
|
84
|
+
const includeHtml = config.includeHtml === true;
|
|
85
|
+
log(`开始上传至 CDN,mode=${mode}, prefix=${prefix}, includeHtml=${includeHtml}`);
|
|
86
|
+
const result = await apiCdnUpload(
|
|
87
|
+
{
|
|
88
|
+
distZipPath,
|
|
89
|
+
prefix,
|
|
90
|
+
mode,
|
|
91
|
+
includeHtml,
|
|
92
|
+
entryHtmlMap: config.entryHtmlMap,
|
|
93
|
+
},
|
|
94
|
+
userCache
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (fs.existsSync(distZipPath)) {
|
|
98
|
+
fs.unlinkSync(distZipPath);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
log(`CDN 上传成功`, result);
|
|
102
|
+
}
|
package/lib/project.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { getCache as getLoginCache } from "./login.mjs";
|
|
|
9
9
|
const isDev = false;
|
|
10
10
|
const apiHost = isDev ? "http://localhost:9000" : "https://fapi.tuwan.com";
|
|
11
11
|
|
|
12
|
-
function genArchive(outputPath, dir) {
|
|
12
|
+
export function genArchive(outputPath, dir) {
|
|
13
13
|
return new Promise((resolve, reject) => {
|
|
14
14
|
const output = fs.createWriteStream(outputPath);
|
|
15
15
|
const archive = archiver("zip", {
|
package/lib/util.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import { join } from "node:path"
|
|
3
|
-
import { homedir } from "node:os"
|
|
4
3
|
import { exec } from 'child_process';
|
|
5
4
|
|
|
6
5
|
let _isDebug = false;
|
|
@@ -21,45 +20,6 @@ export function log(...args) {
|
|
|
21
20
|
}
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
export function getOssToken() {
|
|
25
|
-
let token = "";
|
|
26
|
-
token = process.env.TW_DH_OSS_TOKEN || ""
|
|
27
|
-
if (token) {
|
|
28
|
-
log(`将使用来自环境变量TW_DH_OSS_TOKEN的token`);
|
|
29
|
-
return token;
|
|
30
|
-
}
|
|
31
|
-
const userDeployHelperDir = join(homedir(), "deploy-helper.config.json");
|
|
32
|
-
if (fs.existsSync(userDeployHelperDir)) {
|
|
33
|
-
try {
|
|
34
|
-
const userDeployHelperConfig = JSON.parse(fs.readFileSync(userDeployHelperDir, "utf-8"));
|
|
35
|
-
token = userDeployHelperConfig.ossToken || "";
|
|
36
|
-
} catch (error) {
|
|
37
|
-
console.error(`读取${userDeployHelperDir}配置失败: ${error}`);
|
|
38
|
-
}
|
|
39
|
-
if (token) {
|
|
40
|
-
log(`将使用来自${userDeployHelperDir}的ossToken`);
|
|
41
|
-
return token;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return token;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* 读取dh用户配置文件deploy-helper.config.json
|
|
49
|
-
* @returns {Object|null} 用户配置
|
|
50
|
-
*/
|
|
51
|
-
export function getUserDeployHelperConfig() {
|
|
52
|
-
const userDeployHelperDir = join(homedir(), "deploy-helper.config.json");
|
|
53
|
-
if (fs.existsSync(userDeployHelperDir)) {
|
|
54
|
-
try {
|
|
55
|
-
const userDeployHelperConfig = JSON.parse(fs.readFileSync(userDeployHelperDir, "utf-8"));
|
|
56
|
-
return userDeployHelperConfig;
|
|
57
|
-
} catch (error) {
|
|
58
|
-
console.error(`读取${userDeployHelperDir}配置失败: ${error}`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
23
|
/**
|
|
64
24
|
* json配置
|
|
65
25
|
* @param {string} workDir 项目工作目录
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taole/deploy-helper",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5-beta.1",
|
|
4
4
|
"description": "脚本部署工具,用于将项目部署到测试环境或生产环境",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -25,10 +25,8 @@
|
|
|
25
25
|
"author": "",
|
|
26
26
|
"license": "ISC",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"ali-oss": "^6.23.0",
|
|
29
28
|
"archiver": "^7.0.1",
|
|
30
29
|
"form-data": "^4.0.5",
|
|
31
|
-
"md5-file": "^5.0.0",
|
|
32
30
|
"node-scp": "^0.0.25",
|
|
33
31
|
"simple-git": "^3.28.0"
|
|
34
32
|
}
|
package/lib/offlinePkg.mjs
DELETED
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import archiver from 'archiver';
|
|
3
|
-
import { join } from "node:path"
|
|
4
|
-
import { log, getOssToken, getUserDeployHelperConfig } from './util.mjs';
|
|
5
|
-
import { uploadFile } from './upload.js';
|
|
6
|
-
import md5File from 'md5-file';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function genArchive(outputPath, dir) {
|
|
11
|
-
return new Promise((resolve, reject) => {
|
|
12
|
-
const output = fs.createWriteStream(outputPath);
|
|
13
|
-
const archive = archiver('zip', {
|
|
14
|
-
zlib: { level: 1 } // Sets the compression level.
|
|
15
|
-
});
|
|
16
|
-
let settled = false;
|
|
17
|
-
const onError = (err) => {
|
|
18
|
-
if (settled) return;
|
|
19
|
-
settled = true;
|
|
20
|
-
try {
|
|
21
|
-
archive.abort();
|
|
22
|
-
} catch {
|
|
23
|
-
// ignore
|
|
24
|
-
}
|
|
25
|
-
output.destroy(err);
|
|
26
|
-
reject(err);
|
|
27
|
-
};
|
|
28
|
-
archive.on('error', onError);
|
|
29
|
-
output.on('error', onError);
|
|
30
|
-
// 必须等目标文件流关闭后再继续:仅 archive 'finish' 时缓冲区可能尚未完全刷盘,
|
|
31
|
-
// 后续 md5/上传会读到缺 END 中央目录的截断 zip(如 ADM-ZIP: No END header found)。
|
|
32
|
-
output.on('close', () => {
|
|
33
|
-
if (settled) return;
|
|
34
|
-
settled = true;
|
|
35
|
-
resolve();
|
|
36
|
-
});
|
|
37
|
-
archive.pipe(output);
|
|
38
|
-
archive.directory(dir, false);
|
|
39
|
-
archive.finalize();
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* json配置
|
|
45
|
-
* @param {string} workDir 项目工作目录
|
|
46
|
-
* @returns {Object|null} 配置
|
|
47
|
-
*/
|
|
48
|
-
export function getJsonConfig(workDir, name) {
|
|
49
|
-
const filePath = join(workDir, name);
|
|
50
|
-
if (fs.existsSync(filePath)) {
|
|
51
|
-
try {
|
|
52
|
-
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
53
|
-
} catch (_error) {
|
|
54
|
-
// pass
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 同步离线包列表到api
|
|
62
|
-
* @param {{offlineApi: {get: string, set: string}}} userDeployHelperConfig 用户配置
|
|
63
|
-
* @param {"prod"|"test"} mode 部署模式 prod: 线上, test: 测试
|
|
64
|
-
* @param {{name: string, remove: boolean}} offlineConfig 离线包配置
|
|
65
|
-
* @param {any} packageDescJson 离线包描述json
|
|
66
|
-
*/
|
|
67
|
-
export async function syncApi(userDeployHelperConfig, mode, offlineConfig, packageDescJson) {
|
|
68
|
-
// 根据部署模式,决定更新调用哪个接口
|
|
69
|
-
const doAction = offlineConfig.remove ? "remove" : "add";
|
|
70
|
-
const apiDomain = mode === "prod" ? "https://yapi.tuwan.com" : "https://yapi-test.tuwan.com";
|
|
71
|
-
const getPath = userDeployHelperConfig.offlineApi.get;
|
|
72
|
-
const setPath = userDeployHelperConfig.offlineApi.set;
|
|
73
|
-
const getUrl = `${apiDomain}${getPath}`;
|
|
74
|
-
const setUrl = `${apiDomain}${setPath}`;
|
|
75
|
-
const envPrefix = `${mode === "prod" ? "线上环境" : "测试环境"}[platform=${offlineConfig.platform}]`;
|
|
76
|
-
const nowRes = await fetch(getUrl, {
|
|
77
|
-
headers: {
|
|
78
|
-
platform: String(offlineConfig.platform),
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
const nowData = await nowRes.json();
|
|
82
|
-
let needSetData = true;
|
|
83
|
-
let finalJsonData = null;
|
|
84
|
-
if (!nowData || nowData.error !== 0) {
|
|
85
|
-
throw new Error(`获取离线包列表失败, 获取离线包当前配置失败: ${nowData.error_msg || "未知错误"}`);
|
|
86
|
-
}
|
|
87
|
-
const jsonData = JSON.parse(nowData.data.json || "{}");
|
|
88
|
-
finalJsonData = jsonData;
|
|
89
|
-
jsonData.packages = jsonData.packages || [];
|
|
90
|
-
if (doAction === "add") {
|
|
91
|
-
const idx = jsonData.packages.findIndex(item => item.name === offlineConfig.name);
|
|
92
|
-
if (idx !== -1) {
|
|
93
|
-
log(`离线包${offlineConfig.name}已存在, 更新离线包`);
|
|
94
|
-
jsonData.packages[idx] = packageDescJson;
|
|
95
|
-
} else {
|
|
96
|
-
log(`离线包${offlineConfig.name}不存在, 添加离线包`);
|
|
97
|
-
jsonData.packages.push(packageDescJson);
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
const idx = jsonData.packages.findIndex(item => item.name === offlineConfig.name);
|
|
101
|
-
if (idx === -1) {
|
|
102
|
-
log(`离线包${offlineConfig.name}不存在, 无需删除`);
|
|
103
|
-
needSetData = false;
|
|
104
|
-
}
|
|
105
|
-
jsonData.packages = jsonData.packages.filter(item => item.name !== offlineConfig.name);
|
|
106
|
-
}
|
|
107
|
-
jsonData.packages = jsonData.packages.filter(item => item.name);
|
|
108
|
-
if (needSetData) {
|
|
109
|
-
// console.log('jsonData', jsonData);
|
|
110
|
-
var formdata = new FormData();
|
|
111
|
-
formdata.append("json", JSON.stringify(jsonData));
|
|
112
|
-
const setRes = await fetch(setUrl, {
|
|
113
|
-
method: "POST",
|
|
114
|
-
body: formdata,
|
|
115
|
-
headers: {
|
|
116
|
-
platform: String(offlineConfig.platform),
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
const setData = await setRes.json();
|
|
120
|
-
if (!setData || setData.error !== 0) {
|
|
121
|
-
throw new Error(`更新离线包列表失败, 更新离线包当前配置失败: ${setData.error_msg || "未知错误"}`);
|
|
122
|
-
}
|
|
123
|
-
log(`${envPrefix}离线包列表更新完成: ${JSON.stringify(setData)}`);
|
|
124
|
-
// 再次获取列表,确保更新成功
|
|
125
|
-
const nowRes2 = await fetch(getUrl, {
|
|
126
|
-
headers: {
|
|
127
|
-
platform: String(offlineConfig.platform),
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
const nowData2 = await nowRes2.json();
|
|
131
|
-
if (!nowData2 || nowData2.error !== 0) {
|
|
132
|
-
throw new Error(`再次获取离线包列表失败, 获取离线包当前配置失败: ${nowData2.error_msg || "未知错误"}`);
|
|
133
|
-
}
|
|
134
|
-
finalJsonData = JSON.parse(nowData2.data.json || "{}");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const packages = finalJsonData.packages || [];
|
|
138
|
-
console.log(`${envPrefix}当前离线包列表:`, packages.map(item => ({ ...item, items: '省略' + item.items.length + '个条目' })));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* @deprecated 不再需要这个函数, 如果插件使用的是@taole/vite-plugin-dynamic-base, 则不需要这个函数
|
|
143
|
-
* hack
|
|
144
|
-
* 由于vite-plugin-dynamic-base插件和legacy插件同时使用, 生存的入口文件中代码有点问题,这里直接修改有问题的代码
|
|
145
|
-
*/
|
|
146
|
-
function fixEntryHtml(config, offlineConfig) {
|
|
147
|
-
if (!offlineConfig || !config) return;
|
|
148
|
-
const entryHtmlPath = join(config.workDir, offlineConfig.distDir, "index.html");
|
|
149
|
-
if (!fs.existsSync(entryHtmlPath)) {
|
|
150
|
-
throw new Error(`构建产物${entryHtmlPath}不存在`);
|
|
151
|
-
}
|
|
152
|
-
const markLine = "} else if (item.tagName == 'script') {";
|
|
153
|
-
const replaceLine = `} else if (item.tagName == 'script')/*injected by deploy-helper*/{
|
|
154
|
-
if(item.attrs.id=='vite-legacy-polyfill')childNode.onload=function(){System.import(window.__dynamic_base__ + document.getElementById('vite-legacy-entry').getAttribute('data-src'))};
|
|
155
|
-
`;
|
|
156
|
-
let entryHtml = fs.readFileSync(entryHtmlPath, "utf-8");
|
|
157
|
-
entryHtml = entryHtml.replace(markLine, replaceLine);
|
|
158
|
-
fs.writeFileSync(entryHtmlPath, entryHtml);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* @typedef {Object} CheckOfflinePkgResult
|
|
163
|
-
* @property {string} workDir 工作目录
|
|
164
|
-
* @property {boolean} canBuild 是否可以构建离线包
|
|
165
|
-
* @property {string} errorMsg 错误信息
|
|
166
|
-
* @property {function} hookPostBuild 构建后钩子
|
|
167
|
-
* @property {function} hookPostDeployTest 部署到测试环境后钩子
|
|
168
|
-
* @property {function} hookPostDeployProd 部署到生产环境后钩子
|
|
169
|
-
*/
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* 在项目构建好之后,将项目打包成离线包,并上传到指定服务器
|
|
173
|
-
* @param {Object} config 配置
|
|
174
|
-
* @param {string} config.workDir 工作目录
|
|
175
|
-
* @param {"prod"|"test"} config.mode 部署模式
|
|
176
|
-
* @returns {CheckOfflinePkgResult} 结果
|
|
177
|
-
*/
|
|
178
|
-
export function checkOfflinePkg(config) {
|
|
179
|
-
let canBuildOfflinePkg = true;
|
|
180
|
-
let errorMsg = "";
|
|
181
|
-
const offlineConfig = getJsonConfig(config.workDir, "offline.config.json");
|
|
182
|
-
if (offlineConfig) {
|
|
183
|
-
offlineConfig.distDir = offlineConfig.distDir || "dist";
|
|
184
|
-
offlineConfig.platform = offlineConfig.platform || 3; // 3,4是点点
|
|
185
|
-
}
|
|
186
|
-
const packageJson = getJsonConfig(config.workDir, "package.json");
|
|
187
|
-
const userDeployHelperConfig = getUserDeployHelperConfig();
|
|
188
|
-
const distPath = join(config.workDir, offlineConfig && offlineConfig.distDir ? offlineConfig.distDir : "dist");
|
|
189
|
-
const allDeps = {
|
|
190
|
-
...packageJson.devDependencies,
|
|
191
|
-
...packageJson.dependencies,
|
|
192
|
-
};
|
|
193
|
-
const useOldPDB = !!allDeps['vite-plugin-dynamic-base'];
|
|
194
|
-
|
|
195
|
-
// 如果offlineConfig.remove为true,则不构建离线包
|
|
196
|
-
const willRemovePkg = offlineConfig && offlineConfig.name && offlineConfig.remove === true && offlineConfig.skip !== true;
|
|
197
|
-
const willSkip = !offlineConfig || !offlineConfig.name || offlineConfig.skip === true || (offlineConfig.onlyTest === true && config.mode === "prod");
|
|
198
|
-
if (willRemovePkg) {
|
|
199
|
-
return {
|
|
200
|
-
canBuild: true,
|
|
201
|
-
errorMsg: "",
|
|
202
|
-
hookPostBuild: async () => {
|
|
203
|
-
if (useOldPDB) {
|
|
204
|
-
fixEntryHtml(config, offlineConfig);
|
|
205
|
-
}
|
|
206
|
-
log(`开始删除离线包${offlineConfig.name}`);
|
|
207
|
-
await syncApi(userDeployHelperConfig, config.mode, offlineConfig, null);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (!fs.existsSync(distPath)) {
|
|
212
|
-
errorMsg = `构建产物${distPath}不存在, 请先构建项目`;
|
|
213
|
-
canBuildOfflinePkg = false;
|
|
214
|
-
} else if (offlineConfig && !offlineConfig.name) {
|
|
215
|
-
errorMsg = "离线包配置文件中未配置name, 请先配置";
|
|
216
|
-
canBuildOfflinePkg = false;
|
|
217
|
-
} else if (!userDeployHelperConfig || !userDeployHelperConfig.offlineApi || !userDeployHelperConfig.offlineApi.get || !userDeployHelperConfig.offlineApi.set) {
|
|
218
|
-
errorMsg = "未配置离线包接口, 请移步看配置文档: https://alidocs.dingtalk.com/i/nodes/R1zknDm0WRkbz13oHn0LDz3ZVBQEx5rG?doc_type=wiki_doc";
|
|
219
|
-
canBuildOfflinePkg = false;
|
|
220
|
-
} else if (!packageJson) {
|
|
221
|
-
errorMsg = "啊没有package.json?";
|
|
222
|
-
canBuildOfflinePkg = false;
|
|
223
|
-
} else {
|
|
224
|
-
if (!allDeps['vite-plugin-dynamic-base'] && !allDeps['@taole/vite-plugin-dynamic-base']) {
|
|
225
|
-
errorMsg = "检查到项目中未安装依赖@taole/vite-plugin-dynamic-base, 请先安装依赖并调整构建代码以支持离线化,相关文档:https://alidocs.dingtalk.com/i/nodes/ydxXB52LJqexwD71F9K5XNMrJqjMp697?doc_type=wiki_doc";
|
|
226
|
-
canBuildOfflinePkg = false;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (willSkip) {
|
|
230
|
-
if (!offlineConfig) {
|
|
231
|
-
return {
|
|
232
|
-
canBuild: false,
|
|
233
|
-
errorMsg: ""
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (offlineConfig.onlyTest === true && config.mode === "prod") {
|
|
237
|
-
console.log(`离线包${offlineConfig.name}只用于测试环境, 跳过构建线上环境的离线包构建`);
|
|
238
|
-
}
|
|
239
|
-
return {
|
|
240
|
-
canBuild: true,
|
|
241
|
-
errorMsg: "",
|
|
242
|
-
hookPostBuild: async () => {
|
|
243
|
-
if (useOldPDB) {
|
|
244
|
-
fixEntryHtml(config, offlineConfig);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
let ossToken = "";
|
|
250
|
-
if (canBuildOfflinePkg) {
|
|
251
|
-
ossToken = getOssToken();
|
|
252
|
-
if (!ossToken) {
|
|
253
|
-
errorMsg = "获取ossToken失败";
|
|
254
|
-
canBuildOfflinePkg = false;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (!canBuildOfflinePkg) {
|
|
258
|
-
return {
|
|
259
|
-
canBuild: false,
|
|
260
|
-
errorMsg: errorMsg
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
const hookPostBuild = async () => {
|
|
264
|
-
if (!offlineConfig || !config) return;
|
|
265
|
-
// 在构建完成后,执行一些操作
|
|
266
|
-
if (useOldPDB) {
|
|
267
|
-
fixEntryHtml(config, offlineConfig);
|
|
268
|
-
}
|
|
269
|
-
// 开始进行打包流程
|
|
270
|
-
const offlinePkgDir = join(config.workDir, "./offlinepkgDist");
|
|
271
|
-
if (fs.existsSync(offlinePkgDir)) {
|
|
272
|
-
fs.rmSync(offlinePkgDir, { recursive: true });
|
|
273
|
-
} else {
|
|
274
|
-
fs.mkdirSync(offlinePkgDir, { recursive: true });
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const packageDir = join(offlinePkgDir, "./package");
|
|
278
|
-
fs.mkdirSync(packageDir, { recursive: true });
|
|
279
|
-
// cp dist到offlinepkgDist目录下
|
|
280
|
-
fs.cpSync(join(config.workDir, offlineConfig.distDir), join(packageDir, offlineConfig.distDir), { recursive: true });
|
|
281
|
-
|
|
282
|
-
const targetArchive = join(offlinePkgDir, `./${packageJson.version}.zip`);
|
|
283
|
-
await genArchive(targetArchive, packageDir);
|
|
284
|
-
log(`离线包zip包打包完成: ${targetArchive}`);
|
|
285
|
-
|
|
286
|
-
const packageDescJson = { // 包描述json,之后会通过接口更新
|
|
287
|
-
name: offlineConfig.name,
|
|
288
|
-
version: packageJson.version,
|
|
289
|
-
md5: "",
|
|
290
|
-
packageUrl: "",
|
|
291
|
-
items: [],
|
|
292
|
-
updateTime: String(Math.floor(Date.now() / 1000)),
|
|
293
|
-
cacheTime: offlineConfig.cacheTime || "7200",
|
|
294
|
-
isLazy: offlineConfig.isLazy || 0,
|
|
295
|
-
openType: offlineConfig.openType || "file",
|
|
296
|
-
};
|
|
297
|
-
Object.keys(offlineConfig.entry || {}).forEach(key => {
|
|
298
|
-
const val = offlineConfig.entry[key];
|
|
299
|
-
let vals = [];
|
|
300
|
-
if (typeof val === 'string') {
|
|
301
|
-
vals.push(val);
|
|
302
|
-
} else if (Array.isArray(val)) {
|
|
303
|
-
vals = val;
|
|
304
|
-
}
|
|
305
|
-
vals.forEach(v => {
|
|
306
|
-
packageDescJson.items.push({
|
|
307
|
-
"path": key,
|
|
308
|
-
"mimeType": "text/html",
|
|
309
|
-
"remoteUrl": v,
|
|
310
|
-
"isEntrance": 1
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
packageDescJson.md5 = await md5File(targetArchive);
|
|
315
|
-
const uploadResult = await uploadFile(ossToken, targetArchive, packageDescJson.md5);
|
|
316
|
-
packageDescJson.packageUrl = uploadResult.url;
|
|
317
|
-
fs.writeFileSync(join(offlinePkgDir, "./offline.package.json"), JSON.stringify(packageDescJson, null, 2));
|
|
318
|
-
log(`离线包描述json写入完成: ${join(offlinePkgDir, "./offline.package.json")}`);
|
|
319
|
-
|
|
320
|
-
if (config.mode !== "prod") {
|
|
321
|
-
packageDescJson.items.forEach(item => {
|
|
322
|
-
item.remoteUrl = item.remoteUrl.replace("https://y.tuwan.com", "https://y-test.tuwan.com");
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
await syncApi(userDeployHelperConfig, config.mode, offlineConfig, packageDescJson);
|
|
326
|
-
}
|
|
327
|
-
return {
|
|
328
|
-
canBuild: canBuildOfflinePkg,
|
|
329
|
-
errorMsg: errorMsg,
|
|
330
|
-
hookPostBuild: hookPostBuild,
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
package/lib/upload.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import OSS from "ali-oss";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
function UUIDNOCA() {
|
|
5
|
-
return "xxxxxxxxxxxxxxxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
|
6
|
-
var r = (Math.random() * 16) | 0;
|
|
7
|
-
var v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
8
|
-
return v.toString(16);
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
let keyConfig = null;
|
|
13
|
-
async function getKeyConfig(ossToken) {
|
|
14
|
-
if (keyConfig) {
|
|
15
|
-
return keyConfig;
|
|
16
|
-
}
|
|
17
|
-
const reqUrl = `https://u.tuwan.com/Oss/sts?token=${ossToken}`;
|
|
18
|
-
const res = await fetch(reqUrl);
|
|
19
|
-
const resText = await res.text();
|
|
20
|
-
let fmtJson = resText.substring(1, resText.length - 1).replace("'", '"');
|
|
21
|
-
const realData = JSON.parse(fmtJson).data;
|
|
22
|
-
keyConfig = realData;
|
|
23
|
-
return keyConfig;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
let ossClient = null;
|
|
27
|
-
async function getUploadConfig(ossToken) {
|
|
28
|
-
if (ossClient) {
|
|
29
|
-
return ossClient;
|
|
30
|
-
}
|
|
31
|
-
const keyConfig = await getKeyConfig(ossToken);
|
|
32
|
-
ossClient = new OSS({
|
|
33
|
-
endpoint: "oss-cn-qingdao.aliyuncs.com",
|
|
34
|
-
accessKeyId: keyConfig.AccessKeyId,
|
|
35
|
-
accessKeySecret: keyConfig.AccessKeySecret,
|
|
36
|
-
bucket: "tuwanpicshare",
|
|
37
|
-
stsToken: keyConfig.SecurityToken,
|
|
38
|
-
secure: true,
|
|
39
|
-
});
|
|
40
|
-
return ossClient;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function uploadFile(ossToken, filePath, name) {
|
|
44
|
-
const str = filePath;
|
|
45
|
-
const ext = str.substr(str.lastIndexOf(".") + 1);
|
|
46
|
-
const fileName = `offlinepkg/${name || UUIDNOCA()}.${ext}`;
|
|
47
|
-
const ossClient = await getUploadConfig(ossToken);
|
|
48
|
-
return ossClient.put(fileName, filePath);
|
|
49
|
-
}
|