@seayoo-web/scripts 3.1.11 → 4.0.0
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/dist/git-C1rpf4Ed.js +343 -0
- package/dist/index.js +391 -404
- package/dist/node.js +538 -606
- package/package.json +17 -33
- package/types/index.d.ts +2 -2
- package/types/src/commit.d.ts +0 -1
- package/types/src/create.d.ts +0 -1
- package/types/src/destroy.d.ts +0 -1
- package/types/src/env.d.ts +0 -1
- package/types/src/oxfmt.d.ts +7 -0
- package/types/src/oxlint.d.ts +43 -0
- package/types/src/utils.d.ts +0 -1
- package/types/src/vite.lab.d.ts +1 -2
- package/dist/git-B0Cnp5wC.js +0 -310
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { exec } from "child_process";
|
|
4
|
+
//#region src/utils.ts
|
|
5
|
+
/** 项目模板目录 */
|
|
6
|
+
var TemplateDir = "template";
|
|
7
|
+
/** monorepo config file */
|
|
8
|
+
var WorkspaceFile = "pnpm-workspace.yaml";
|
|
9
|
+
/**
|
|
10
|
+
* monorepo 项目根目录
|
|
11
|
+
*/
|
|
12
|
+
var _root = null;
|
|
13
|
+
async function getMonorepoRoot(ignoreMissing) {
|
|
14
|
+
if (_root !== null) return _root;
|
|
15
|
+
let currentDir = process.cwd();
|
|
16
|
+
while (currentDir !== path.parse(currentDir).root) {
|
|
17
|
+
if (fs.existsSync(path.join(currentDir, WorkspaceFile))) return currentDir;
|
|
18
|
+
currentDir = path.dirname(currentDir);
|
|
19
|
+
}
|
|
20
|
+
if (fs.existsSync(path.join(currentDir, "pnpm-workspace.yaml"))) {
|
|
21
|
+
_root = currentDir;
|
|
22
|
+
return currentDir;
|
|
23
|
+
}
|
|
24
|
+
if (ignoreMissing) return "";
|
|
25
|
+
throw new Error("当前项目不是 monorepo 项目!");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 读取所有模板项目信息
|
|
29
|
+
*/
|
|
30
|
+
async function getTemplates() {
|
|
31
|
+
return await getRepos(TemplateDir);
|
|
32
|
+
}
|
|
33
|
+
/** 读取指定项目的 repo 信息 */
|
|
34
|
+
async function getRepos(project) {
|
|
35
|
+
const root = await getMonorepoRoot();
|
|
36
|
+
const projectDir = path.join(root, project);
|
|
37
|
+
const repos = (await fs.readdir(projectDir)).filter((item) => fs.statSync(path.join(projectDir, item)).isDirectory());
|
|
38
|
+
const descArr = await Promise.all(repos.map((repo) => getRepoDesc(project, repo)));
|
|
39
|
+
return repos.map((dir, index) => ({
|
|
40
|
+
id: dir,
|
|
41
|
+
desc: descArr[index]
|
|
42
|
+
})).filter((item) => item.desc !== null);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 读取模板的描述信息
|
|
46
|
+
*/
|
|
47
|
+
async function getRepoDesc(project, dir) {
|
|
48
|
+
const root = await getMonorepoRoot();
|
|
49
|
+
const packageFile = path.join(root, project, dir, "package.json");
|
|
50
|
+
if (!await fs.exists(packageFile)) return null;
|
|
51
|
+
return ((await fs.readJson(packageFile, "utf8")).description || "") + "";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 从 pnpm-workspace.yaml 读取支持的项目名称
|
|
55
|
+
*/
|
|
56
|
+
async function getProjects(returnAll) {
|
|
57
|
+
const root = await getMonorepoRoot();
|
|
58
|
+
const rootPkgPath = path.join(root, WorkspaceFile);
|
|
59
|
+
if (await fs.pathExists(rootPkgPath)) {
|
|
60
|
+
const content = await fs.readFile(rootPkgPath, "utf8");
|
|
61
|
+
const dirs = /packages:\n(?:\s+-\s".+"\/\*)+/.test(content) ? (content.match(/- "(?:.+)\/\*"/g) || []).map((t) => t.slice(3, -3)) : (content.match(/- (?:.+)\/\*/g) || []).map((t) => t.slice(2, -2));
|
|
62
|
+
const names = await Promise.all(dirs.map(getProjectName));
|
|
63
|
+
const projects = dirs.map((id, index) => ({
|
|
64
|
+
id,
|
|
65
|
+
name: names[index]
|
|
66
|
+
})).filter((project) => !project.name.includes("disabled"));
|
|
67
|
+
return returnAll === true ? projects : projects.filter((project) => project.id !== "internal" && project.id !== "template");
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 读取项目的中文名称
|
|
73
|
+
*/
|
|
74
|
+
async function getProjectName(project) {
|
|
75
|
+
const nameOfFile = path.join(project, ".name");
|
|
76
|
+
if (await fs.exists(nameOfFile)) return (await fs.readFile(nameOfFile, "utf8")).trim().replace(/\n[\d\D]+/g, "");
|
|
77
|
+
return project;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 复制模板到目标目录
|
|
81
|
+
*/
|
|
82
|
+
async function copyTemplate(templateName, targetDir) {
|
|
83
|
+
const root = await getMonorepoRoot();
|
|
84
|
+
const templateSourceDir = path.join(root, TemplateDir, templateName);
|
|
85
|
+
await fs.copy(templateSourceDir, targetDir, { filter: (src) => {
|
|
86
|
+
return !src.includes("node_modules") && !src.includes(".git") && !src.endsWith(".local");
|
|
87
|
+
} });
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 复制全局文件
|
|
91
|
+
*/
|
|
92
|
+
async function copyGlobalFiles(targetDir) {
|
|
93
|
+
const root = await getMonorepoRoot();
|
|
94
|
+
const globalFiles = await fs.readdir(path.join(root, TemplateDir));
|
|
95
|
+
await Promise.all(globalFiles.filter((item) => fs.statSync(path.join(root, TemplateDir, item)).isFile()).map((item) => fs.copy(path.join(root, TemplateDir, item), path.join(targetDir, item))));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 更新 workspace.yaml
|
|
99
|
+
*/
|
|
100
|
+
async function addProjectToWorkspace(project) {
|
|
101
|
+
const root = await getMonorepoRoot();
|
|
102
|
+
const rootWorkspacePath = path.join(root, WorkspaceFile);
|
|
103
|
+
if (await fs.pathExists(rootWorkspacePath)) {
|
|
104
|
+
let content = await fs.readFile(rootWorkspacePath, "utf8");
|
|
105
|
+
if (/packages:\n(?:\s+-\s".+"\/*)+/.test(content)) {
|
|
106
|
+
if (!content.includes(`- "${project}/*"`)) content = content.replace(/(packages:\n(?:\s{2,}-\s.+\n?)+)/, `$1\n - "${project}/*"\n`).replace(/\n{2,}/g, "\n");
|
|
107
|
+
} else if (!content.includes(`- ${project}/*`)) content = content.replace(/(packages:\n(?:\s{2,}-\s.+\n?)+)/, `$1\n - ${project}/*\n`).replace(/\n{2,}/g, "\n");
|
|
108
|
+
await fs.writeFile(rootWorkspacePath, content);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 更新 workspace.yaml
|
|
113
|
+
*/
|
|
114
|
+
async function removeProjectFromWorkspace(project) {
|
|
115
|
+
const root = await getMonorepoRoot();
|
|
116
|
+
const rootWorkspacePath = path.join(root, WorkspaceFile);
|
|
117
|
+
if (await fs.pathExists(rootWorkspacePath)) {
|
|
118
|
+
const content = (await fs.readFile(rootWorkspacePath, "utf8")).replace(new RegExp(`\\n.*\\- "${project}\\/\\*"`), "");
|
|
119
|
+
await fs.writeFile(rootWorkspacePath, content);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function getNowTime() {
|
|
123
|
+
const now = /* @__PURE__ */ new Date();
|
|
124
|
+
return `${now.getFullYear()}/${fillZ(now.getMonth() + 1)}/${fillZ(now.getDate())} ${fillZ(now.getHours())}:${fillZ(now.getMinutes())}`;
|
|
125
|
+
}
|
|
126
|
+
function fillZ(a) {
|
|
127
|
+
return ("0" + a).slice(-2);
|
|
128
|
+
}
|
|
129
|
+
function deepMerge(target, ...sourceList) {
|
|
130
|
+
for (const source of sourceList) {
|
|
131
|
+
if (!source || typeof source !== "object") continue;
|
|
132
|
+
for (const key in source) if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
133
|
+
const sourceValue = source[key];
|
|
134
|
+
const targetValue = target[key];
|
|
135
|
+
if (isPlainObject(targetValue) && isPlainObject(sourceValue)) target[key] = deepMerge({ ...targetValue }, sourceValue);
|
|
136
|
+
else if (isPlainObject(sourceValue)) target[key] = deepMerge({}, sourceValue);
|
|
137
|
+
else if (Array.isArray(sourceValue)) target[key] = [...sourceValue];
|
|
138
|
+
else target[key] = sourceValue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return target;
|
|
142
|
+
}
|
|
143
|
+
function isPlainObject(value) {
|
|
144
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]";
|
|
145
|
+
}
|
|
146
|
+
var formatReg = /\{\{\s*([\w.]+)\s*\}\}/g;
|
|
147
|
+
/**
|
|
148
|
+
* 模板格式化
|
|
149
|
+
* @param string 要格式化的模板字符串
|
|
150
|
+
* @param source 要填充的数据源
|
|
151
|
+
*/
|
|
152
|
+
function format(string, source) {
|
|
153
|
+
return string.replace(formatReg, function(_, keys) {
|
|
154
|
+
let val = source;
|
|
155
|
+
const keyPath = keys.split(".");
|
|
156
|
+
for (let i = 0; i < keyPath.length; i++) {
|
|
157
|
+
const p = keyPath[i];
|
|
158
|
+
val = typeof val === "object" && !!val ? p in val ? val[p] : void 0 : void 0;
|
|
159
|
+
}
|
|
160
|
+
return val === void 0 || val === null ? "" : val + "";
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/git.ts
|
|
165
|
+
/** 主干分支名 */
|
|
166
|
+
var MainBranchName = "main";
|
|
167
|
+
/** tag前缀 */
|
|
168
|
+
var DeployTagPrefix = "deploy_";
|
|
169
|
+
var BackupTagPrefix = "backup_";
|
|
170
|
+
function convertToDeployTagName(deployTo) {
|
|
171
|
+
return `${DeployTagPrefix}${deployTo.toLowerCase().replace(/(?:^https?:\/\/|\/*$)/gi, "").replace(/\//g, "_")}`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 执行命令并返回响应
|
|
175
|
+
*/
|
|
176
|
+
async function execCmd(cmd, ignoreWarning = false) {
|
|
177
|
+
const root = await getMonorepoRoot(true);
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
exec(cmd, {
|
|
180
|
+
cwd: root || "./",
|
|
181
|
+
env: { ...process.env }
|
|
182
|
+
}, (error, stdout, stderr) => {
|
|
183
|
+
if (error || stderr && !ignoreWarning) {
|
|
184
|
+
console.error(cmd, error, stderr);
|
|
185
|
+
reject(`执行命令时出错: ${error?.message || stderr}`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
resolve(stdout.trim());
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 读取提交信息
|
|
194
|
+
*/
|
|
195
|
+
async function getCommitInfo(command, mode, page, deployTo) {
|
|
196
|
+
if (command !== "build") return {
|
|
197
|
+
hash: await getCommitHash(),
|
|
198
|
+
logs: []
|
|
199
|
+
};
|
|
200
|
+
if (mode === "preview" || mode === "prev") {
|
|
201
|
+
if (!deployTo.includes("preview")) {
|
|
202
|
+
console.error(`预览部署的目标路径必须包含 preview 字样!`.red);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
hash: await getCommitHash(),
|
|
207
|
+
logs: []
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (await hasUncommittedChanges()) {
|
|
211
|
+
console.error(`部署前需要提交所有代码!`.red);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
if (await getCurrentBranchName() !== MainBranchName) {
|
|
215
|
+
console.error(`部署需要在 ${`${MainBranchName}分支`.bgRed} 操作!`.red);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
const unCommitChanges = await countUnSubmitChanges(MainBranchName);
|
|
219
|
+
if (unCommitChanges > 0) {
|
|
220
|
+
console.error("部署前需要先将代码同步到服务器!".red, `发现 ${unCommitChanges} 个 commit 差异`.red);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
await execCmd("git pull", true);
|
|
224
|
+
const [currentCommit, lastDeployTag] = await Promise.all([getCommitHash(), getLastDeployTag(deployTo)]);
|
|
225
|
+
return {
|
|
226
|
+
hash: currentCommit,
|
|
227
|
+
logs: await getCommitLogs(currentCommit, lastDeployTag, page || "./")
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* 读取上次部署的信息
|
|
232
|
+
*/
|
|
233
|
+
async function getLastDeployTag(deployTo) {
|
|
234
|
+
await fetchTags();
|
|
235
|
+
const deployTags = await getDeployTags();
|
|
236
|
+
const tag = convertToDeployTagName(deployTo);
|
|
237
|
+
return deployTags.includes(tag) ? tag : "";
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* 更新部署目标的 commit 标记,在页面成功部署后调用
|
|
241
|
+
*/
|
|
242
|
+
async function createPageDeployTag(page, deployTo, finderUser) {
|
|
243
|
+
const tagName = convertToDeployTagName(deployTo);
|
|
244
|
+
const user = (await execCmd(`git config user.email`).catch(() => "") || finderUser).replace(/@.+/g, "");
|
|
245
|
+
await execCmd([
|
|
246
|
+
`git tag -f ${tagName} -m "${getNowTime()} deployed by ${user}`,
|
|
247
|
+
page ? `from ${page}` : "",
|
|
248
|
+
"\""
|
|
249
|
+
].filter((c) => !!c).join(" "));
|
|
250
|
+
try {
|
|
251
|
+
await execCmd(`git push origin -f ${tagName}`, true);
|
|
252
|
+
} catch {
|
|
253
|
+
await execCmd(`git push origin -f --tags`, true);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* 检查最近一次 commit 信息
|
|
258
|
+
*/
|
|
259
|
+
async function getCommitHash() {
|
|
260
|
+
return (await execCmd("git rev-parse HEAD")).slice(0, 8);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* 检查当前分支名称
|
|
264
|
+
*/
|
|
265
|
+
async function getCurrentBranchName() {
|
|
266
|
+
return await execCmd("git rev-parse --abbrev-ref HEAD");
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* 拉取所有 tags
|
|
270
|
+
*/
|
|
271
|
+
async function fetchTags() {
|
|
272
|
+
await execCmd("git fetch --tags --force", true);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* 读取所有部署 tag(以 deploy 开头)
|
|
276
|
+
*/
|
|
277
|
+
async function getDeployTags() {
|
|
278
|
+
return (await execCmd("git tag")).split("\n").filter((tag) => tag.startsWith(DeployTagPrefix));
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* 查询 commit 提交日志,最多返回 60 条日志
|
|
282
|
+
*/
|
|
283
|
+
async function getCommitLogs(branchOrCommit, sinceCommit, codePath) {
|
|
284
|
+
const result = await execCmd([
|
|
285
|
+
"git log",
|
|
286
|
+
sinceCommit ? `${sinceCommit}..${branchOrCommit || "HEAD"}` : branchOrCommit || "",
|
|
287
|
+
`--oneline --pretty=format:"[%cd] %s" --date=short -n 60`,
|
|
288
|
+
codePath ? `-- ${codePath}` : ""
|
|
289
|
+
].filter((f) => !!f && !f.includes("Merge branch")).join(" "));
|
|
290
|
+
return result ? result.split("\n") : [];
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* 检查当前分支是否有尚未提交的代码
|
|
294
|
+
*/
|
|
295
|
+
async function hasUncommittedChanges() {
|
|
296
|
+
return await execCmd("git status --porcelain") !== "";
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* 检查尚未同步服务器的提交数量
|
|
300
|
+
*/
|
|
301
|
+
async function countUnSubmitChanges(branch) {
|
|
302
|
+
const result = await execCmd(`git rev-list --count ${branch} "^origin/${branch}"`);
|
|
303
|
+
return parseInt(result, 10);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* 在删除工程或目录前检查 git 状态
|
|
307
|
+
*/
|
|
308
|
+
async function checkGitStatusBeforeDestroy() {
|
|
309
|
+
if (await hasUncommittedChanges()) {
|
|
310
|
+
console.error(`在销毁前需要提交所有代码!`.red);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
if (await getCurrentBranchName() !== MainBranchName) {
|
|
314
|
+
console.error(`销毁操作需要在 ${`${MainBranchName}分支`.bgRed} 进行!`.red);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
const unCommitChanges = await countUnSubmitChanges(MainBranchName);
|
|
318
|
+
if (unCommitChanges > 0) {
|
|
319
|
+
console.error("销毁操作需要先将代码同步到服务器!".red, `发现 ${unCommitChanges} 个 commit 差异`.red);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* 创建备份 tag
|
|
325
|
+
*/
|
|
326
|
+
async function createBackupTag(action, info) {
|
|
327
|
+
const tagName = `${BackupTagPrefix}${action}_${info.replace(/\//g, "-")}`;
|
|
328
|
+
const user = (await execCmd(`git config user.email`).catch(() => "") || "unknown").replace(/@.+/g, "");
|
|
329
|
+
await execCmd(`git tag -f ${tagName} -m "${getNowTime()} destroyed by ${user}"`);
|
|
330
|
+
try {
|
|
331
|
+
await execCmd(`git push origin -f ${tagName}`, true);
|
|
332
|
+
} catch {
|
|
333
|
+
await execCmd(`git push origin --tags`, true);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* 将所有删除提交
|
|
338
|
+
*/
|
|
339
|
+
async function submitAllDeletionChanges(message) {
|
|
340
|
+
await execCmd(`git add --all :/ && git commit -m "${message}" && git push origin ${await getCurrentBranchName()}`, true);
|
|
341
|
+
}
|
|
342
|
+
//#endregion
|
|
343
|
+
export { removeProjectFromWorkspace as _, submitAllDeletionChanges as a, copyTemplate as c, getMonorepoRoot as d, getNowTime as f, isPlainObject as g, getTemplates as h, getCommitInfo as i, deepMerge as l, getRepos as m, createBackupTag as n, addProjectToWorkspace as o, getProjects as p, createPageDeployTag as r, copyGlobalFiles as s, checkGitStatusBeforeDestroy as t, format as u };
|