@quiteer/scripts 0.0.1 → 0.0.3
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/index.d.mts +1 -0
- package/dist/index.mjs +583 -163
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -1,100 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import cac from "cac";
|
|
2
|
-
import { bgRed, blue, gray, green, lightGreen, red, yellow } from "kolorist";
|
|
3
|
-
import {
|
|
4
|
-
import { access, writeFile } from "node:fs/promises";
|
|
3
|
+
import { bgGreen, bgRed, blue, gray, green, lightBlue, lightCyan, lightGreen, red, white, yellow } from "kolorist";
|
|
4
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import process from "node:process";
|
|
7
|
-
import {
|
|
7
|
+
import { execa } from "execa";
|
|
8
|
+
import { rimraf } from "rimraf";
|
|
8
9
|
import { loadConfig } from "c12";
|
|
9
10
|
import { readFileSync } from "node:fs";
|
|
11
|
+
import enquirer from "enquirer";
|
|
10
12
|
import { versionBump } from "bumpp";
|
|
11
13
|
|
|
12
14
|
//#region package.json
|
|
13
|
-
var version = "0.0.
|
|
14
|
-
|
|
15
|
-
//#endregion
|
|
16
|
-
//#region src/commands/cleanup.ts
|
|
17
|
-
async function cleanup(paths) {
|
|
18
|
-
await rimraf(paths, { glob: true });
|
|
19
|
-
}
|
|
15
|
+
var version = "0.0.2";
|
|
20
16
|
|
|
21
17
|
//#endregion
|
|
22
|
-
//#region src/
|
|
23
|
-
const defaultOptions = {
|
|
24
|
-
cwd: process.cwd(),
|
|
25
|
-
cleanupDirs: [
|
|
26
|
-
"**/dist",
|
|
27
|
-
"**/node_modules",
|
|
28
|
-
"!node_modules/**"
|
|
29
|
-
],
|
|
30
|
-
lang: "zh-cn",
|
|
31
|
-
ncuCommandArgs: ["--deep", "-u"],
|
|
32
|
-
gitCommitVerifyIgnores: [
|
|
33
|
-
/^((Merge pull request)|(Merge (.*?) into (.*)|(Merge branch (.*)))(?:\r?\n)*$)/m,
|
|
34
|
-
/^(Merge tag (.*))(?:\r?\n)*$/m,
|
|
35
|
-
/^(R|r)evert (.*)/,
|
|
36
|
-
/^(amend|fixup|squash)!/,
|
|
37
|
-
/^(Merged (.*?)(in|into) (.*)|Merged PR (.*): (.*))/,
|
|
38
|
-
/^Merge remote-tracking branch(\s*)(.*)/,
|
|
39
|
-
/^Automatic merge(.*)/,
|
|
40
|
-
/^Auto-merged (.*?) into (.*)/
|
|
41
|
-
]
|
|
42
|
-
};
|
|
43
|
-
async function loadCliOptions(overrides, cwd = process.cwd()) {
|
|
44
|
-
const { config } = await loadConfig({
|
|
45
|
-
name: "quiteer",
|
|
46
|
-
defaults: defaultOptions,
|
|
47
|
-
overrides,
|
|
48
|
-
cwd,
|
|
49
|
-
packageJson: true
|
|
50
|
-
});
|
|
51
|
-
return config;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
//#endregion
|
|
55
|
-
//#region src/commands/generate-cfg.ts
|
|
18
|
+
//#region src/locales/changelog.ts
|
|
56
19
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
20
|
+
* 获取 changelog 类型到分组标题的映射
|
|
21
|
+
* @param lang 输出语言
|
|
22
|
+
* @returns 类型分组标题映射
|
|
59
23
|
*/
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
24
|
+
function getChangelogHeadingMap(lang) {
|
|
25
|
+
if (lang === "en-us") return {
|
|
26
|
+
"feat": "Features",
|
|
27
|
+
"feat-wip": "Features",
|
|
28
|
+
"fix": "Fixes",
|
|
29
|
+
"docs": "Docs",
|
|
30
|
+
"typo": "Typo",
|
|
31
|
+
"style": "Style",
|
|
32
|
+
"refactor": "Refactor",
|
|
33
|
+
"perf": "Performance",
|
|
34
|
+
"optimize": "Optimization",
|
|
35
|
+
"test": "Tests",
|
|
36
|
+
"build": "Build",
|
|
37
|
+
"ci": "CI",
|
|
38
|
+
"chore": "Chore",
|
|
39
|
+
"revert": "Revert"
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
"feat": "新功能",
|
|
43
|
+
"feat-wip": "新功能",
|
|
44
|
+
"fix": "修复",
|
|
45
|
+
"docs": "文档",
|
|
46
|
+
"typo": "勘误",
|
|
47
|
+
"style": "代码风格",
|
|
48
|
+
"refactor": "重构",
|
|
49
|
+
"perf": "性能优化",
|
|
50
|
+
"optimize": "质量优化",
|
|
51
|
+
"test": "测试",
|
|
52
|
+
"build": "构建",
|
|
53
|
+
"ci": "CI",
|
|
54
|
+
"chore": "其他",
|
|
55
|
+
"revert": "回滚"
|
|
80
56
|
};
|
|
81
|
-
await writeFile(configPath, [
|
|
82
|
-
"// 自动生成的 quiteer 配置文件",
|
|
83
|
-
"",
|
|
84
|
-
"export default {",
|
|
85
|
-
` cleanupDirs: ${indentJson(defaultOptions.cleanupDirs)},`,
|
|
86
|
-
` lang: ${JSON.stringify(defaultOptions.lang)},`,
|
|
87
|
-
` ncuCommandArgs: ${indentJson(defaultOptions.ncuCommandArgs)},`,
|
|
88
|
-
" gitCommitVerifyIgnores: [",
|
|
89
|
-
...regexList.map((r, idx, arr) => ` ${r}${idx < arr.length - 1 ? "," : ""}`),
|
|
90
|
-
" ]",
|
|
91
|
-
"}",
|
|
92
|
-
""
|
|
93
|
-
].join("\n"), "utf8");
|
|
94
57
|
}
|
|
95
58
|
|
|
96
59
|
//#endregion
|
|
97
|
-
//#region src/locales/
|
|
60
|
+
//#region src/locales/commit.ts
|
|
98
61
|
const locales = {
|
|
99
62
|
"zh-cn": {
|
|
100
63
|
gitCommitMessages: {
|
|
@@ -173,83 +136,469 @@ const locales = {
|
|
|
173
136
|
//#endregion
|
|
174
137
|
//#region src/shared/index.ts
|
|
175
138
|
async function execCommand(cmd, args, options) {
|
|
176
|
-
const { execa } = await import("execa");
|
|
177
139
|
return ((await execa(cmd, args, options))?.stdout)?.trim() || "";
|
|
178
140
|
}
|
|
179
141
|
|
|
180
142
|
//#endregion
|
|
181
|
-
//#region src/commands/
|
|
143
|
+
//#region src/commands/changelog.ts
|
|
182
144
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
145
|
+
* 获取分组标题映射(使用 locales/changelog 聚合配置)
|
|
146
|
+
* @param lang 输出语言
|
|
185
147
|
*/
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (!
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
148
|
+
function getHeadingMap(lang) {
|
|
149
|
+
return getChangelogHeadingMap(lang);
|
|
150
|
+
}
|
|
151
|
+
function parseCommit(line) {
|
|
152
|
+
const [hash, short, date, author, email, subject] = line.split("|");
|
|
153
|
+
const m = subject.match(/(?<type>[a-z]+)(?:\((?<scope>.+)\))?!?: (?<description>.+)/i);
|
|
154
|
+
if (!m?.groups) return null;
|
|
155
|
+
return {
|
|
156
|
+
type: m.groups.type.toLowerCase(),
|
|
157
|
+
scope: m.groups.scope,
|
|
158
|
+
description: m.groups.description.trim(),
|
|
159
|
+
hash,
|
|
160
|
+
short,
|
|
161
|
+
author,
|
|
162
|
+
email,
|
|
163
|
+
date,
|
|
164
|
+
added: 0,
|
|
165
|
+
deleted: 0
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function groupByType(items, lang) {
|
|
169
|
+
const map = /* @__PURE__ */ new Map();
|
|
170
|
+
const headingMap = getHeadingMap(lang);
|
|
171
|
+
for (const it of items) {
|
|
172
|
+
const heading = headingMap[it.type] || (lang === "en-us" ? "Other" : "其他");
|
|
173
|
+
const arr = map.get(heading) || [];
|
|
174
|
+
arr.push(it);
|
|
175
|
+
map.set(heading, arr);
|
|
176
|
+
}
|
|
177
|
+
return map;
|
|
178
|
+
}
|
|
179
|
+
/** 获取类型图标映射 */
|
|
180
|
+
function getTypeIcon(type) {
|
|
181
|
+
return {
|
|
182
|
+
"feat": "✨ ",
|
|
183
|
+
"feat-wip": "🧪 ",
|
|
184
|
+
"fix": "🐛 ",
|
|
185
|
+
"docs": "📝 ",
|
|
186
|
+
"style": "🎨 ",
|
|
187
|
+
"refactor": "♻️ ",
|
|
188
|
+
"perf": "⚡ ",
|
|
189
|
+
"optimize": "🧹 ",
|
|
190
|
+
"test": "✅ ",
|
|
191
|
+
"build": "🏗️ ",
|
|
192
|
+
"ci": "⚙️ ",
|
|
193
|
+
"chore": "🔧 ",
|
|
194
|
+
"deps": "📦 ",
|
|
195
|
+
"revert": "⏪ ",
|
|
196
|
+
"typo": "✏️ "
|
|
197
|
+
}[type] || "";
|
|
198
|
+
}
|
|
199
|
+
async function getTags() {
|
|
200
|
+
return (await execCommand("git", [
|
|
201
|
+
"tag",
|
|
202
|
+
"--list",
|
|
203
|
+
"--sort=-version:refname"
|
|
204
|
+
])).split("\n").filter(Boolean);
|
|
205
|
+
}
|
|
206
|
+
async function getRootCommit() {
|
|
207
|
+
return await execCommand("git", [
|
|
208
|
+
"rev-list",
|
|
209
|
+
"--max-parents=0",
|
|
210
|
+
"HEAD"
|
|
211
|
+
]);
|
|
212
|
+
}
|
|
213
|
+
async function getDateForRef(ref) {
|
|
214
|
+
return await execCommand("git", [
|
|
215
|
+
"log",
|
|
216
|
+
"-1",
|
|
217
|
+
"--date=short",
|
|
218
|
+
"--format=%ad",
|
|
219
|
+
ref
|
|
220
|
+
]) || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
221
|
+
}
|
|
222
|
+
async function getCommitsInRange(range) {
|
|
223
|
+
return (await execCommand("git", [
|
|
224
|
+
"log",
|
|
225
|
+
range,
|
|
226
|
+
"--no-merges",
|
|
227
|
+
"--pretty=format:%H|%h|%ad|%an|%ae|%s",
|
|
228
|
+
"--date=format:%Y-%m-%d %H:%M %z"
|
|
229
|
+
])).split("\n").filter(Boolean).map(parseCommit).filter((x) => !!x);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* 读取 package homepage 以生成提交链接
|
|
233
|
+
* @returns 仓库主页 URL
|
|
234
|
+
*/
|
|
235
|
+
async function readHomepage() {
|
|
236
|
+
try {
|
|
237
|
+
const content = await readFile(path.join(process.cwd(), "scripts", "package.json"), "utf8");
|
|
238
|
+
const json = JSON.parse(content);
|
|
239
|
+
return typeof json.homepage === "string" ? json.homepage.replace(/\/$/, "") : void 0;
|
|
240
|
+
} catch {
|
|
206
241
|
return;
|
|
207
242
|
}
|
|
208
|
-
|
|
243
|
+
}
|
|
244
|
+
async function prependFile(filePath, content) {
|
|
245
|
+
try {
|
|
246
|
+
await access(filePath);
|
|
247
|
+
const prev = await readFile(filePath, "utf8");
|
|
248
|
+
await writeFile(filePath, [
|
|
249
|
+
content.trim(),
|
|
250
|
+
"",
|
|
251
|
+
prev.trim()
|
|
252
|
+
].join("\n"), "utf8");
|
|
253
|
+
} catch {
|
|
254
|
+
await writeFile(filePath, `${content.trim()}\n`, "utf8");
|
|
255
|
+
}
|
|
209
256
|
}
|
|
210
257
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* @param lang
|
|
258
|
+
* 为每个提交补充变更文件与统计信息
|
|
259
|
+
* @param items 提交项
|
|
214
260
|
*/
|
|
215
|
-
async function
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
261
|
+
async function enrichCommit(items) {
|
|
262
|
+
const results = [];
|
|
263
|
+
for (const it of items) {
|
|
264
|
+
const out = await execCommand("git", [
|
|
265
|
+
"show",
|
|
266
|
+
it.hash,
|
|
267
|
+
"--numstat",
|
|
268
|
+
"--pretty=format:"
|
|
269
|
+
]);
|
|
270
|
+
let added = 0;
|
|
271
|
+
let deleted = 0;
|
|
272
|
+
for (const line of out.split("\n").filter(Boolean)) {
|
|
273
|
+
const parts = line.split(" ");
|
|
274
|
+
if (parts.length === 3) {
|
|
275
|
+
const ai = Number.parseInt(parts[0], 10);
|
|
276
|
+
const di = Number.parseInt(parts[1], 10);
|
|
277
|
+
if (!Number.isNaN(ai)) added += ai;
|
|
278
|
+
if (!Number.isNaN(di)) deleted += di;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const statusOut = await execCommand("git", [
|
|
282
|
+
"show",
|
|
283
|
+
it.hash,
|
|
284
|
+
"--name-status",
|
|
285
|
+
"--pretty=format:"
|
|
286
|
+
]);
|
|
287
|
+
const filesAdded = [];
|
|
288
|
+
const filesModified = [];
|
|
289
|
+
const filesDeleted = [];
|
|
290
|
+
for (const sLine of statusOut.split("\n").filter(Boolean)) {
|
|
291
|
+
const [status, file] = sLine.split(" ");
|
|
292
|
+
if (status === "A") filesAdded.push(file);
|
|
293
|
+
else if (status === "M") filesModified.push(file);
|
|
294
|
+
else if (status === "D") filesDeleted.push(file);
|
|
295
|
+
else if (status?.startsWith("R") && file) filesModified.push(file);
|
|
296
|
+
}
|
|
297
|
+
results.push({
|
|
298
|
+
...it,
|
|
299
|
+
added,
|
|
300
|
+
deleted,
|
|
301
|
+
filesAdded,
|
|
302
|
+
filesModified,
|
|
303
|
+
filesDeleted
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return results;
|
|
307
|
+
}
|
|
308
|
+
function formatSection(title, date, items, repoUrl, lang) {
|
|
309
|
+
const groups = groupByType(items, lang);
|
|
310
|
+
const lines = [];
|
|
311
|
+
lines.push("## 变更日志");
|
|
312
|
+
lines.push("");
|
|
313
|
+
for (const [heading, arr] of groups) {
|
|
314
|
+
if (!arr.length) continue;
|
|
315
|
+
lines.push(`### ${heading}`);
|
|
316
|
+
const dayMap = groupByDay(arr);
|
|
317
|
+
for (const [day, list] of dayMap) {
|
|
318
|
+
const dayAdded = list.reduce((s, c) => s + (c.filesAdded?.length || 0), 0);
|
|
319
|
+
const dayMod = list.reduce((s, c) => s + (c.filesModified?.length || 0), 0);
|
|
320
|
+
const dayDel = list.reduce((s, c) => s + (c.filesDeleted?.length || 0), 0);
|
|
321
|
+
lines.push(`#### ${day} \`✏️ ${dayMod}+\` \`➕ ${dayAdded}+\` \`🗑️ ${dayDel}+\``);
|
|
322
|
+
for (const it of list) {
|
|
323
|
+
const scopeFmt = it.scope ? `\`${it.scope}\`` : "";
|
|
324
|
+
const link = repoUrl ? ` ([\`${it.short}\`](${repoUrl}/commit/${it.hash}))` : "";
|
|
325
|
+
const email = ` <${it.email}>`;
|
|
326
|
+
const typeIcon = getTypeIcon(it.type);
|
|
327
|
+
const typeLabel = it.type === "chore" ? "**chore**" : `**${it.type}**`;
|
|
328
|
+
const time = getTime(it.date);
|
|
329
|
+
lines.push(`- ${typeIcon} ${typeLabel} ${scopeFmt ? `${scopeFmt}: ` : ""}${it.description}`);
|
|
330
|
+
lines.push(` > **🕒 ${time}** · \`➕${it.added}\` / \`➖${it.deleted}\``);
|
|
331
|
+
lines.push(` > \`👤 ${it.author}\` ${email}${link}`);
|
|
332
|
+
for (const f of it.filesAdded ?? []) lines.push(` - ➕ \`${f}\``);
|
|
333
|
+
for (const f of it.filesModified ?? []) lines.push(` - ✏️ \`${f}\``);
|
|
334
|
+
for (const f of it.filesDeleted ?? []) lines.push(` - 🗑️ ~~~~\`${f}\`~~~~`);
|
|
335
|
+
}
|
|
336
|
+
lines.push("");
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return lines.join("\n");
|
|
340
|
+
}
|
|
341
|
+
function formatTimeline(items, repoUrl) {
|
|
342
|
+
const lines = [];
|
|
343
|
+
lines.push("## 变更日志");
|
|
344
|
+
lines.push("");
|
|
345
|
+
const dayMap = groupByDay(items);
|
|
346
|
+
for (const [day, list] of dayMap) {
|
|
347
|
+
const dayAdded = list.reduce((s, c) => s + (c.filesAdded?.length || 0), 0);
|
|
348
|
+
const dayMod = list.reduce((s, c) => s + (c.filesModified?.length || 0), 0);
|
|
349
|
+
const dayDel = list.reduce((s, c) => s + (c.filesDeleted?.length || 0), 0);
|
|
350
|
+
lines.push(`### ${day} \`✏️ ${dayMod}+\` \`➕ ${dayAdded}+\` \`🗑️ ${dayDel}+\``);
|
|
351
|
+
for (const it of list) {
|
|
352
|
+
const link = repoUrl ? ` ([\`${it.short}\`](${repoUrl}/commit/${it.hash}))` : "";
|
|
353
|
+
const email = ` <${it.email}>`;
|
|
354
|
+
const typeIcon = getTypeIcon(it.type);
|
|
355
|
+
const time = getTime(it.date);
|
|
356
|
+
const scopeFmt = it.scope ? `\`${it.scope}\`` : "";
|
|
357
|
+
const typeLabel = it.type === "chore" ? "**chore**" : `**${it.type}**`;
|
|
358
|
+
lines.push(`- ${typeIcon} ${typeLabel} ${scopeFmt ? `${scopeFmt}: ` : ""}${it.description}`);
|
|
359
|
+
lines.push(` > **🕒 ${time}** · \`➕${it.added}\` / \`➖${it.deleted}\``);
|
|
360
|
+
lines.push(` > \`👤 ${it.author}\` ${email}${link}`);
|
|
361
|
+
for (const f of it.filesAdded ?? []) lines.push(` - ➕ \`${f}\``);
|
|
362
|
+
for (const f of it.filesModified ?? []) lines.push(` - ✏️ \`${f}\``);
|
|
363
|
+
for (const f of it.filesDeleted ?? []) lines.push(` - 🗑️ ~~~~\`${f}\`~~~~`);
|
|
364
|
+
}
|
|
365
|
+
lines.push("");
|
|
366
|
+
}
|
|
367
|
+
return lines.join("\n");
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* 生成两种样式的 CHANGELOG 文件
|
|
371
|
+
* @param {{ lang: Lang, format: 'group' | 'timeline' | 'both', groupOutput: string, timelineOutput: string }} options 生成选项
|
|
372
|
+
* @param {'zh-cn'|'en-us'} options.lang 输出语言
|
|
373
|
+
* @param {'group'|'timeline'|'both'} options.format 输出样式
|
|
374
|
+
* @param {string} options.groupOutput 分组样式输出文件路径
|
|
375
|
+
* @param {string} options.timelineOutput 时间轴样式输出文件路径
|
|
376
|
+
* @returns {Promise<void>} 异步任务
|
|
377
|
+
*/
|
|
378
|
+
async function generateChangelogFiles(options) {
|
|
379
|
+
const repoRoot = await execCommand("git", ["rev-parse", "--show-toplevel"]);
|
|
380
|
+
const homepage = await readHomepage();
|
|
381
|
+
const tags = await getTags();
|
|
382
|
+
let title = "未发布";
|
|
383
|
+
let date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
384
|
+
let range = "";
|
|
385
|
+
if (tags.length >= 1) {
|
|
386
|
+
const latest = tags[0];
|
|
387
|
+
const prev = tags[1];
|
|
388
|
+
title = latest;
|
|
389
|
+
date = await getDateForRef(latest);
|
|
390
|
+
range = prev ? `${prev}..${latest}` : `${await getRootCommit()}..${latest}`;
|
|
391
|
+
} else range = "HEAD";
|
|
392
|
+
let items = await getCommitsInRange(range);
|
|
393
|
+
items = await enrichCommit(items);
|
|
394
|
+
const fallback = [
|
|
395
|
+
`## ${title} - ${date}`,
|
|
396
|
+
"",
|
|
397
|
+
"- 无符合 Conventional Commits 标准的提交",
|
|
398
|
+
""
|
|
399
|
+
].join("\n");
|
|
400
|
+
if (options.format === "group" || options.format === "both") {
|
|
401
|
+
const content = items.length ? formatSection(title, date, items, homepage, options.lang) : fallback;
|
|
402
|
+
await prependFile(path.join(repoRoot, options.groupOutput), content);
|
|
403
|
+
}
|
|
404
|
+
if (options.format === "timeline" || options.format === "both") {
|
|
405
|
+
const content = items.length ? formatTimeline(items, homepage) : fallback;
|
|
406
|
+
await prependFile(path.join(repoRoot, options.timelineOutput), content);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function getDay(d) {
|
|
410
|
+
return d.match(/^(\d{4}-\d{2}-\d{2})/)?.[1] || d;
|
|
411
|
+
}
|
|
412
|
+
function getTime(d) {
|
|
413
|
+
return d.match(/^\d{4}-\d{2}-\d{2}\s(\d{2}:\d{2})/)?.[1] || d;
|
|
414
|
+
}
|
|
415
|
+
function groupByDay(items) {
|
|
416
|
+
const map = /* @__PURE__ */ new Map();
|
|
417
|
+
for (const it of items) {
|
|
418
|
+
const day = getDay(it.date);
|
|
419
|
+
const arr = map.get(day) || [];
|
|
420
|
+
arr.push(it);
|
|
421
|
+
map.set(day, arr);
|
|
422
|
+
}
|
|
423
|
+
return map;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
//#endregion
|
|
427
|
+
//#region src/commands/cleanup.ts
|
|
428
|
+
async function cleanup(paths) {
|
|
429
|
+
await rimraf(paths, { glob: true });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/config/index.ts
|
|
434
|
+
const defaultOptions = {
|
|
435
|
+
cwd: process.cwd(),
|
|
436
|
+
cleanupDirs: [
|
|
437
|
+
"**/dist",
|
|
438
|
+
"**/node_modules",
|
|
439
|
+
"!node_modules/**"
|
|
440
|
+
],
|
|
441
|
+
lang: "zh-cn",
|
|
442
|
+
ncuCommandArgs: ["--deep", "-u"],
|
|
443
|
+
gitCommitVerifyIgnores: [
|
|
444
|
+
/^((Merge pull request)|(Merge (.*?) into (.*)|(Merge branch (.*)))(?:\r?\n)*$)/m,
|
|
445
|
+
/^(Merge tag (.*))(?:\r?\n)*$/m,
|
|
446
|
+
/^(R|r)evert (.*)/,
|
|
447
|
+
/^(amend|fixup|squash)!/,
|
|
448
|
+
/^(Merged (.*?)(in|into) (.*)|Merged PR (.*): (.*))/,
|
|
449
|
+
/^Merge remote-tracking branch(\s*)(.*)/,
|
|
450
|
+
/^Automatic merge(.*)/,
|
|
451
|
+
/^Auto-merged (.*?) into (.*)/
|
|
452
|
+
],
|
|
453
|
+
release: {
|
|
454
|
+
execute: "qui cl",
|
|
455
|
+
push: true
|
|
456
|
+
},
|
|
457
|
+
changelog: {
|
|
458
|
+
groupOutput: "CHANGELOG.md",
|
|
459
|
+
timelineOutput: "CHANGELOG_TIMELINE.md",
|
|
460
|
+
formats: "both"
|
|
461
|
+
},
|
|
462
|
+
gitCommit: { add: true }
|
|
463
|
+
};
|
|
464
|
+
async function loadCliOptions(overrides, cwd = process.cwd()) {
|
|
465
|
+
const { config } = await loadConfig({
|
|
466
|
+
name: "quiteer",
|
|
467
|
+
defaults: defaultOptions,
|
|
468
|
+
overrides,
|
|
469
|
+
cwd,
|
|
470
|
+
packageJson: true
|
|
222
471
|
});
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
472
|
+
return config;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
//#endregion
|
|
476
|
+
//#region src/commands/generate-cfg.ts
|
|
477
|
+
/**
|
|
478
|
+
* 函数:在项目根目录生成 quiteer 配置文件
|
|
479
|
+
* 作用:若已有配置则询问是否覆盖;按默认配置生成 `quiteer.config.ts`
|
|
480
|
+
*/
|
|
481
|
+
async function generateConfig() {
|
|
482
|
+
const cwd = process.cwd();
|
|
483
|
+
const configPath = path.join(cwd, "quiteer.config.ts");
|
|
484
|
+
const regexList = defaultOptions.gitCommitVerifyIgnores.map((r) => r.toString());
|
|
485
|
+
const indentJson = (val) => {
|
|
486
|
+
return JSON.stringify(val, null, 2).split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n");
|
|
487
|
+
};
|
|
488
|
+
await writeFile(configPath, [
|
|
489
|
+
"// 自动生成的 quiteer 配置文件",
|
|
490
|
+
"// 说明:可在此文件中覆盖默认配置项,所有未定义项将采用默认值",
|
|
491
|
+
"",
|
|
492
|
+
"export default {",
|
|
493
|
+
" // 清理命令默认清理的目录(支持 glob)",
|
|
494
|
+
` cleanupDirs: ${indentJson(defaultOptions.cleanupDirs)},`,
|
|
495
|
+
"",
|
|
496
|
+
" // CLI 提示语言:zh-cn | en-us",
|
|
497
|
+
` lang: ${JSON.stringify(defaultOptions.lang)},`,
|
|
498
|
+
"",
|
|
499
|
+
" // npm-check-updates 命令参数",
|
|
500
|
+
` ncuCommandArgs: ${indentJson(defaultOptions.ncuCommandArgs)},`,
|
|
501
|
+
"",
|
|
502
|
+
" // 提交信息校验的忽略规则(正则)",
|
|
503
|
+
" gitCommitVerifyIgnores: [",
|
|
504
|
+
...regexList.map((r, idx, arr) => ` ${r}${idx < arr.length - 1 ? "," : ""}`),
|
|
505
|
+
" ],",
|
|
506
|
+
"",
|
|
507
|
+
" // 发布配置:执行的发布命令与是否推送",
|
|
508
|
+
` release: ${indentJson(defaultOptions.release)},`,
|
|
509
|
+
"",
|
|
510
|
+
" // changelog 配置(仅保留输出文件与样式)",
|
|
511
|
+
` changelog: ${indentJson(defaultOptions.changelog)},`,
|
|
512
|
+
"",
|
|
513
|
+
" // git-commit 配置:是否默认添加变更文件到暂存区",
|
|
514
|
+
` gitCommit: ${indentJson(defaultOptions.gitCommit)},`,
|
|
515
|
+
"}",
|
|
516
|
+
""
|
|
517
|
+
].join("\n"), "utf8");
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region src/commands/git-commit.ts
|
|
522
|
+
/**
|
|
523
|
+
* 交互式添加变更文件到暂存区
|
|
524
|
+
* - 先询问是否添加全部;若否,列出变更文件供多选后执行 git add
|
|
525
|
+
* - 在非交互或输入中断等情况下安全退出,不抛出未处理异常
|
|
526
|
+
* @returns {Promise<void>} 异步任务
|
|
527
|
+
*/
|
|
528
|
+
async function gitCommitAdd() {
|
|
529
|
+
const { prompt: ask } = enquirer;
|
|
530
|
+
try {
|
|
531
|
+
if (!!!(await ask([{
|
|
532
|
+
name: "confirm",
|
|
533
|
+
type: "confirm",
|
|
534
|
+
message: "是否添加所有变更文件到暂存区?"
|
|
535
|
+
}]))?.confirm) {
|
|
536
|
+
const files = (await execCommand("git", ["diff", "--name-only"])).split("\n").filter(Boolean);
|
|
537
|
+
if (files.length === 0) return;
|
|
538
|
+
const selected = (await ask([{
|
|
539
|
+
name: "selected",
|
|
540
|
+
type: "multiselect",
|
|
541
|
+
message: "选择需要添加到暂存区的文件(空格选择,回车确认)",
|
|
542
|
+
choices: files.map((f) => ({
|
|
543
|
+
name: f,
|
|
544
|
+
value: f
|
|
545
|
+
}))
|
|
546
|
+
}]))?.selected ?? [];
|
|
547
|
+
if (!selected.length) return;
|
|
548
|
+
await execCommand("git", ["add", ...selected], { stdio: "inherit" });
|
|
549
|
+
return;
|
|
244
550
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
551
|
+
await execCommand("git", ["add", "."], { stdio: "inherit" });
|
|
552
|
+
} catch {}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* 交互式生成符合 Conventional Commits 的提交信息并执行提交
|
|
556
|
+
* - 支持取消或非交互环境下安全退出
|
|
557
|
+
* @param {Lang} lang 交互提示语言
|
|
558
|
+
* @returns {Promise<void>} 异步任务
|
|
559
|
+
*/
|
|
560
|
+
async function gitCommit(lang = "en-us") {
|
|
561
|
+
try {
|
|
562
|
+
const { prompt: ask } = enquirer;
|
|
563
|
+
const { gitCommitMessages, gitCommitTypes, gitCommitScopes } = locales[lang];
|
|
564
|
+
const typesChoices = gitCommitTypes.map(([value, msg]) => {
|
|
565
|
+
return {
|
|
566
|
+
name: value,
|
|
567
|
+
message: `${`${value}:`.padEnd(12)}${msg}`
|
|
568
|
+
};
|
|
569
|
+
});
|
|
570
|
+
const scopesChoices = gitCommitScopes.map(([value, msg]) => ({
|
|
571
|
+
name: value,
|
|
572
|
+
message: `${value.padEnd(30)} (${msg})`
|
|
573
|
+
}));
|
|
574
|
+
const result = await ask([
|
|
575
|
+
{
|
|
576
|
+
name: "types",
|
|
577
|
+
type: "select",
|
|
578
|
+
message: gitCommitMessages.types,
|
|
579
|
+
choices: typesChoices
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
name: "scopes",
|
|
583
|
+
type: "select",
|
|
584
|
+
message: gitCommitMessages.scopes,
|
|
585
|
+
choices: scopesChoices
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: "description",
|
|
589
|
+
type: "text",
|
|
590
|
+
message: gitCommitMessages.description
|
|
591
|
+
}
|
|
592
|
+
]);
|
|
593
|
+
if (!result) return;
|
|
594
|
+
const breaking = result.description.startsWith("!") ? "!" : "";
|
|
595
|
+
const description = result.description.replace(/^!/, "").trim();
|
|
596
|
+
await execCommand("git", [
|
|
597
|
+
"commit",
|
|
598
|
+
"-m",
|
|
599
|
+
`${result.types}(${result.scopes})${breaking}: ${description}`
|
|
600
|
+
], { stdio: "inherit" });
|
|
601
|
+
} catch {}
|
|
253
602
|
}
|
|
254
603
|
/** Git commit message verify */
|
|
255
604
|
async function gitCommitVerify(lang = "zh-cn", ignores = []) {
|
|
@@ -264,14 +613,77 @@ async function gitCommitVerify(lang = "zh-cn", ignores = []) {
|
|
|
264
613
|
|
|
265
614
|
//#endregion
|
|
266
615
|
//#region src/commands/release.ts
|
|
267
|
-
|
|
616
|
+
/**
|
|
617
|
+
* 版本管理:交互选择需要提升版本的包、创建自定义前缀标签、生成变更日志
|
|
618
|
+
* - 多层级仓库:通过交互选择需要更新的包,未选中的包跳过
|
|
619
|
+
* - 标签:支持自定义前缀,命名为 <prefix>-v${version},存在则跳过创建
|
|
620
|
+
* - 变更日志:默认生成 CHANGELOG.md 与 CHANGELOG_TIMELINE.md(中文,both 格式)
|
|
621
|
+
* - 不包含推送操作,如需发布请手动执行 git push / pnpm publish
|
|
622
|
+
* @param {string} [tagPrefix] 标签前缀(可选,留空则交互输入)
|
|
623
|
+
* @returns {Promise<void>} 异步任务
|
|
624
|
+
*/
|
|
625
|
+
async function release(tagPrefix) {
|
|
626
|
+
const { prompt: ask } = enquirer;
|
|
627
|
+
const repoRoot = await execCommand("git", ["rev-parse", "--show-toplevel"]);
|
|
628
|
+
const files = (await execCommand("git", [
|
|
629
|
+
"-C",
|
|
630
|
+
repoRoot,
|
|
631
|
+
"ls-files",
|
|
632
|
+
"**/package.json"
|
|
633
|
+
])).split("\n").filter(Boolean).filter((p) => !p.includes("node_modules"));
|
|
634
|
+
const choices = [];
|
|
635
|
+
for (const rel of files) try {
|
|
636
|
+
const abs = path.join(repoRoot, rel);
|
|
637
|
+
const json = JSON.parse(await readFile(abs, "utf8"));
|
|
638
|
+
if (json?.name) choices.push({
|
|
639
|
+
name: `${json.name} (${rel})`,
|
|
640
|
+
value: rel
|
|
641
|
+
});
|
|
642
|
+
} catch {}
|
|
643
|
+
const selected = ((await ask([{
|
|
644
|
+
name: "selected",
|
|
645
|
+
type: "multiselect",
|
|
646
|
+
message: "选择需要提升版本的包(空格选择,回车确认)",
|
|
647
|
+
choices
|
|
648
|
+
}]))?.selected ?? []).map((s) => {
|
|
649
|
+
return choices.find((c) => c.value === s || c.name === s)?.value || s;
|
|
650
|
+
}).filter(Boolean);
|
|
651
|
+
if (!selected.length) return;
|
|
268
652
|
await versionBump({
|
|
269
|
-
files:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
653
|
+
files: selected.map((rel) => path.join(repoRoot, rel)),
|
|
654
|
+
all: false,
|
|
655
|
+
tag: false,
|
|
656
|
+
commit: "chore(release): v%s"
|
|
657
|
+
});
|
|
658
|
+
const firstPkgPath = path.join(repoRoot, selected[0]);
|
|
659
|
+
const version$1 = JSON.parse(await readFile(firstPkgPath, "utf8")).version;
|
|
660
|
+
let prefix = tagPrefix;
|
|
661
|
+
if (prefix === void 0) try {
|
|
662
|
+
prefix = (await ask([{
|
|
663
|
+
name: "prefix",
|
|
664
|
+
type: "text",
|
|
665
|
+
message: "请输入标签前缀(可留空)"
|
|
666
|
+
}]))?.prefix?.trim() || "";
|
|
667
|
+
} catch {
|
|
668
|
+
prefix = "";
|
|
669
|
+
}
|
|
670
|
+
const tagName = `${prefix ? `${prefix}-` : ""}v${version$1}`;
|
|
671
|
+
if (!(await execCommand("git", [
|
|
672
|
+
"tag",
|
|
673
|
+
"--list",
|
|
674
|
+
tagName
|
|
675
|
+
])).trim()) await execCommand("git", [
|
|
676
|
+
"tag",
|
|
677
|
+
"--annotate",
|
|
678
|
+
"--message",
|
|
679
|
+
`chore(projects): release ${tagName}`,
|
|
680
|
+
tagName
|
|
681
|
+
]);
|
|
682
|
+
await generateChangelogFiles({
|
|
683
|
+
lang: "zh-cn",
|
|
684
|
+
format: "both",
|
|
685
|
+
groupOutput: "CHANGELOG.md",
|
|
686
|
+
timelineOutput: "CHANGELOG_TIMELINE.md"
|
|
275
687
|
});
|
|
276
688
|
}
|
|
277
689
|
|
|
@@ -291,35 +703,43 @@ async function setupCli() {
|
|
|
291
703
|
* @returns Promise<void>
|
|
292
704
|
*/
|
|
293
705
|
const cliOptions = await loadCliOptions();
|
|
294
|
-
const cli = cac(blue("quiteer
|
|
295
|
-
cli.
|
|
296
|
-
cli.command("generate-config", "在项目根目录下生成配置文件").alias("g").action(async () => {
|
|
706
|
+
const cli = cac(blue("quiteer"));
|
|
707
|
+
cli.command("generate-config", `${bgGreen(white("便捷命令"))} ${lightCyan("qui g")} 在项目根目录下生成配置文件`).alias("g").action(async () => {
|
|
297
708
|
await generateConfig();
|
|
298
709
|
});
|
|
299
|
-
cli.command("remove [path]", "
|
|
710
|
+
cli.command("remove [path]", `${bgGreen(white("便捷命令"))} ${lightBlue("qui rm")} 删除单个或者多个文件 , 多个值用逗号分隔,递归删除`).alias("rm").action(async (paths) => {
|
|
300
711
|
if (paths && paths.includes(",")) await cleanup(paths.split(","));
|
|
301
712
|
else if (paths) await cleanup([paths]);
|
|
302
713
|
else console.info("quiteer-script :>> ", gray("无事发生"));
|
|
303
714
|
});
|
|
304
|
-
cli.command("cleanup [path]", "清除目录 node_modules、dist
|
|
715
|
+
cli.command("cleanup [path]", `${bgGreen(white("便捷命令"))} ${lightBlue("qui c")} 清除目录 node_modules、dist 等`).alias("c").action(async (paths) => {
|
|
305
716
|
if (paths && paths.includes(",")) await cleanup(paths.split(","));
|
|
306
717
|
else if (paths) await cleanup([paths]);
|
|
307
718
|
else await cleanup(cliOptions.cleanupDirs);
|
|
308
719
|
});
|
|
309
|
-
cli.command("update-pkg", "更新 package.json
|
|
720
|
+
cli.command("update-pkg", `${bgGreen(white("便捷命令"))} ${lightBlue("qui u")} 更新 package.json 依赖版本`).alias("u").action(async () => {
|
|
310
721
|
await updatePkg(cliOptions.ncuCommandArgs);
|
|
311
722
|
});
|
|
312
|
-
cli.command("git-commit", "
|
|
723
|
+
cli.command("git-commit", `${bgGreen(white("便捷命令"))} ${lightBlue("qui gc")} git 提交前后的操作和规范等`).alias("gc").option("--add", "添加所有变更文件到暂存区", { default: cliOptions.gitCommit.add }).option("-l ,--lang", "校验提交信息的语言", { default: cliOptions.lang }).action(async (args) => {
|
|
313
724
|
if (args?.add) await gitCommitAdd();
|
|
314
725
|
await gitCommit(args?.lang);
|
|
315
726
|
});
|
|
316
|
-
cli.command("git-commit-verify", "校验提交信息是否符合 Conventional Commits
|
|
727
|
+
cli.command("git-commit-verify", `${bgGreen(white("便捷命令"))} ${lightBlue("qui gv")} 校验提交信息是否符合 Conventional Commits 标准`).alias("gv").option("-l --lang", "校验提交信息的语言", { default: cliOptions.lang }).action(async (args) => {
|
|
317
728
|
await gitCommitVerify(args?.lang, cliOptions.gitCommitVerifyIgnores);
|
|
318
729
|
});
|
|
319
|
-
cli.command("release", "
|
|
320
|
-
await release(args?.
|
|
730
|
+
cli.command("release", `${bgGreen(white("便捷命令"))} ${lightBlue("qui r")} 版本管理:选择包并提升版本,创建自定义前缀标签并生成 changelog`).alias("r").option("--tag-prefix <prefix>", "标签前缀(可选,留空则交互输入)").action(async (args) => {
|
|
731
|
+
await release(args?.tagPrefix);
|
|
732
|
+
});
|
|
733
|
+
cli.command("changelog", `${bgGreen(white("便捷命令"))} ${lightBlue("qui cl")} 生成变更日志 CHANGELOG.md、CHANGELOG_TIMELINE`).alias("cl").option("-f, --format <format>", "输出样式:group|timeline|both", { default: cliOptions.changelog.formats }).option("--group-output <path>", "分组样式输出文件", { default: cliOptions.changelog.groupOutput }).option("--timeline-output <path>", "时间轴样式输出文件", { default: cliOptions.changelog.timelineOutput }).option("-l, --lang <lang>", "输出语言", { default: cliOptions.lang }).action(async (args) => {
|
|
734
|
+
const cfg = cliOptions.changelog;
|
|
735
|
+
await generateChangelogFiles({
|
|
736
|
+
lang: args.lang || cliOptions.lang,
|
|
737
|
+
format: args.format || cfg.formats,
|
|
738
|
+
groupOutput: args.groupOutput || cfg.groupOutput,
|
|
739
|
+
timelineOutput: args.timelineOutput || cfg.timelineOutput
|
|
740
|
+
});
|
|
321
741
|
});
|
|
322
|
-
cli.parse();
|
|
742
|
+
cli.version(lightGreen(version)).help().parse();
|
|
323
743
|
}
|
|
324
744
|
setupCli();
|
|
325
745
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quiteer/scripts",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"homepage": "https://github.com/TaiAiAc/web",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public",
|
|
@@ -49,6 +49,6 @@
|
|
|
49
49
|
"scripts": {
|
|
50
50
|
"dev": "tsdown -w",
|
|
51
51
|
"build": "tsdown",
|
|
52
|
-
"release": "
|
|
52
|
+
"release": "qui r --tag-prefix scripts"
|
|
53
53
|
}
|
|
54
54
|
}
|