@soybeanjs/cli 0.4.0 → 0.4.2

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