@soybeanjs/cli 0.4.1 → 0.5.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.
Files changed (3) hide show
  1. package/dist/index.cjs +587 -11
  2. package/dist/index.mjs +586 -13
  3. package/package.json +12 -7
package/dist/index.cjs CHANGED
@@ -3,7 +3,6 @@
3
3
 
4
4
  const cac = require('cac');
5
5
  const enquirer = require('enquirer');
6
- const execa = require('execa');
7
6
  const actualFS = require('fs');
8
7
  const kolorist = require('kolorist');
9
8
  const rimraf = require('rimraf');
@@ -13,6 +12,11 @@ const promises = require('fs/promises');
13
12
  const EE = require('events');
14
13
  const Stream = require('stream');
15
14
  const stringdecoder = require('string_decoder');
15
+ const cliProgress = require('cli-progress');
16
+ const ofetch = require('ofetch');
17
+ const dayjs = require('dayjs');
18
+ const convertGitmoji = require('convert-gitmoji');
19
+ const versionBumpp = require('bumpp');
16
20
 
17
21
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
18
22
 
@@ -34,8 +38,55 @@ const actualFS__namespace = /*#__PURE__*/_interopNamespaceCompat(actualFS);
34
38
  const EE__default = /*#__PURE__*/_interopDefaultCompat(EE);
35
39
  const Stream__default = /*#__PURE__*/_interopDefaultCompat(Stream);
36
40
  const stringdecoder__default = /*#__PURE__*/_interopDefaultCompat(stringdecoder);
41
+ const cliProgress__default = /*#__PURE__*/_interopDefaultCompat(cliProgress);
42
+ const dayjs__default = /*#__PURE__*/_interopDefaultCompat(dayjs);
43
+ const versionBumpp__default = /*#__PURE__*/_interopDefaultCompat(versionBumpp);
37
44
 
38
- const version = "0.4.1";
45
+ const version = "0.5.0";
46
+
47
+ async function execCommand(cmd, args, options) {
48
+ const { execa } = await import('execa');
49
+ const res = await execa(cmd, args, options);
50
+ return res?.stdout?.trim() || "";
51
+ }
52
+ function notNullish(v) {
53
+ return v !== null && v !== void 0;
54
+ }
55
+ function partition(array, ...filters) {
56
+ const result = new Array(filters.length + 1).fill(null).map(() => []);
57
+ array.forEach((e, idx, arr) => {
58
+ let i = 0;
59
+ for (const filter of filters) {
60
+ if (filter(e, idx, arr)) {
61
+ result[i].push(e);
62
+ return;
63
+ }
64
+ i += 1;
65
+ }
66
+ result[i].push(e);
67
+ });
68
+ return result;
69
+ }
70
+ function groupBy(items, key, groups = {}) {
71
+ for (const item of items) {
72
+ const v = item[key];
73
+ groups[v] = groups[v] || [];
74
+ groups[v].push(item);
75
+ }
76
+ return groups;
77
+ }
78
+ function capitalize(str) {
79
+ return str.charAt(0).toUpperCase() + str.slice(1);
80
+ }
81
+ function join(array, glue = ", ", finalGlue = " and ") {
82
+ if (!array || array.length === 0)
83
+ return "";
84
+ if (array.length === 1)
85
+ return array[0];
86
+ if (array.length === 2)
87
+ return array.join(finalGlue);
88
+ return `${array.slice(0, -1).join(glue)}${finalGlue}${array.slice(-1)}`;
89
+ }
39
90
 
40
91
  const gitCommitTypes = [
41
92
  { value: "init", title: "init: \u9879\u76EE\u521D\u59CB\u5316" },
@@ -90,7 +141,7 @@ async function gitCommit() {
90
141
  }
91
142
  ]);
92
143
  const commitMsg = `${result.types}(${result.scopes}): ${result.description}`;
93
- execa.execa("git", ["commit", "-m", commitMsg], { stdio: "inherit" });
144
+ execCommand("git", ["commit", "-m", commitMsg], { stdio: "inherit" });
94
145
  }
95
146
 
96
147
  function gitCommitVerify() {
@@ -7429,14 +7480,14 @@ async function initSimpleGitHooks() {
7429
7480
  const existHusky = actualFS.existsSync(huskyDir);
7430
7481
  if (existHusky) {
7431
7482
  await rimraf.rimraf(".husky");
7432
- await execa.execa("git", ["config", "core.hooksPath", ".git/hooks/"], { stdio: "inherit" });
7483
+ await execCommand("git", ["config", "core.hooksPath", ".git/hooks/"], { stdio: "inherit" });
7433
7484
  }
7434
7485
  await rimraf.rimraf(".git/hooks");
7435
- await execa.execa("npx", ["simple-git-hooks"], { stdio: "inherit" });
7486
+ await execCommand("npx", ["simple-git-hooks"], { stdio: "inherit" });
7436
7487
  }
7437
7488
 
7438
7489
  async function updatePkg() {
7439
- execa.execa("npx", ["ncu", "--deep", "-u"], { stdio: "inherit" });
7490
+ execCommand("npx", ["ncu", "--deep", "-u"], { stdio: "inherit" });
7440
7491
  }
7441
7492
 
7442
7493
  function prettierFormat() {
@@ -7456,26 +7507,541 @@ function prettierFormat() {
7456
7507
  "!.github",
7457
7508
  "!__snapshots__"
7458
7509
  ];
7459
- execa.execa("npx", ["prettier", ".", "--write", ...formatFiles], {
7510
+ execCommand("npx", ["prettier", ".", "--write", ...formatFiles], {
7460
7511
  stdio: "inherit"
7461
7512
  });
7462
7513
  }
7463
7514
 
7464
7515
  async function eslintPretter() {
7465
- await execa.execa("npx", ["eslint", ".", "--fix"], {
7516
+ await execCommand("npx", ["eslint", ".", "--fix"], {
7466
7517
  stdio: "inherit"
7467
7518
  });
7468
- await execa.execa("npx", ["soy", "prettier-format"], {
7519
+ await execCommand("npx", ["soy", "prettier-format"], {
7469
7520
  stdio: "inherit"
7470
7521
  });
7471
7522
  }
7472
7523
 
7473
7524
  function lintStaged() {
7474
- execa.execa("npx", ["lint-staged", "--config", "@soybeanjs/cli/lint-staged"], { stdio: "inherit" });
7525
+ execCommand("npx", ["lint-staged", "--config", "@soybeanjs/cli/lint-staged"], { stdio: "inherit" });
7526
+ }
7527
+
7528
+ async function getTotalGitTags() {
7529
+ const tagStr = await execCommand("git", ["--no-pager", "tag", "-l", "--sort=creatordate"]);
7530
+ const tags = tagStr.split("\n");
7531
+ return tags;
7532
+ }
7533
+ async function getTagDateMap() {
7534
+ const tagDateStr = await execCommand("git", [
7535
+ "--no-pager",
7536
+ "log",
7537
+ "--tags",
7538
+ "--simplify-by-decoration",
7539
+ "--pretty=format:%ci %d"
7540
+ ]);
7541
+ const TAG_MARK = "(tag: ";
7542
+ const map = /* @__PURE__ */ new Map();
7543
+ const dates = tagDateStr.split("\n").filter((item) => item.includes(TAG_MARK));
7544
+ dates.forEach((item) => {
7545
+ const [dateStr, tagStr] = item.split(TAG_MARK);
7546
+ const date = dayjs__default(dateStr.trim()).format("YYYY-MM-DD");
7547
+ const VERSION_REG = /v\d+\.\d+\.\d+/;
7548
+ const tag = tagStr.match(VERSION_REG)?.[0];
7549
+ if (tag && date) {
7550
+ map.set(tag.trim(), date);
7551
+ }
7552
+ });
7553
+ return map;
7554
+ }
7555
+ function getFromToTags(tags) {
7556
+ const result = [];
7557
+ tags.forEach((tag, index) => {
7558
+ if (index < tags.length - 1) {
7559
+ result.push({ from: tag, to: tags[index + 1] });
7560
+ }
7561
+ });
7562
+ return result;
7563
+ }
7564
+ async function getLastGitTag(delta = 0) {
7565
+ const tags = await getTotalGitTags();
7566
+ return tags[tags.length + delta - 1];
7567
+ }
7568
+ async function getGitMainBranchName() {
7569
+ const main = await execCommand("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
7570
+ return main;
7571
+ }
7572
+ async function getCurrentGitBranch() {
7573
+ const tag = await execCommand("git", ["tag", "--points-at", "HEAD"]);
7574
+ const main = getGitMainBranchName();
7575
+ return tag || main;
7576
+ }
7577
+ async function getGitHubRepo() {
7578
+ const url = await execCommand("git", ["config", "--get", "remote.origin.url"]);
7579
+ const match = url.match(/github\.com[/:]([\w\d._-]+?)\/([\w\d._-]+?)(\.git)?$/i);
7580
+ if (!match) {
7581
+ throw new Error(`Can not parse GitHub repo from url ${url}`);
7582
+ }
7583
+ return `${match[1]}/${match[2]}`;
7584
+ }
7585
+ function isPrerelease(version) {
7586
+ const REG = /^[^.]*[\d.]+$/;
7587
+ return !REG.test(version);
7588
+ }
7589
+ function getFirstGitCommit() {
7590
+ return execCommand("git", ["rev-list", "--max-parents=0", "HEAD"]);
7591
+ }
7592
+ async function getGitDiff(from, to = "HEAD") {
7593
+ const rawGit = await execCommand("git", [
7594
+ "--no-pager",
7595
+ "log",
7596
+ `${from ? `${from}...` : ""}${to}`,
7597
+ '--pretty="----%n%s|%h|%an|%ae%n%b"',
7598
+ "--name-status"
7599
+ ]);
7600
+ const rwaGitLines = rawGit.split("----\n").splice(1);
7601
+ const gitCommits = rwaGitLines.map((line) => {
7602
+ const [firstLine, ...body] = line.split("\n");
7603
+ const [message, shortHash, authorName, authorEmail] = firstLine.split("|");
7604
+ const gitCommmit = {
7605
+ message,
7606
+ shortHash,
7607
+ author: { name: authorName, email: authorEmail },
7608
+ body: body.join("\n")
7609
+ };
7610
+ return gitCommmit;
7611
+ });
7612
+ return gitCommits;
7613
+ }
7614
+ function parseGitCommit(commit, scopeMap) {
7615
+ const ConventionalCommitRegex = /(?<type>[a-z]+)(\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
7616
+ const CoAuthoredByRegex = /co-authored-by:\s*(?<name>.+)(<(?<email>.+)>)/gim;
7617
+ const PullRequestRE = /\([a-z]*(#\d+)\s*\)/gm;
7618
+ const IssueRE = /(#\d+)/gm;
7619
+ const match = commit.message.match(ConventionalCommitRegex);
7620
+ if (!match?.groups) {
7621
+ return null;
7622
+ }
7623
+ const type = match.groups.type;
7624
+ let scope = match.groups.scope || "";
7625
+ scope = scopeMap[scope] || scope;
7626
+ const isBreaking = Boolean(match.groups.breaking);
7627
+ let description = match.groups.description;
7628
+ const references = [];
7629
+ for (const m of description.matchAll(PullRequestRE)) {
7630
+ references.push({ type: "pull-request", value: m[1] });
7631
+ }
7632
+ for (const m of description.matchAll(IssueRE)) {
7633
+ if (!references.some((i) => i.value === m[1])) {
7634
+ references.push({ type: "issue", value: m[1] });
7635
+ }
7636
+ }
7637
+ references.push({ value: commit.shortHash, type: "hash" });
7638
+ description = description.replace(PullRequestRE, "").trim();
7639
+ const authors = [commit.author];
7640
+ const matchs = commit.body.matchAll(CoAuthoredByRegex);
7641
+ for (const $match of matchs) {
7642
+ const { name = "", email = "" } = $match.groups || {};
7643
+ const author = {
7644
+ name: name.trim(),
7645
+ email: email.trim()
7646
+ };
7647
+ authors.push(author);
7648
+ }
7649
+ return {
7650
+ ...commit,
7651
+ authors,
7652
+ resolvedAuthors: [],
7653
+ description,
7654
+ type,
7655
+ scope,
7656
+ references,
7657
+ isBreaking
7658
+ };
7659
+ }
7660
+ async function getGitCommits(from, to = "HEAD", scopeMap = {}) {
7661
+ const rwaGitCommits = await getGitDiff(from, to);
7662
+ const commits = rwaGitCommits.map((commit) => parseGitCommit(commit, scopeMap)).filter(notNullish);
7663
+ return commits;
7664
+ }
7665
+ function getHeaders(githubToken) {
7666
+ return {
7667
+ accept: "application/vnd.github.v3+json",
7668
+ authorization: `token ${githubToken}`
7669
+ };
7670
+ }
7671
+ async function getResolvedAuthorLogin(params) {
7672
+ const { github, githubToken, commitHashes, email } = params;
7673
+ let login = "";
7674
+ if (!githubToken) {
7675
+ return login;
7676
+ }
7677
+ try {
7678
+ const data = await ofetch.ofetch(`https://api.github.com/search/users?q=${encodeURIComponent(email)}`);
7679
+ login = data.items[0].login;
7680
+ } catch {
7681
+ }
7682
+ if (login) {
7683
+ return login;
7684
+ }
7685
+ if (commitHashes.length) {
7686
+ try {
7687
+ const data = await ofetch.ofetch(`https://api.github.com/repos/${github}/commits/${commitHashes[0]}`, {
7688
+ headers: getHeaders(githubToken)
7689
+ });
7690
+ login = data?.author?.login || "";
7691
+ } catch (e) {
7692
+ }
7693
+ }
7694
+ return login;
7695
+ }
7696
+ async function getGitCommitsAndResolvedAuthors(commits, github, githubToken, resolvedLogins) {
7697
+ const resultCommits = [];
7698
+ const map = /* @__PURE__ */ new Map();
7699
+ for (const commit of commits) {
7700
+ const resolvedAuthors = [];
7701
+ for (const [index, author] of Object.entries(commit.authors)) {
7702
+ const { email, name } = author;
7703
+ if (email && name) {
7704
+ const commitHashes = [];
7705
+ if (index === "0") {
7706
+ commitHashes.push(commit.shortHash);
7707
+ }
7708
+ const resolvedAuthor = {
7709
+ name,
7710
+ email,
7711
+ commits: commitHashes,
7712
+ login: ""
7713
+ };
7714
+ if (!resolvedLogins?.has(email)) {
7715
+ const login = await getResolvedAuthorLogin({ github, githubToken, commitHashes, email });
7716
+ resolvedAuthor.login = login;
7717
+ resolvedLogins?.set(email, login);
7718
+ } else {
7719
+ const login = resolvedLogins?.get(email) || "";
7720
+ resolvedAuthor.login = login;
7721
+ }
7722
+ resolvedAuthors.push(resolvedAuthor);
7723
+ if (!map.has(email)) {
7724
+ map.set(email, resolvedAuthor);
7725
+ }
7726
+ }
7727
+ }
7728
+ const resultCommit = { ...commit, resolvedAuthors };
7729
+ resultCommits.push(resultCommit);
7730
+ }
7731
+ return {
7732
+ commits: resultCommits,
7733
+ contributors: Array.from(map.values())
7734
+ };
7735
+ }
7736
+
7737
+ function formatReferences(references, github, type) {
7738
+ const refs = references.filter((i) => {
7739
+ if (type === "issues")
7740
+ return i.type === "issue" || i.type === "pull-request";
7741
+ return i.type === "hash";
7742
+ }).map((ref) => {
7743
+ if (!github)
7744
+ return ref.value;
7745
+ if (ref.type === "pull-request" || ref.type === "issue")
7746
+ return `https://github.com/${github}/issues/${ref.value.slice(1)}`;
7747
+ return `[<samp>(${ref.value.slice(0, 5)})</samp>](https://github.com/${github}/commit/${ref.value})`;
7748
+ });
7749
+ const referencesString = join(refs).trim();
7750
+ if (type === "issues")
7751
+ return referencesString && `in ${referencesString}`;
7752
+ return referencesString;
7753
+ }
7754
+ function formatLine(commit, options) {
7755
+ const prRefs = formatReferences(commit.references, options.github, "issues");
7756
+ const hashRefs = formatReferences(commit.references, options.github, "hash");
7757
+ let authors = join([...new Set(commit.resolvedAuthors.map((i) => i.login ? `@${i.login}` : `**${i.name}**`))]).trim();
7758
+ if (authors) {
7759
+ authors = `by ${authors}`;
7760
+ }
7761
+ let refs = [authors, prRefs, hashRefs].filter((i) => i?.trim()).join(" ");
7762
+ if (refs) {
7763
+ refs = `&nbsp;-&nbsp; ${refs}`;
7764
+ }
7765
+ const description = options.capitalize ? capitalize(commit.description) : commit.description;
7766
+ return [description, refs].filter((i) => i?.trim()).join(" ");
7767
+ }
7768
+ function formatTitle(name, options) {
7769
+ const emojisRE = /([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g;
7770
+ let formatName = name.trim();
7771
+ if (!options.emoji) {
7772
+ formatName = name.replace(emojisRE, "").trim();
7773
+ }
7774
+ return `### &nbsp;&nbsp;&nbsp;${formatName}`;
7775
+ }
7776
+ function formatSection(commits, sectionName, options) {
7777
+ if (!commits.length)
7778
+ return [];
7779
+ const lines = ["", formatTitle(sectionName, options), ""];
7780
+ const scopes = groupBy(commits, "scope");
7781
+ let useScopeGroup = options.group;
7782
+ if (!Object.entries(scopes).some(([k, v]) => k && v.length > 1))
7783
+ useScopeGroup = false;
7784
+ Object.keys(scopes).sort().forEach((scope) => {
7785
+ let padding = "";
7786
+ let prefix = "";
7787
+ const scopeText = `**${options.scopeMap[scope] || scope}**`;
7788
+ if (scope && (useScopeGroup === true || useScopeGroup === "multiple" && scopes[scope].length > 1)) {
7789
+ lines.push(`- ${scopeText}:`);
7790
+ padding = " ";
7791
+ } else if (scope) {
7792
+ prefix = `${scopeText}: `;
7793
+ }
7794
+ lines.push(...scopes[scope].reverse().map((commit) => `${padding}- ${prefix}${formatLine(commit, options)}`));
7795
+ });
7796
+ return lines;
7797
+ }
7798
+ function getGitUserAvatar(userName) {
7799
+ const avatarUrl = `https://github.com/${userName}.png?size=48`;
7800
+ return avatarUrl;
7801
+ }
7802
+ function createContributorLine(contributors) {
7803
+ let loginLine = "";
7804
+ let unloginLine = "";
7805
+ contributors.forEach((contributor, index) => {
7806
+ const { name, email, login } = contributor;
7807
+ if (!login) {
7808
+ let line = `[${name}](mailto:${email})`;
7809
+ if (index < contributors.length - 1) {
7810
+ line += ", ";
7811
+ }
7812
+ unloginLine += line;
7813
+ } else {
7814
+ const avatar = getGitUserAvatar(login);
7815
+ loginLine += `![${login}](${avatar}) `;
7816
+ }
7817
+ });
7818
+ return `${loginLine}
7819
+ ${unloginLine}`;
7820
+ }
7821
+ function generateMarkdown(params) {
7822
+ const VERSION_REG = /v\d+\.\d+\.\d+/;
7823
+ const { commits, options, showTitle, contributors } = params;
7824
+ const lines = [];
7825
+ const url = `https://github.com/${options.github}/compare/${options.from}...${options.to}`;
7826
+ if (showTitle) {
7827
+ const version = VERSION_REG.test(options.to) ? options.to : options.newVersion;
7828
+ const date = options.tagDateMap.get(options.to);
7829
+ let title = `## [${version}](${url})`;
7830
+ if (date) {
7831
+ title += ` (${date})`;
7832
+ }
7833
+ lines.push(title);
7834
+ }
7835
+ const [breaking, changes] = partition(commits, (c) => c.isBreaking);
7836
+ const group = groupBy(changes, "type");
7837
+ lines.push(...formatSection(breaking, options.titles.breakingChanges, options));
7838
+ for (const type of Object.keys(options.types)) {
7839
+ const items = group[type] || [];
7840
+ lines.push(...formatSection(items, options.types[type].title, options));
7841
+ }
7842
+ if (!lines.length) {
7843
+ lines.push("*No significant changes*");
7844
+ }
7845
+ if (!showTitle) {
7846
+ lines.push("", `##### &nbsp;&nbsp;&nbsp;&nbsp;[View changes on GitHub](${url})`);
7847
+ }
7848
+ if (showTitle) {
7849
+ lines.push("", "### \u2764\uFE0F Contributors", "");
7850
+ const contributorLine = createContributorLine(contributors);
7851
+ lines.push(contributorLine);
7852
+ }
7853
+ const md = convertGitmoji.convert(lines.join("\n").trim(), true);
7854
+ const markdown = md.replace(/&nbsp;/g, "");
7855
+ return markdown;
7856
+ }
7857
+ async function isVersionInMarkdown(version, mdPath) {
7858
+ const VERSION_REG_OF_MARKDOWN = /## \[v\d+\.\d+\.\d+\]/g;
7859
+ let isIn = false;
7860
+ const md = await promises.readFile(mdPath, "utf8");
7861
+ if (md) {
7862
+ const matches = md.match(VERSION_REG_OF_MARKDOWN);
7863
+ if (matches?.length) {
7864
+ const versionInMarkdown = `## [${version}]`;
7865
+ isIn = matches.includes(versionInMarkdown);
7866
+ }
7867
+ }
7868
+ return isIn;
7869
+ }
7870
+ async function writeMarkdown(md, mdPath, override = false) {
7871
+ let changelogMD;
7872
+ const changelogPrefix = "# Changelog";
7873
+ if (!override && actualFS.existsSync(mdPath)) {
7874
+ changelogMD = await promises.readFile(mdPath, "utf8");
7875
+ if (!changelogMD.startsWith(changelogPrefix)) {
7876
+ changelogMD = `${changelogPrefix}
7877
+
7878
+ ${changelogMD}`;
7879
+ }
7880
+ } else {
7881
+ changelogMD = `${changelogPrefix}
7882
+
7883
+ `;
7884
+ }
7885
+ const lastEntry = changelogMD.match(/^###?\s+.*$/m);
7886
+ if (lastEntry) {
7887
+ changelogMD = `${changelogMD.slice(0, lastEntry.index) + md}
7888
+
7889
+ ${changelogMD.slice(lastEntry.index)}`;
7890
+ } else {
7891
+ changelogMD += `
7892
+ ${md}
7893
+
7894
+ `;
7895
+ }
7896
+ await promises.writeFile(mdPath, changelogMD);
7897
+ }
7898
+
7899
+ function createDefaultOptions() {
7900
+ const cwd = process.cwd();
7901
+ const options = {
7902
+ cwd,
7903
+ types: {
7904
+ feat: { title: "\u{1F680} Features" },
7905
+ fix: { title: "\u{1F41E} Bug Fixes" },
7906
+ perf: { title: "\u{1F525} Performance" },
7907
+ refactor: { title: "\u{1F485} Refactors" },
7908
+ docs: { title: "\u{1F4D6} Documentation" },
7909
+ build: { title: "\u{1F4E6} Build" },
7910
+ types: { title: "\u{1F30A} Types" },
7911
+ chore: { title: "\u{1F3E1} Chore" },
7912
+ examples: { title: "\u{1F3C0} Examples" },
7913
+ test: { title: "\u2705 Tests" },
7914
+ style: { title: "\u{1F3A8} Styles" },
7915
+ ci: { title: "\u{1F916} CI" }
7916
+ },
7917
+ scopeMap: {},
7918
+ github: "",
7919
+ githubToken: process.env.GITHUB_TOKEN || "",
7920
+ from: "",
7921
+ to: "",
7922
+ tags: [],
7923
+ tagDateMap: /* @__PURE__ */ new Map(),
7924
+ prerelease: false,
7925
+ capitalize: true,
7926
+ emoji: true,
7927
+ titles: {
7928
+ breakingChanges: "\u{1F6A8} Breaking Changes"
7929
+ },
7930
+ output: "CHANGELOG.md",
7931
+ overrideChangelog: false,
7932
+ newVersion: ""
7933
+ };
7934
+ return options;
7935
+ }
7936
+ async function getOptionsFromPkg(cwd) {
7937
+ let githubToken = "";
7938
+ let newVersion = "";
7939
+ try {
7940
+ const pkgJson = await promises.readFile(`${cwd}/package.json`, "utf-8");
7941
+ const pkg = JSON.parse(pkgJson);
7942
+ githubToken = pkg?.["github-token"] || "";
7943
+ newVersion = pkg?.version || "";
7944
+ } catch {
7945
+ }
7946
+ return {
7947
+ githubToken,
7948
+ newVersion
7949
+ };
7950
+ }
7951
+ async function initOptions() {
7952
+ const options = createDefaultOptions();
7953
+ const { githubToken, newVersion } = await getOptionsFromPkg(options.cwd);
7954
+ if (!options.githubToken) {
7955
+ options.githubToken = githubToken;
7956
+ }
7957
+ if (newVersion) {
7958
+ options.newVersion = `v${newVersion}`;
7959
+ }
7960
+ if (!options.from) {
7961
+ options.from = await getLastGitTag();
7962
+ }
7963
+ if (!options.to) {
7964
+ options.to = await getCurrentGitBranch();
7965
+ }
7966
+ if (options.to === options.from) {
7967
+ const lastTag = await getLastGitTag(-1);
7968
+ const firstCommit = await getFirstGitCommit();
7969
+ options.from = lastTag || firstCommit;
7970
+ }
7971
+ options.tags = await getTotalGitTags();
7972
+ options.tagDateMap = await getTagDateMap();
7973
+ options.github = await getGitHubRepo();
7974
+ options.prerelease = isPrerelease(options.to);
7975
+ return options;
7976
+ }
7977
+ async function generateChangelogByTag(options) {
7978
+ const gitCommits = await getGitCommits(options.from, options.to, options.scopeMap);
7979
+ const { commits, contributors } = await getGitCommitsAndResolvedAuthors(
7980
+ gitCommits,
7981
+ options.github,
7982
+ options.githubToken
7983
+ );
7984
+ const md = generateMarkdown({ commits, options, showTitle: true, contributors });
7985
+ return md;
7986
+ }
7987
+ async function generateChangelogByTags(options) {
7988
+ const tags = getFromToTags(options.tags);
7989
+ const bar = new cliProgress__default.SingleBar({}, cliProgress__default.Presets.shades_classic);
7990
+ bar.start(tags.length, 0);
7991
+ let md = "";
7992
+ const resolvedLogins = /* @__PURE__ */ new Map();
7993
+ for (let i = 0; i < tags.length; i += 1) {
7994
+ const { from, to } = tags[i];
7995
+ const gitCommits = await getGitCommits(from, to, options.scopeMap);
7996
+ const { commits, contributors } = await getGitCommitsAndResolvedAuthors(
7997
+ gitCommits,
7998
+ options.github,
7999
+ options.githubToken,
8000
+ resolvedLogins
8001
+ );
8002
+ const opts = { ...options, from, to };
8003
+ const nextMd = generateMarkdown({ commits, options: opts, showTitle: true, contributors });
8004
+ md = `${nextMd}
8005
+
8006
+ ${md}`;
8007
+ bar.update(i + 1);
8008
+ }
8009
+ bar.stop();
8010
+ return md;
8011
+ }
8012
+ async function generateChangelog(total = false) {
8013
+ const options = await initOptions();
8014
+ let md = "";
8015
+ if (total) {
8016
+ md = await generateChangelogByTags(options);
8017
+ await writeMarkdown(md, options.output, true);
8018
+ return;
8019
+ }
8020
+ const isIn = await isVersionInMarkdown(options.to, options.output);
8021
+ if (!options.overrideChangelog && isIn) {
8022
+ return;
8023
+ }
8024
+ md = await generateChangelogByTag(options);
8025
+ await writeMarkdown(md, options.output, false);
8026
+ }
8027
+
8028
+ async function genChangelog(total = false) {
8029
+ await generateChangelog(total);
8030
+ }
8031
+
8032
+ async function release() {
8033
+ await versionBumpp__default({
8034
+ files: ["**/package.json", "!**/node_modules"],
8035
+ execute: "npx soy changelog",
8036
+ all: true,
8037
+ tag: true,
8038
+ commit: "chore(projects): release v%s",
8039
+ push: true
8040
+ });
7475
8041
  }
7476
8042
 
7477
8043
  const cli = cac__default("soybean");
7478
- cli.version(version).help();
8044
+ cli.version(version).option("--total", "Generate changelog by total tags").help();
7479
8045
  const commands = {
7480
8046
  "git-commit": {
7481
8047
  desc: "\u751F\u6210\u7B26\u5408 Angular \u89C4\u8303\u7684 git commit",
@@ -7508,6 +8074,16 @@ const commands = {
7508
8074
  "lint-staged": {
7509
8075
  desc: "\u6267\u884Clint-staged",
7510
8076
  action: lintStaged
8077
+ },
8078
+ changelog: {
8079
+ desc: "\u751F\u6210changelog",
8080
+ action: async (args) => {
8081
+ await genChangelog(args?.total);
8082
+ }
8083
+ },
8084
+ release: {
8085
+ desc: "\u53D1\u5E03\uFF1A\u66F4\u65B0\u7248\u672C\u53F7\u3001\u751F\u6210changelog\u3001\u63D0\u4EA4\u4EE3\u7801",
8086
+ action: release
7511
8087
  }
7512
8088
  };
7513
8089
  Object.entries(commands).forEach(([command, { desc, action }]) => {
package/dist/index.mjs CHANGED
@@ -1,19 +1,67 @@
1
1
  #!/usr/bin/env node
2
2
  import cac from 'cac';
3
3
  import enquirer from 'enquirer';
4
- import { execa } from 'execa';
5
4
  import * as actualFS from 'fs';
6
5
  import { readFileSync, realpathSync as realpathSync$1, lstatSync, readdir, readdirSync, readlinkSync, existsSync } from 'fs';
7
6
  import { bgRed, red, green } from 'kolorist';
8
7
  import { rimraf } from 'rimraf';
9
8
  import { win32, posix } from 'path';
10
9
  import { fileURLToPath } from 'url';
11
- import { lstat, readdir as readdir$1, readlink, realpath } from 'fs/promises';
10
+ import { lstat, readdir as readdir$1, readlink, realpath, readFile, writeFile } from 'fs/promises';
12
11
  import EE from 'events';
13
12
  import Stream from 'stream';
14
13
  import stringdecoder from 'string_decoder';
15
-
16
- const version = "0.4.1";
14
+ import cliProgress from 'cli-progress';
15
+ import { ofetch } from 'ofetch';
16
+ import dayjs from 'dayjs';
17
+ import { convert } from 'convert-gitmoji';
18
+ import versionBumpp from 'bumpp';
19
+
20
+ const version = "0.5.0";
21
+
22
+ async function execCommand(cmd, args, options) {
23
+ const { execa } = await import('execa');
24
+ const res = await execa(cmd, args, options);
25
+ return res?.stdout?.trim() || "";
26
+ }
27
+ function notNullish(v) {
28
+ return v !== null && v !== void 0;
29
+ }
30
+ function partition(array, ...filters) {
31
+ const result = new Array(filters.length + 1).fill(null).map(() => []);
32
+ array.forEach((e, idx, arr) => {
33
+ let i = 0;
34
+ for (const filter of filters) {
35
+ if (filter(e, idx, arr)) {
36
+ result[i].push(e);
37
+ return;
38
+ }
39
+ i += 1;
40
+ }
41
+ result[i].push(e);
42
+ });
43
+ return result;
44
+ }
45
+ function groupBy(items, key, groups = {}) {
46
+ for (const item of items) {
47
+ const v = item[key];
48
+ groups[v] = groups[v] || [];
49
+ groups[v].push(item);
50
+ }
51
+ return groups;
52
+ }
53
+ function capitalize(str) {
54
+ return str.charAt(0).toUpperCase() + str.slice(1);
55
+ }
56
+ function join(array, glue = ", ", finalGlue = " and ") {
57
+ if (!array || array.length === 0)
58
+ return "";
59
+ if (array.length === 1)
60
+ return array[0];
61
+ if (array.length === 2)
62
+ return array.join(finalGlue);
63
+ return `${array.slice(0, -1).join(glue)}${finalGlue}${array.slice(-1)}`;
64
+ }
17
65
 
18
66
  const gitCommitTypes = [
19
67
  { value: "init", title: "init: \u9879\u76EE\u521D\u59CB\u5316" },
@@ -68,7 +116,7 @@ async function gitCommit() {
68
116
  }
69
117
  ]);
70
118
  const commitMsg = `${result.types}(${result.scopes}): ${result.description}`;
71
- execa("git", ["commit", "-m", commitMsg], { stdio: "inherit" });
119
+ execCommand("git", ["commit", "-m", commitMsg], { stdio: "inherit" });
72
120
  }
73
121
 
74
122
  function gitCommitVerify() {
@@ -7407,14 +7455,14 @@ async function initSimpleGitHooks() {
7407
7455
  const existHusky = existsSync(huskyDir);
7408
7456
  if (existHusky) {
7409
7457
  await rimraf(".husky");
7410
- await execa("git", ["config", "core.hooksPath", ".git/hooks/"], { stdio: "inherit" });
7458
+ await execCommand("git", ["config", "core.hooksPath", ".git/hooks/"], { stdio: "inherit" });
7411
7459
  }
7412
7460
  await rimraf(".git/hooks");
7413
- await execa("npx", ["simple-git-hooks"], { stdio: "inherit" });
7461
+ await execCommand("npx", ["simple-git-hooks"], { stdio: "inherit" });
7414
7462
  }
7415
7463
 
7416
7464
  async function updatePkg() {
7417
- execa("npx", ["ncu", "--deep", "-u"], { stdio: "inherit" });
7465
+ execCommand("npx", ["ncu", "--deep", "-u"], { stdio: "inherit" });
7418
7466
  }
7419
7467
 
7420
7468
  function prettierFormat() {
@@ -7434,26 +7482,541 @@ function prettierFormat() {
7434
7482
  "!.github",
7435
7483
  "!__snapshots__"
7436
7484
  ];
7437
- execa("npx", ["prettier", ".", "--write", ...formatFiles], {
7485
+ execCommand("npx", ["prettier", ".", "--write", ...formatFiles], {
7438
7486
  stdio: "inherit"
7439
7487
  });
7440
7488
  }
7441
7489
 
7442
7490
  async function eslintPretter() {
7443
- await execa("npx", ["eslint", ".", "--fix"], {
7491
+ await execCommand("npx", ["eslint", ".", "--fix"], {
7444
7492
  stdio: "inherit"
7445
7493
  });
7446
- await execa("npx", ["soy", "prettier-format"], {
7494
+ await execCommand("npx", ["soy", "prettier-format"], {
7447
7495
  stdio: "inherit"
7448
7496
  });
7449
7497
  }
7450
7498
 
7451
7499
  function lintStaged() {
7452
- execa("npx", ["lint-staged", "--config", "@soybeanjs/cli/lint-staged"], { stdio: "inherit" });
7500
+ execCommand("npx", ["lint-staged", "--config", "@soybeanjs/cli/lint-staged"], { stdio: "inherit" });
7501
+ }
7502
+
7503
+ async function getTotalGitTags() {
7504
+ const tagStr = await execCommand("git", ["--no-pager", "tag", "-l", "--sort=creatordate"]);
7505
+ const tags = tagStr.split("\n");
7506
+ return tags;
7507
+ }
7508
+ async function getTagDateMap() {
7509
+ const tagDateStr = await execCommand("git", [
7510
+ "--no-pager",
7511
+ "log",
7512
+ "--tags",
7513
+ "--simplify-by-decoration",
7514
+ "--pretty=format:%ci %d"
7515
+ ]);
7516
+ const TAG_MARK = "(tag: ";
7517
+ const map = /* @__PURE__ */ new Map();
7518
+ const dates = tagDateStr.split("\n").filter((item) => item.includes(TAG_MARK));
7519
+ dates.forEach((item) => {
7520
+ const [dateStr, tagStr] = item.split(TAG_MARK);
7521
+ const date = dayjs(dateStr.trim()).format("YYYY-MM-DD");
7522
+ const VERSION_REG = /v\d+\.\d+\.\d+/;
7523
+ const tag = tagStr.match(VERSION_REG)?.[0];
7524
+ if (tag && date) {
7525
+ map.set(tag.trim(), date);
7526
+ }
7527
+ });
7528
+ return map;
7529
+ }
7530
+ function getFromToTags(tags) {
7531
+ const result = [];
7532
+ tags.forEach((tag, index) => {
7533
+ if (index < tags.length - 1) {
7534
+ result.push({ from: tag, to: tags[index + 1] });
7535
+ }
7536
+ });
7537
+ return result;
7538
+ }
7539
+ async function getLastGitTag(delta = 0) {
7540
+ const tags = await getTotalGitTags();
7541
+ return tags[tags.length + delta - 1];
7542
+ }
7543
+ async function getGitMainBranchName() {
7544
+ const main = await execCommand("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
7545
+ return main;
7546
+ }
7547
+ async function getCurrentGitBranch() {
7548
+ const tag = await execCommand("git", ["tag", "--points-at", "HEAD"]);
7549
+ const main = getGitMainBranchName();
7550
+ return tag || main;
7551
+ }
7552
+ async function getGitHubRepo() {
7553
+ const url = await execCommand("git", ["config", "--get", "remote.origin.url"]);
7554
+ const match = url.match(/github\.com[/:]([\w\d._-]+?)\/([\w\d._-]+?)(\.git)?$/i);
7555
+ if (!match) {
7556
+ throw new Error(`Can not parse GitHub repo from url ${url}`);
7557
+ }
7558
+ return `${match[1]}/${match[2]}`;
7559
+ }
7560
+ function isPrerelease(version) {
7561
+ const REG = /^[^.]*[\d.]+$/;
7562
+ return !REG.test(version);
7563
+ }
7564
+ function getFirstGitCommit() {
7565
+ return execCommand("git", ["rev-list", "--max-parents=0", "HEAD"]);
7566
+ }
7567
+ async function getGitDiff(from, to = "HEAD") {
7568
+ const rawGit = await execCommand("git", [
7569
+ "--no-pager",
7570
+ "log",
7571
+ `${from ? `${from}...` : ""}${to}`,
7572
+ '--pretty="----%n%s|%h|%an|%ae%n%b"',
7573
+ "--name-status"
7574
+ ]);
7575
+ const rwaGitLines = rawGit.split("----\n").splice(1);
7576
+ const gitCommits = rwaGitLines.map((line) => {
7577
+ const [firstLine, ...body] = line.split("\n");
7578
+ const [message, shortHash, authorName, authorEmail] = firstLine.split("|");
7579
+ const gitCommmit = {
7580
+ message,
7581
+ shortHash,
7582
+ author: { name: authorName, email: authorEmail },
7583
+ body: body.join("\n")
7584
+ };
7585
+ return gitCommmit;
7586
+ });
7587
+ return gitCommits;
7588
+ }
7589
+ function parseGitCommit(commit, scopeMap) {
7590
+ const ConventionalCommitRegex = /(?<type>[a-z]+)(\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
7591
+ const CoAuthoredByRegex = /co-authored-by:\s*(?<name>.+)(<(?<email>.+)>)/gim;
7592
+ const PullRequestRE = /\([a-z]*(#\d+)\s*\)/gm;
7593
+ const IssueRE = /(#\d+)/gm;
7594
+ const match = commit.message.match(ConventionalCommitRegex);
7595
+ if (!match?.groups) {
7596
+ return null;
7597
+ }
7598
+ const type = match.groups.type;
7599
+ let scope = match.groups.scope || "";
7600
+ scope = scopeMap[scope] || scope;
7601
+ const isBreaking = Boolean(match.groups.breaking);
7602
+ let description = match.groups.description;
7603
+ const references = [];
7604
+ for (const m of description.matchAll(PullRequestRE)) {
7605
+ references.push({ type: "pull-request", value: m[1] });
7606
+ }
7607
+ for (const m of description.matchAll(IssueRE)) {
7608
+ if (!references.some((i) => i.value === m[1])) {
7609
+ references.push({ type: "issue", value: m[1] });
7610
+ }
7611
+ }
7612
+ references.push({ value: commit.shortHash, type: "hash" });
7613
+ description = description.replace(PullRequestRE, "").trim();
7614
+ const authors = [commit.author];
7615
+ const matchs = commit.body.matchAll(CoAuthoredByRegex);
7616
+ for (const $match of matchs) {
7617
+ const { name = "", email = "" } = $match.groups || {};
7618
+ const author = {
7619
+ name: name.trim(),
7620
+ email: email.trim()
7621
+ };
7622
+ authors.push(author);
7623
+ }
7624
+ return {
7625
+ ...commit,
7626
+ authors,
7627
+ resolvedAuthors: [],
7628
+ description,
7629
+ type,
7630
+ scope,
7631
+ references,
7632
+ isBreaking
7633
+ };
7634
+ }
7635
+ async function getGitCommits(from, to = "HEAD", scopeMap = {}) {
7636
+ const rwaGitCommits = await getGitDiff(from, to);
7637
+ const commits = rwaGitCommits.map((commit) => parseGitCommit(commit, scopeMap)).filter(notNullish);
7638
+ return commits;
7639
+ }
7640
+ function getHeaders(githubToken) {
7641
+ return {
7642
+ accept: "application/vnd.github.v3+json",
7643
+ authorization: `token ${githubToken}`
7644
+ };
7645
+ }
7646
+ async function getResolvedAuthorLogin(params) {
7647
+ const { github, githubToken, commitHashes, email } = params;
7648
+ let login = "";
7649
+ if (!githubToken) {
7650
+ return login;
7651
+ }
7652
+ try {
7653
+ const data = await ofetch(`https://api.github.com/search/users?q=${encodeURIComponent(email)}`);
7654
+ login = data.items[0].login;
7655
+ } catch {
7656
+ }
7657
+ if (login) {
7658
+ return login;
7659
+ }
7660
+ if (commitHashes.length) {
7661
+ try {
7662
+ const data = await ofetch(`https://api.github.com/repos/${github}/commits/${commitHashes[0]}`, {
7663
+ headers: getHeaders(githubToken)
7664
+ });
7665
+ login = data?.author?.login || "";
7666
+ } catch (e) {
7667
+ }
7668
+ }
7669
+ return login;
7670
+ }
7671
+ async function getGitCommitsAndResolvedAuthors(commits, github, githubToken, resolvedLogins) {
7672
+ const resultCommits = [];
7673
+ const map = /* @__PURE__ */ new Map();
7674
+ for (const commit of commits) {
7675
+ const resolvedAuthors = [];
7676
+ for (const [index, author] of Object.entries(commit.authors)) {
7677
+ const { email, name } = author;
7678
+ if (email && name) {
7679
+ const commitHashes = [];
7680
+ if (index === "0") {
7681
+ commitHashes.push(commit.shortHash);
7682
+ }
7683
+ const resolvedAuthor = {
7684
+ name,
7685
+ email,
7686
+ commits: commitHashes,
7687
+ login: ""
7688
+ };
7689
+ if (!resolvedLogins?.has(email)) {
7690
+ const login = await getResolvedAuthorLogin({ github, githubToken, commitHashes, email });
7691
+ resolvedAuthor.login = login;
7692
+ resolvedLogins?.set(email, login);
7693
+ } else {
7694
+ const login = resolvedLogins?.get(email) || "";
7695
+ resolvedAuthor.login = login;
7696
+ }
7697
+ resolvedAuthors.push(resolvedAuthor);
7698
+ if (!map.has(email)) {
7699
+ map.set(email, resolvedAuthor);
7700
+ }
7701
+ }
7702
+ }
7703
+ const resultCommit = { ...commit, resolvedAuthors };
7704
+ resultCommits.push(resultCommit);
7705
+ }
7706
+ return {
7707
+ commits: resultCommits,
7708
+ contributors: Array.from(map.values())
7709
+ };
7710
+ }
7711
+
7712
+ function formatReferences(references, github, type) {
7713
+ const refs = references.filter((i) => {
7714
+ if (type === "issues")
7715
+ return i.type === "issue" || i.type === "pull-request";
7716
+ return i.type === "hash";
7717
+ }).map((ref) => {
7718
+ if (!github)
7719
+ return ref.value;
7720
+ if (ref.type === "pull-request" || ref.type === "issue")
7721
+ return `https://github.com/${github}/issues/${ref.value.slice(1)}`;
7722
+ return `[<samp>(${ref.value.slice(0, 5)})</samp>](https://github.com/${github}/commit/${ref.value})`;
7723
+ });
7724
+ const referencesString = join(refs).trim();
7725
+ if (type === "issues")
7726
+ return referencesString && `in ${referencesString}`;
7727
+ return referencesString;
7728
+ }
7729
+ function formatLine(commit, options) {
7730
+ const prRefs = formatReferences(commit.references, options.github, "issues");
7731
+ const hashRefs = formatReferences(commit.references, options.github, "hash");
7732
+ let authors = join([...new Set(commit.resolvedAuthors.map((i) => i.login ? `@${i.login}` : `**${i.name}**`))]).trim();
7733
+ if (authors) {
7734
+ authors = `by ${authors}`;
7735
+ }
7736
+ let refs = [authors, prRefs, hashRefs].filter((i) => i?.trim()).join(" ");
7737
+ if (refs) {
7738
+ refs = `&nbsp;-&nbsp; ${refs}`;
7739
+ }
7740
+ const description = options.capitalize ? capitalize(commit.description) : commit.description;
7741
+ return [description, refs].filter((i) => i?.trim()).join(" ");
7742
+ }
7743
+ function formatTitle(name, options) {
7744
+ const emojisRE = /([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g;
7745
+ let formatName = name.trim();
7746
+ if (!options.emoji) {
7747
+ formatName = name.replace(emojisRE, "").trim();
7748
+ }
7749
+ return `### &nbsp;&nbsp;&nbsp;${formatName}`;
7750
+ }
7751
+ function formatSection(commits, sectionName, options) {
7752
+ if (!commits.length)
7753
+ return [];
7754
+ const lines = ["", formatTitle(sectionName, options), ""];
7755
+ const scopes = groupBy(commits, "scope");
7756
+ let useScopeGroup = options.group;
7757
+ if (!Object.entries(scopes).some(([k, v]) => k && v.length > 1))
7758
+ useScopeGroup = false;
7759
+ Object.keys(scopes).sort().forEach((scope) => {
7760
+ let padding = "";
7761
+ let prefix = "";
7762
+ const scopeText = `**${options.scopeMap[scope] || scope}**`;
7763
+ if (scope && (useScopeGroup === true || useScopeGroup === "multiple" && scopes[scope].length > 1)) {
7764
+ lines.push(`- ${scopeText}:`);
7765
+ padding = " ";
7766
+ } else if (scope) {
7767
+ prefix = `${scopeText}: `;
7768
+ }
7769
+ lines.push(...scopes[scope].reverse().map((commit) => `${padding}- ${prefix}${formatLine(commit, options)}`));
7770
+ });
7771
+ return lines;
7772
+ }
7773
+ function getGitUserAvatar(userName) {
7774
+ const avatarUrl = `https://github.com/${userName}.png?size=48`;
7775
+ return avatarUrl;
7776
+ }
7777
+ function createContributorLine(contributors) {
7778
+ let loginLine = "";
7779
+ let unloginLine = "";
7780
+ contributors.forEach((contributor, index) => {
7781
+ const { name, email, login } = contributor;
7782
+ if (!login) {
7783
+ let line = `[${name}](mailto:${email})`;
7784
+ if (index < contributors.length - 1) {
7785
+ line += ", ";
7786
+ }
7787
+ unloginLine += line;
7788
+ } else {
7789
+ const avatar = getGitUserAvatar(login);
7790
+ loginLine += `![${login}](${avatar}) `;
7791
+ }
7792
+ });
7793
+ return `${loginLine}
7794
+ ${unloginLine}`;
7795
+ }
7796
+ function generateMarkdown(params) {
7797
+ const VERSION_REG = /v\d+\.\d+\.\d+/;
7798
+ const { commits, options, showTitle, contributors } = params;
7799
+ const lines = [];
7800
+ const url = `https://github.com/${options.github}/compare/${options.from}...${options.to}`;
7801
+ if (showTitle) {
7802
+ const version = VERSION_REG.test(options.to) ? options.to : options.newVersion;
7803
+ const date = options.tagDateMap.get(options.to);
7804
+ let title = `## [${version}](${url})`;
7805
+ if (date) {
7806
+ title += ` (${date})`;
7807
+ }
7808
+ lines.push(title);
7809
+ }
7810
+ const [breaking, changes] = partition(commits, (c) => c.isBreaking);
7811
+ const group = groupBy(changes, "type");
7812
+ lines.push(...formatSection(breaking, options.titles.breakingChanges, options));
7813
+ for (const type of Object.keys(options.types)) {
7814
+ const items = group[type] || [];
7815
+ lines.push(...formatSection(items, options.types[type].title, options));
7816
+ }
7817
+ if (!lines.length) {
7818
+ lines.push("*No significant changes*");
7819
+ }
7820
+ if (!showTitle) {
7821
+ lines.push("", `##### &nbsp;&nbsp;&nbsp;&nbsp;[View changes on GitHub](${url})`);
7822
+ }
7823
+ if (showTitle) {
7824
+ lines.push("", "### \u2764\uFE0F Contributors", "");
7825
+ const contributorLine = createContributorLine(contributors);
7826
+ lines.push(contributorLine);
7827
+ }
7828
+ const md = convert(lines.join("\n").trim(), true);
7829
+ const markdown = md.replace(/&nbsp;/g, "");
7830
+ return markdown;
7831
+ }
7832
+ async function isVersionInMarkdown(version, mdPath) {
7833
+ const VERSION_REG_OF_MARKDOWN = /## \[v\d+\.\d+\.\d+\]/g;
7834
+ let isIn = false;
7835
+ const md = await readFile(mdPath, "utf8");
7836
+ if (md) {
7837
+ const matches = md.match(VERSION_REG_OF_MARKDOWN);
7838
+ if (matches?.length) {
7839
+ const versionInMarkdown = `## [${version}]`;
7840
+ isIn = matches.includes(versionInMarkdown);
7841
+ }
7842
+ }
7843
+ return isIn;
7844
+ }
7845
+ async function writeMarkdown(md, mdPath, override = false) {
7846
+ let changelogMD;
7847
+ const changelogPrefix = "# Changelog";
7848
+ if (!override && existsSync(mdPath)) {
7849
+ changelogMD = await readFile(mdPath, "utf8");
7850
+ if (!changelogMD.startsWith(changelogPrefix)) {
7851
+ changelogMD = `${changelogPrefix}
7852
+
7853
+ ${changelogMD}`;
7854
+ }
7855
+ } else {
7856
+ changelogMD = `${changelogPrefix}
7857
+
7858
+ `;
7859
+ }
7860
+ const lastEntry = changelogMD.match(/^###?\s+.*$/m);
7861
+ if (lastEntry) {
7862
+ changelogMD = `${changelogMD.slice(0, lastEntry.index) + md}
7863
+
7864
+ ${changelogMD.slice(lastEntry.index)}`;
7865
+ } else {
7866
+ changelogMD += `
7867
+ ${md}
7868
+
7869
+ `;
7870
+ }
7871
+ await writeFile(mdPath, changelogMD);
7872
+ }
7873
+
7874
+ function createDefaultOptions() {
7875
+ const cwd = process.cwd();
7876
+ const options = {
7877
+ cwd,
7878
+ types: {
7879
+ feat: { title: "\u{1F680} Features" },
7880
+ fix: { title: "\u{1F41E} Bug Fixes" },
7881
+ perf: { title: "\u{1F525} Performance" },
7882
+ refactor: { title: "\u{1F485} Refactors" },
7883
+ docs: { title: "\u{1F4D6} Documentation" },
7884
+ build: { title: "\u{1F4E6} Build" },
7885
+ types: { title: "\u{1F30A} Types" },
7886
+ chore: { title: "\u{1F3E1} Chore" },
7887
+ examples: { title: "\u{1F3C0} Examples" },
7888
+ test: { title: "\u2705 Tests" },
7889
+ style: { title: "\u{1F3A8} Styles" },
7890
+ ci: { title: "\u{1F916} CI" }
7891
+ },
7892
+ scopeMap: {},
7893
+ github: "",
7894
+ githubToken: process.env.GITHUB_TOKEN || "",
7895
+ from: "",
7896
+ to: "",
7897
+ tags: [],
7898
+ tagDateMap: /* @__PURE__ */ new Map(),
7899
+ prerelease: false,
7900
+ capitalize: true,
7901
+ emoji: true,
7902
+ titles: {
7903
+ breakingChanges: "\u{1F6A8} Breaking Changes"
7904
+ },
7905
+ output: "CHANGELOG.md",
7906
+ overrideChangelog: false,
7907
+ newVersion: ""
7908
+ };
7909
+ return options;
7910
+ }
7911
+ async function getOptionsFromPkg(cwd) {
7912
+ let githubToken = "";
7913
+ let newVersion = "";
7914
+ try {
7915
+ const pkgJson = await readFile(`${cwd}/package.json`, "utf-8");
7916
+ const pkg = JSON.parse(pkgJson);
7917
+ githubToken = pkg?.["github-token"] || "";
7918
+ newVersion = pkg?.version || "";
7919
+ } catch {
7920
+ }
7921
+ return {
7922
+ githubToken,
7923
+ newVersion
7924
+ };
7925
+ }
7926
+ async function initOptions() {
7927
+ const options = createDefaultOptions();
7928
+ const { githubToken, newVersion } = await getOptionsFromPkg(options.cwd);
7929
+ if (!options.githubToken) {
7930
+ options.githubToken = githubToken;
7931
+ }
7932
+ if (newVersion) {
7933
+ options.newVersion = `v${newVersion}`;
7934
+ }
7935
+ if (!options.from) {
7936
+ options.from = await getLastGitTag();
7937
+ }
7938
+ if (!options.to) {
7939
+ options.to = await getCurrentGitBranch();
7940
+ }
7941
+ if (options.to === options.from) {
7942
+ const lastTag = await getLastGitTag(-1);
7943
+ const firstCommit = await getFirstGitCommit();
7944
+ options.from = lastTag || firstCommit;
7945
+ }
7946
+ options.tags = await getTotalGitTags();
7947
+ options.tagDateMap = await getTagDateMap();
7948
+ options.github = await getGitHubRepo();
7949
+ options.prerelease = isPrerelease(options.to);
7950
+ return options;
7951
+ }
7952
+ async function generateChangelogByTag(options) {
7953
+ const gitCommits = await getGitCommits(options.from, options.to, options.scopeMap);
7954
+ const { commits, contributors } = await getGitCommitsAndResolvedAuthors(
7955
+ gitCommits,
7956
+ options.github,
7957
+ options.githubToken
7958
+ );
7959
+ const md = generateMarkdown({ commits, options, showTitle: true, contributors });
7960
+ return md;
7961
+ }
7962
+ async function generateChangelogByTags(options) {
7963
+ const tags = getFromToTags(options.tags);
7964
+ const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
7965
+ bar.start(tags.length, 0);
7966
+ let md = "";
7967
+ const resolvedLogins = /* @__PURE__ */ new Map();
7968
+ for (let i = 0; i < tags.length; i += 1) {
7969
+ const { from, to } = tags[i];
7970
+ const gitCommits = await getGitCommits(from, to, options.scopeMap);
7971
+ const { commits, contributors } = await getGitCommitsAndResolvedAuthors(
7972
+ gitCommits,
7973
+ options.github,
7974
+ options.githubToken,
7975
+ resolvedLogins
7976
+ );
7977
+ const opts = { ...options, from, to };
7978
+ const nextMd = generateMarkdown({ commits, options: opts, showTitle: true, contributors });
7979
+ md = `${nextMd}
7980
+
7981
+ ${md}`;
7982
+ bar.update(i + 1);
7983
+ }
7984
+ bar.stop();
7985
+ return md;
7986
+ }
7987
+ async function generateChangelog(total = false) {
7988
+ const options = await initOptions();
7989
+ let md = "";
7990
+ if (total) {
7991
+ md = await generateChangelogByTags(options);
7992
+ await writeMarkdown(md, options.output, true);
7993
+ return;
7994
+ }
7995
+ const isIn = await isVersionInMarkdown(options.to, options.output);
7996
+ if (!options.overrideChangelog && isIn) {
7997
+ return;
7998
+ }
7999
+ md = await generateChangelogByTag(options);
8000
+ await writeMarkdown(md, options.output, false);
8001
+ }
8002
+
8003
+ async function genChangelog(total = false) {
8004
+ await generateChangelog(total);
8005
+ }
8006
+
8007
+ async function release() {
8008
+ await versionBumpp({
8009
+ files: ["**/package.json", "!**/node_modules"],
8010
+ execute: "npx soy changelog",
8011
+ all: true,
8012
+ tag: true,
8013
+ commit: "chore(projects): release v%s",
8014
+ push: true
8015
+ });
7453
8016
  }
7454
8017
 
7455
8018
  const cli = cac("soybean");
7456
- cli.version(version).help();
8019
+ cli.version(version).option("--total", "Generate changelog by total tags").help();
7457
8020
  const commands = {
7458
8021
  "git-commit": {
7459
8022
  desc: "\u751F\u6210\u7B26\u5408 Angular \u89C4\u8303\u7684 git commit",
@@ -7486,6 +8049,16 @@ const commands = {
7486
8049
  "lint-staged": {
7487
8050
  desc: "\u6267\u884Clint-staged",
7488
8051
  action: lintStaged
8052
+ },
8053
+ changelog: {
8054
+ desc: "\u751F\u6210changelog",
8055
+ action: async (args) => {
8056
+ await genChangelog(args?.total);
8057
+ }
8058
+ },
8059
+ release: {
8060
+ desc: "\u53D1\u5E03\uFF1A\u66F4\u65B0\u7248\u672C\u53F7\u3001\u751F\u6210changelog\u3001\u63D0\u4EA4\u4EE3\u7801",
8061
+ action: release
7489
8062
  }
7490
8063
  };
7491
8064
  Object.entries(commands).forEach(([command, { desc, action }]) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soybeanjs/cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "SoybeanJS's command line tools",
5
5
  "author": {
6
6
  "name": "Soybean",
@@ -25,26 +25,31 @@
25
25
  }
26
26
  },
27
27
  "bin": {
28
- "soybean": "dist/index.mjs",
29
- "soy": "dist/index.mjs"
28
+ "soybean": "dist/index.cjs",
29
+ "soy": "dist/index.cjs"
30
30
  },
31
31
  "files": [
32
32
  "dist"
33
33
  ],
34
34
  "dependencies": {
35
- "cac": "^6.7.14",
35
+ "bumpp": "9.1.0",
36
+ "cac": "6.7.14",
37
+ "cli-progress": "^3.12.0",
38
+ "convert-gitmoji": "0.1.3",
39
+ "dayjs": "1.11.8",
36
40
  "enquirer": "2.3.6",
37
41
  "execa": "7.1.1",
38
42
  "kolorist": "1.8.0",
39
43
  "lint-staged": "13.2.2",
40
44
  "minimist": "1.2.8",
41
45
  "npm-check-updates": "16.10.12",
46
+ "ofetch": "1.0.1",
42
47
  "rimraf": "5.0.1"
43
48
  },
44
49
  "devDependencies": {
45
50
  "@soybeanjs/cli": "link:",
51
+ "@types/cli-progress": "^3.11.0",
46
52
  "@types/node": "20.2.5",
47
- "bumpp": "9.1.0",
48
53
  "eslint": "8.41.0",
49
54
  "eslint-config-soybeanjs": "0.4.7",
50
55
  "simple-git-hooks": "2.8.1",
@@ -56,6 +61,7 @@
56
61
  "commit-msg": "pnpm soy git-commit-verify",
57
62
  "pre-commit": "pnpm typecheck && pnpm soy lint-staged"
58
63
  },
64
+ "github-token": "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",
59
65
  "scripts": {
60
66
  "build": "pnpm typecheck && unbuild",
61
67
  "lint": "eslint . --fix",
@@ -63,9 +69,8 @@
63
69
  "commit": "soy git-commit",
64
70
  "cleanup": "soy cleanup",
65
71
  "update-pkg": "soy update-pkg",
66
- "update-version": "bumpp --commit --push --tag",
67
72
  "publish-pkg": "pnpm -r publish --access public",
68
73
  "typecheck": "tsc --noEmit",
69
- "release": "pnpm update-version && pnpm build && pnpm publish-pkg"
74
+ "release": "soy release && pnpm build && pnpm publish-pkg"
70
75
  }
71
76
  }