@storm-software/git-tools
Version:
Tools for managing Git repositories within a Nx workspace.
1,484 lines (1,461 loc) • 151 kB
JavaScript
#!/usr/bin/env node
import { run } from './chunk-54Q5U5WW.js';
import { getConfig, handleProcess, writeSuccess, exitWithSuccess, exitWithError, writeInfo, findWorkspaceRootSafe, writeFatal, joinPaths, writeDebug, defu, isVerbose, writeWarning, getWorkspaceConfig, writeTrace, __require, STORM_DEFAULT_RELEASE_BANNER } from './chunk-DKDPGA3P.js';
import TOML from '@ltd/j-toml';
import { Command, Option } from 'commander';
import { select, confirm, input } from '@inquirer/prompts';
import shellescape from 'any-shell-escape';
import chalkTemplate from 'chalk-template';
import fs, { readFile as readFile$1, writeFile } from 'node:fs/promises';
import { existsSync } from 'fs';
import { readFile } from 'fs/promises';
import childProcess, { execSync } from 'node:child_process';
import defaultRules from '@commitlint/rules';
import 'stream';
import util from 'util';
import { readCachedProjectGraph, createProjectGraphAsync as createProjectGraphAsync$1, readProjectsConfigurationFromProjectGraph as readProjectsConfigurationFromProjectGraph$1 } from 'nx/src/project-graph/project-graph';
import { existsSync as existsSync$1, rmSync, readdirSync, readFileSync, writeFileSync, statSync, promises } from 'node:fs';
import wrap from 'word-wrap';
import Path, { join as join$1, extname } from 'node:path';
import { createProjectGraphAsync, readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph.js';
import { parse, Syntax } from '@textlint/markdown-to-ast';
import anchor from 'anchor-markdown-header';
import { Parser } from 'htmlparser2';
import _ from 'underscore';
import updateSection from 'update-section';
import { readCachedProjectGraph as readCachedProjectGraph$1, createProjectGraphAsync as createProjectGraphAsync$2, output as output$1, joinPathFragments } from '@nx/devkit';
import { createAPI as createAPI$2 } from 'nx/src/command-line/release/publish.js';
import { createAPI as createAPI$1 } from 'nx/src/command-line/release/version.js';
import { readNxJson } from 'nx/src/config/nx-json.js';
import { prompt } from 'enquirer';
import { createNxReleaseConfig, handleNxReleaseConfigError } from 'nx/src/command-line/release/config/config';
import { deepMergeJson } from 'nx/src/command-line/release/config/deep-merge-json';
import { filterReleaseGroups } from 'nx/src/command-line/release/config/filter-release-groups';
import { readRawVersionPlans, setResolvedVersionPlansOnGroups } from 'nx/src/command-line/release/config/version-plans';
import { getCommitHash, parseGitCommit, getLatestGitTagForPattern, getFirstGitCommit, getGitDiff, parseCommits, gitAdd, gitPush } from 'nx/src/command-line/release/utils/git';
import { launchEditor } from 'nx/src/command-line/release/utils/launch-editor';
import { printAndFlushChanges, printDiff } from 'nx/src/command-line/release/utils/print-changes';
import { printConfigAndExit } from 'nx/src/command-line/release/utils/print-config';
import { defaultCreateReleaseProvider, GithubRemoteReleaseClient } from 'nx/src/command-line/release/utils/remote-release-clients/github';
import { resolveNxJsonConfigErrorMessage } from 'nx/src/command-line/release/utils/resolve-nx-json-error-message';
import { createCommitMessageValues, createGitTagValues, handleDuplicateGitTags, ReleaseVersion, noDiffInChangelogMessage } from 'nx/src/command-line/release/utils/shared';
import { readNxJson as readNxJson$1 } from 'nx/src/config/nx-json';
import { FsTree } from 'nx/src/generators/tree';
import { createProjectFileMapUsingProjectGraph, createFileMapUsingProjectGraph } from 'nx/src/project-graph/file-map-utils';
import { interpolate } from 'nx/src/tasks-runner/utils';
import { isCI } from 'nx/src/utils/is-ci';
import { output } from 'nx/src/utils/output';
import { joinPathFragments as joinPathFragments$1 } from 'nx/src/utils/path';
import { workspaceRoot } from 'nx/src/utils/workspace-root';
import { valid, major } from 'semver';
import { dirSync } from 'tmp';
import { resolveConfig, format } from 'prettier';
import { execCommand } from 'nx/src/command-line/release/utils/exec-command.js';
import axios from 'axios';
import DefaultChangelogRenderer from 'nx/release/changelog-renderer';
import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from 'nx/src/command-line/release/config/conventional-commits';
import { homedir } from 'node:os';
function parseCargoToml(cargoString) {
if (!cargoString) {
throw new Error("Cargo.toml is empty");
}
return TOML.parse(cargoString, {
x: { comment: true }
});
}
function stringifyCargoToml(cargoToml) {
const tomlString = TOML.stringify(cargoToml, {
newlineAround: "section"
});
if (Array.isArray(tomlString)) {
return tomlString.join("\n");
}
return tomlString;
}
// src/types.ts
var COMMIT_TYPES = {
/* --- Bumps version when selected --- */
"chore": {
"description": "Other changes that don't modify src or test files",
"title": "Chore",
"emoji": "\u2699\uFE0F ",
"semverBump": "patch",
"changelog": {
"title": "Miscellaneous",
"hidden": false
}
},
"fix": {
"description": "A change that resolves an issue previously identified with the package",
"title": "Bug Fix",
"emoji": "\u{1FAB2} ",
"semverBump": "patch",
"changelog": {
"title": "Bug Fixes",
"hidden": false
}
},
"feat": {
"description": "A change that adds a new feature to the package",
"title": "Feature",
"emoji": "\u{1F511} ",
"semverBump": "minor",
"changelog": {
"title": "Features",
"hidden": false
}
},
"ci": {
"description": "Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)",
"title": "Continuous Integration",
"emoji": "\u{1F9F0} ",
"semverBump": "patch",
"changelog": {
"title": "Continuous Integration",
"hidden": false
}
},
"refactor": {
"description": "A code change that neither fixes a bug nor adds a feature",
"title": "Code Refactoring",
"emoji": "\u{1F9EA} ",
"semverBump": "patch",
"changelog": {
"title": "Source Code Improvements",
"hidden": false
}
},
"style": {
"description": "Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)",
"title": "Style Improvements",
"emoji": "\u{1F48E} ",
"semverBump": "patch",
"changelog": {
"title": "Style Improvements",
"hidden": false
}
},
"perf": {
"description": "A code change that improves performance",
"title": "Performance Improvement",
"emoji": "\u23F1\uFE0F ",
"semverBump": "patch",
"changelog": {
"title": "Performance Improvements",
"hidden": false
}
},
/* --- Does not bump version when selected --- */
"docs": {
"description": "A change that only includes documentation updates",
"title": "Documentation",
"emoji": "\u{1F4DC} ",
"semverBump": "none",
"changelog": {
"title": "Documentation",
"hidden": false
}
},
"test": {
"description": "Adding missing tests or correcting existing tests",
"title": "Testing",
"emoji": "\u{1F6A8} ",
"semverBump": "none",
"changelog": {
"title": "Testing",
"hidden": true
}
},
/* --- Not included in commitlint but included in changelog --- */
"deps": {
"description": "Changes that add, update, or remove dependencies. This includes devDependencies and peerDependencies",
"title": "Dependencies",
"emoji": "\u{1F4E6} ",
"hidden": true,
"semverBump": "patch",
"changelog": {
"title": "Dependency Upgrades",
"hidden": false
}
},
/* --- Not included in commitlint or changelog --- */
"build": {
"description": "Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)",
"title": "Build",
"emoji": "\u{1F6E0} ",
"hidden": true,
"semverBump": "none",
"changelog": {
"title": "Build",
"hidden": true
}
},
"release": {
"description": "Publishing a commit containing a newly released version",
"title": "Publish Release",
"emoji": "\u{1F680} ",
"hidden": true,
"semverBump": "none",
"changelog": {
"title": "Publish Release",
"hidden": true
}
}
};
var DEFAULT_COMMIT_QUESTIONS = {
type: {
type: "select",
title: "Commit Type",
description: "Select the commit type that best describes your changes",
enum: Object.keys(COMMIT_TYPES).filter(
(type) => COMMIT_TYPES[type].hidden !== true
).reduce((ret, type) => {
ret[type] = COMMIT_TYPES[type];
return ret;
}, {}),
defaultValue: "chore",
maxLength: 20,
minLength: 3
},
scope: {
type: "select",
title: "Commit Scope",
description: "Select the monorepo project that is primarily impacted by this change",
enum: {},
defaultValue: "monorepo",
maxLength: 50,
minLength: 1
},
subject: {
type: "input",
title: "Commit Subject",
description: "Write a short, imperative tense description of the change",
maxLength: 150,
minLength: 3
},
body: {
type: "input",
title: "Commit Body",
description: "Provide a longer description of the change",
maxLength: 600
},
isBreaking: {
type: "confirm",
title: "Breaking Changes",
description: "Are there any breaking changes as a result of this commit?",
defaultValue: false
},
breakingBody: {
type: "input",
title: "Breaking Changes (Details)",
description: "A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself",
when: (answers) => answers.isBreaking === true,
maxLength: 600,
minLength: 3
},
isIssueAffected: {
type: "confirm",
title: "Open Issue Affected",
description: "Does this change impact any open issues?",
defaultValue: false
},
issuesBody: {
type: "input",
title: "Open Issue Affected (Details)",
description: "If issues are closed, the commit requires a body. Please enter a longer description of the commit itself",
when: (answers) => answers.isIssueAffected === true,
maxLength: 600,
minLength: 3
}
};
var DEFAULT_COMMIT_PROMPT_MESSAGES = {
skip: "press enter to skip",
max: "must be %d chars at most",
min: "must be %d chars at least",
emptyWarning: "can not be empty",
upperLimitWarning: "%s is %d characters longer than the upper limit",
lowerLimitWarning: "%s is %d characters less than the lower limit",
closedIssueMessage: "Closes: "
};
var DEFAULT_COMMIT_MESSAGE_FORMAT = "{type}({scope}): {emoji}{subject}";
var DEFAULT_COMMIT_SETTINGS = {
enableMultipleScopes: false,
disableEmoji: true,
breakingChangePrefix: "\u{1F4A3} ",
closedIssuePrefix: "\u2705 ",
format: DEFAULT_COMMIT_MESSAGE_FORMAT
};
// src/commitlint/config.ts
var DEFAULT_COMMIT_RULES = {
"body-leading-blank": [1 /* Warning */, "always"],
"body-max-length": [2 /* Error */, "always", 600],
"footer-leading-blank": [1 /* Warning */, "always"],
"footer-max-line-length": [2 /* Error */, "always", 150],
"header-max-length": [2 /* Error */, "always", 150],
"header-trim": [2 /* Error */, "always"],
"subject-case": [2 /* Error */, "always", ["sentence-case"]],
"subject-empty": [2 /* Error */, "never"],
"subject-full-stop": [2 /* Error */, "never", "."],
"subject-max-length": [2 /* Error */, "always", 150],
"subject-min-length": [2 /* Error */, "always", 3],
"type-case": [2 /* Error */, "always", "kebab-case"],
"type-empty": [2 /* Error */, "never"],
"type-enum": [
2 /* Error */,
"always",
Object.keys(COMMIT_TYPES)
],
"type-max-length": [2 /* Error */, "always", 20],
"type-min-length": [2 /* Error */, "always", 3],
"scope-case": [2 /* Error */, "always", ["kebab-case"]],
"scope-empty": [2 /* Error */, "never"]
};
var DEFAULT_COMMITLINT_CONFIG = {
rules: DEFAULT_COMMIT_RULES,
helpUrl: "https://developer.stormsoftware.com/commitlint",
parserOpts: {
headerPattern: /^(\w*)(?:\((.*)\))?!?: (.*)$/,
breakingHeaderPattern: /^(\w*)(?:\((.*)\))?!: (.*)$/,
headerCorrespondence: ["type", "scope", "subject"],
noteKeywords: ["BREAKING CHANGE", "BREAKING-CHANGE"],
revertPattern: /^(?:Revert|revert:)\s"?([\s\S]+?)"?\s*This reverts commit (\w*)\./i,
revertCorrespondence: ["header", "hash"],
issuePrefixes: ["#"]
}
};
// ../../node_modules/.pnpm/conventional-commits-parser@6.2.0/node_modules/conventional-commits-parser/dist/regex.js
var nomatchRegex = /(?!.*)/;
function join(parts, joiner) {
return parts.map((val) => val.trim()).filter(Boolean).join(joiner);
}
function getNotesRegex(noteKeywords, notesPattern) {
if (!noteKeywords) {
return nomatchRegex;
}
const noteKeywordsSelection = join(noteKeywords, "|");
if (!notesPattern) {
return new RegExp(`^[\\s|*]*(${noteKeywordsSelection})[:\\s]+(.*)`, "i");
}
return notesPattern(noteKeywordsSelection);
}
function getReferencePartsRegex(issuePrefixes, issuePrefixesCaseSensitive) {
if (!issuePrefixes) {
return nomatchRegex;
}
const flags = issuePrefixesCaseSensitive ? "g" : "gi";
return new RegExp(`(?:.*?)??\\s*([\\w-\\.\\/]*?)??(${join(issuePrefixes, "|")})([\\w-]*\\d+)`, flags);
}
function getReferencesRegex(referenceActions) {
if (!referenceActions) {
return /()(.+)/gi;
}
const joinedKeywords = join(referenceActions, "|");
return new RegExp(`(${joinedKeywords})(?:\\s+(.*?))(?=(?:${joinedKeywords})|$)`, "gi");
}
function getParserRegexes(options = {}) {
const notes = getNotesRegex(options.noteKeywords, options.notesPattern);
const referenceParts = getReferencePartsRegex(options.issuePrefixes, options.issuePrefixesCaseSensitive);
const references = getReferencesRegex(options.referenceActions);
return {
notes,
referenceParts,
references,
mentions: /@([\w-]+)/g,
url: /\b(?:https?):\/\/(?:www\.)?([-a-zA-Z0-9@:%_+.~#?&//=])+\b/
};
}
// ../../node_modules/.pnpm/conventional-commits-parser@6.2.0/node_modules/conventional-commits-parser/dist/utils.js
var SCISSOR = "------------------------ >8 ------------------------";
function trimNewLines(input2) {
const matches = input2.match(/[^\r\n]/);
if (typeof matches?.index !== "number") {
return "";
}
const firstIndex = matches.index;
let lastIndex = input2.length - 1;
while (input2[lastIndex] === "\r" || input2[lastIndex] === "\n") {
lastIndex--;
}
return input2.substring(firstIndex, lastIndex + 1);
}
function appendLine(src, line) {
return src ? `${src}
${line || ""}` : line || "";
}
function getCommentFilter(char) {
return char ? (line) => !line.startsWith(char) : () => true;
}
function truncateToScissor(lines, commentChar) {
const scissorIndex = lines.indexOf(`${commentChar} ${SCISSOR}`);
if (scissorIndex === -1) {
return lines;
}
return lines.slice(0, scissorIndex);
}
function gpgFilter(line) {
return !line.match(/^\s*gpg:/);
}
function assignMatchedCorrespondence(target, matches, correspondence) {
const { groups } = matches;
for (let i = 0, len = correspondence.length, key; i < len; i++) {
key = correspondence[i];
target[key] = (groups ? groups[key] : matches[i + 1]) || null;
}
return target;
}
// ../../node_modules/.pnpm/conventional-commits-parser@6.2.0/node_modules/conventional-commits-parser/dist/options.js
var defaultOptions = {
noteKeywords: ["BREAKING CHANGE", "BREAKING-CHANGE"],
issuePrefixes: ["#"],
referenceActions: [
"close",
"closes",
"closed",
"fix",
"fixes",
"fixed",
"resolve",
"resolves",
"resolved"
],
headerPattern: /^(\w*)(?:\(([\w$@.\-*/ ]*)\))?: (.*)$/,
headerCorrespondence: [
"type",
"scope",
"subject"
],
revertPattern: /^Revert\s"([\s\S]*)"\s*This reverts commit (\w*)\./,
revertCorrespondence: ["header", "hash"],
fieldPattern: /^-(.*?)-$/
};
// ../../node_modules/.pnpm/conventional-commits-parser@6.2.0/node_modules/conventional-commits-parser/dist/CommitParser.js
function createCommitObject(initialData = {}) {
return {
merge: null,
revert: null,
header: null,
body: null,
footer: null,
notes: [],
mentions: [],
references: [],
...initialData
};
}
var CommitParser = class {
options;
regexes;
lines = [];
lineIndex = 0;
commit = createCommitObject();
constructor(options = {}) {
this.options = {
...defaultOptions,
...options
};
this.regexes = getParserRegexes(this.options);
}
currentLine() {
return this.lines[this.lineIndex];
}
nextLine() {
return this.lines[this.lineIndex++];
}
isLineAvailable() {
return this.lineIndex < this.lines.length;
}
parseReference(input2, action) {
const { regexes } = this;
if (regexes.url.test(input2)) {
return null;
}
const matches = regexes.referenceParts.exec(input2);
if (!matches) {
return null;
}
let [raw, repository = null, prefix, issue] = matches;
let owner = null;
if (repository) {
const slashIndex = repository.indexOf("/");
if (slashIndex !== -1) {
owner = repository.slice(0, slashIndex);
repository = repository.slice(slashIndex + 1);
}
}
return {
raw,
action,
owner,
repository,
prefix,
issue
};
}
parseReferences(input2) {
const { regexes } = this;
const regex = input2.match(regexes.references) ? regexes.references : /()(.+)/gi;
const references = [];
let matches;
let action;
let sentence;
let reference;
while (true) {
matches = regex.exec(input2);
if (!matches) {
break;
}
action = matches[1] || null;
sentence = matches[2] || "";
while (true) {
reference = this.parseReference(sentence, action);
if (!reference) {
break;
}
references.push(reference);
}
}
return references;
}
skipEmptyLines() {
let line = this.currentLine();
while (line !== void 0 && !line.trim()) {
this.nextLine();
line = this.currentLine();
}
}
parseMerge() {
const { commit, options } = this;
const correspondence = options.mergeCorrespondence || [];
const merge = this.currentLine();
const matches = merge && options.mergePattern ? merge.match(options.mergePattern) : null;
if (matches) {
this.nextLine();
commit.merge = matches[0] || null;
assignMatchedCorrespondence(commit, matches, correspondence);
return true;
}
return false;
}
parseHeader(isMergeCommit) {
if (isMergeCommit) {
this.skipEmptyLines();
}
const { commit, options } = this;
const correspondence = options.headerCorrespondence || [];
const header = commit.header ?? this.nextLine();
let matches = null;
if (header) {
if (options.breakingHeaderPattern) {
matches = header.match(options.breakingHeaderPattern);
}
if (!matches && options.headerPattern) {
matches = header.match(options.headerPattern);
}
}
if (header) {
commit.header = header;
}
if (matches) {
assignMatchedCorrespondence(commit, matches, correspondence);
}
}
parseMeta() {
const { options, commit } = this;
if (!options.fieldPattern || !this.isLineAvailable()) {
return false;
}
let matches;
let field = null;
let parsed = false;
while (this.isLineAvailable()) {
matches = this.currentLine().match(options.fieldPattern);
if (matches) {
field = matches[1] || null;
this.nextLine();
continue;
}
if (field) {
parsed = true;
commit[field] = appendLine(commit[field], this.currentLine());
this.nextLine();
} else {
break;
}
}
return parsed;
}
parseNotes() {
const { regexes, commit } = this;
if (!this.isLineAvailable()) {
return false;
}
const matches = this.currentLine().match(regexes.notes);
let references = [];
if (matches) {
const note = {
title: matches[1],
text: matches[2]
};
commit.notes.push(note);
commit.footer = appendLine(commit.footer, this.currentLine());
this.nextLine();
while (this.isLineAvailable()) {
if (this.parseMeta()) {
return true;
}
if (this.parseNotes()) {
return true;
}
references = this.parseReferences(this.currentLine());
if (references.length) {
commit.references.push(...references);
} else {
note.text = appendLine(note.text, this.currentLine());
}
commit.footer = appendLine(commit.footer, this.currentLine());
this.nextLine();
if (references.length) {
break;
}
}
return true;
}
return false;
}
parseBodyAndFooter(isBody) {
const { commit } = this;
if (!this.isLineAvailable()) {
return isBody;
}
const references = this.parseReferences(this.currentLine());
const isStillBody = !references.length && isBody;
if (isStillBody) {
commit.body = appendLine(commit.body, this.currentLine());
} else {
commit.references.push(...references);
commit.footer = appendLine(commit.footer, this.currentLine());
}
this.nextLine();
return isStillBody;
}
parseBreakingHeader() {
const { commit, options } = this;
if (!options.breakingHeaderPattern || commit.notes.length || !commit.header) {
return;
}
const matches = commit.header.match(options.breakingHeaderPattern);
if (matches) {
commit.notes.push({
title: "BREAKING CHANGE",
text: matches[3]
});
}
}
parseMentions(input2) {
const { commit, regexes } = this;
let matches;
for (; ; ) {
matches = regexes.mentions.exec(input2);
if (!matches) {
break;
}
commit.mentions.push(matches[1]);
}
}
parseRevert(input2) {
const { commit, options } = this;
const correspondence = options.revertCorrespondence || [];
const matches = options.revertPattern ? input2.match(options.revertPattern) : null;
if (matches) {
commit.revert = assignMatchedCorrespondence({}, matches, correspondence);
}
}
cleanupCommit() {
const { commit } = this;
if (commit.body) {
commit.body = trimNewLines(commit.body);
}
if (commit.footer) {
commit.footer = trimNewLines(commit.footer);
}
commit.notes.forEach((note) => {
note.text = trimNewLines(note.text);
});
}
/**
* Parse commit message string into an object.
* @param input - Commit message string.
* @returns Commit object.
*/
parse(input2) {
if (!input2.trim()) {
throw new TypeError("Expected a raw commit");
}
const { commentChar } = this.options;
const commentFilter = getCommentFilter(commentChar);
const rawLines = trimNewLines(input2).split(/\r?\n/);
const lines = commentChar ? truncateToScissor(rawLines, commentChar).filter((line) => commentFilter(line) && gpgFilter(line)) : rawLines.filter((line) => gpgFilter(line));
const commit = createCommitObject();
this.lines = lines;
this.lineIndex = 0;
this.commit = commit;
const isMergeCommit = this.parseMerge();
this.parseHeader(isMergeCommit);
if (commit.header) {
commit.references = this.parseReferences(commit.header);
}
let isBody = true;
while (this.isLineAvailable()) {
this.parseMeta();
if (this.parseNotes()) {
isBody = false;
}
if (!this.parseBodyAndFooter(isBody)) {
isBody = false;
}
}
this.parseBreakingHeader();
this.parseMentions(input2);
this.parseRevert(input2);
this.cleanupCommit();
return commit;
}
};
var buildCommitMessage = ({
header,
body,
footer
}) => {
let message = header;
message = body ? `${message}
${body}` : message;
message = footer ? `${message}
${footer}` : message;
return message || "";
};
async function lint(message, rawRulesConfig, rawOpts) {
const rulesConfig = rawRulesConfig || {};
const parser = new CommitParser(
rawOpts?.parserOpts ?? DEFAULT_COMMITLINT_CONFIG.parserOpts
);
const parsed = parser.parse(message);
if (parsed.header === null && parsed.body === null && parsed.footer === null) {
return {
valid: true,
errors: [],
warnings: [],
input: message
};
}
const allRules = new Map(Object.entries(defaultRules));
const missing = Object.keys(rulesConfig).filter(
(name) => typeof allRules.get(name) !== "function"
);
if (missing.length > 0) {
const names = [...allRules.keys()];
throw new RangeError(
[
`Found rules without implementation: ${missing.join(", ")}.`,
`Supported rules are: ${names.join(", ")}.`
].join("\n")
);
}
const invalid = Object.entries(rulesConfig).map(([name, config]) => {
if (!Array.isArray(config)) {
return new Error(
`config for rule ${name} must be array, received ${util.inspect(
config
)} of type ${typeof config}`
);
}
const [level] = config;
if (level === 0 /* Disabled */ && config.length === 1) {
return null;
}
const [, when] = config;
if (typeof level !== "number" || isNaN(level)) {
return new Error(
`level for rule ${name} must be number, received ${util.inspect(
level
)} of type ${typeof level}`
);
}
if (config.length < 2 || config.length > 3) {
return new Error(
`config for rule ${name} must be 2 or 3 items long, received ${util.inspect(
config
)} of length ${config.length}`
);
}
if (level < 0 || level > 2) {
return new RangeError(
`level for rule ${name} must be between 0 and 2, received ${util.inspect(
level
)}`
);
}
if (typeof when !== "string") {
return new Error(
`condition for rule ${name} must be string, received ${util.inspect(
when
)} of type ${typeof when}`
);
}
if (when !== "never" && when !== "always") {
return new Error(
`condition for rule ${name} must be "always" or "never", received ${util.inspect(
when
)}`
);
}
return null;
}).filter((item) => item instanceof Error);
if (invalid.length > 0) {
throw new Error(invalid.map((i) => i.message).join("\n"));
}
const pendingResults = Object.entries(rulesConfig).filter(([, config]) => !!config && config.length && config[0] > 0).map(async (entry) => {
const [name, config] = entry;
const [level, when, value] = config;
const rule = allRules.get(name);
if (!rule) {
throw new Error(`Could not find rule implementation for ${name}`);
}
const executableRule = rule;
const [valid3, message2] = await executableRule(parsed, when, value);
return {
level,
valid: valid3,
name,
message: message2
};
});
const results = (await Promise.all(pendingResults)).filter(
(result) => result !== null
);
const errors = results.filter(
(result) => result.level === 2 /* Error */ && !result.valid
);
const warnings = results.filter(
(result) => result.level === 1 /* Warning */ && !result.valid
);
const valid2 = errors.length === 0;
return {
valid: valid2,
errors,
warnings,
input: buildCommitMessage(parsed)
};
}
async function getNxScopes(context) {
let projectGraph;
try {
projectGraph = readCachedProjectGraph();
} catch {
await createProjectGraphAsync$1();
projectGraph = readCachedProjectGraph();
}
if (!projectGraph) {
throw new Error(
"The commit process failed because the project graph is not available. Please run the build command again."
);
}
const projectConfigs = readProjectsConfigurationFromProjectGraph$1(projectGraph);
const result = Object.entries(projectConfigs.projects || {}).map(([name, project]) => ({
name,
...project
})).filter(
(project) => project.name && project.root && project.root !== "." && project.root !== context.config.workspaceRoot && !project.name.includes("e2e")
).filter((project) => project.targets).map((project) => project.name).filter(Boolean).sort((a, b) => a.localeCompare(b));
result.unshift("monorepo");
return result;
}
function getScopeEnumUtil(context) {
return () => getScopeEnum(context);
}
function getScopeEnum(context) {
return getNxScopes(context);
}
function getRuleFromScopeEnum(scopeEnum) {
if (!scopeEnum?.filter(Boolean).length) {
throw new Error("No scopes found in the Storm workspace.");
}
return [
2 /* Error */,
"always",
scopeEnum.filter(Boolean)
];
}
// src/commitlint/run.ts
var COMMIT_EDITMSG_PATH = ".git/COMMIT_EDITMSG";
var runCommitLint = async (config, params) => {
writeInfo(
"\u{1F4DD} Validating git commit message aligns with the Storm Software specification",
config
);
let commitMessage;
if (params.message && params.message !== COMMIT_EDITMSG_PATH) {
commitMessage = params.message;
} else {
const commitFile = joinPaths(
config.workspaceRoot,
params.file || params.message || COMMIT_EDITMSG_PATH
);
if (existsSync(commitFile)) {
commitMessage = (await readFile(commitFile, "utf8"))?.trim();
}
}
if (!commitMessage) {
let gitLogCmd = "git log -1 --no-merges";
const gitRemotes = childProcess.execSync("git remote -v").toString().trim().split("\n");
const upstreamRemote = gitRemotes.find(
(remote) => remote.includes(`${config.name}.git`)
);
if (upstreamRemote) {
const upstreamRemoteIdentifier = upstreamRemote.split(" ")[0]?.trim();
if (!upstreamRemoteIdentifier) {
writeWarning(
`No upstream remote found for ${config.name}.git. Skipping comparison.`,
config
);
return;
}
writeDebug(`Comparing against remote ${upstreamRemoteIdentifier}`);
const currentBranch = childProcess.execSync("git branch --show-current").toString().trim();
gitLogCmd = gitLogCmd + ` ${currentBranch} ^${upstreamRemoteIdentifier}/main`;
} else {
writeWarning(
`No upstream remote found for ${config.name}.git. Skipping comparison against upstream main.`,
config
);
return;
}
commitMessage = childProcess.execSync(gitLogCmd).toString().trim();
if (!commitMessage) {
writeWarning(
"No commits found. Skipping commit message validation.",
config
);
return;
}
}
const allowedTypes = Object.keys(COMMIT_TYPES).join("|");
const allowedScopes = await getNxScopes({
config
});
const commitMsgRegex = `(${allowedTypes})\\((${allowedScopes})\\)!?:\\s(([a-z0-9:-s])+)`;
const matchCommit = new RegExp(commitMsgRegex, "g").test(commitMessage);
const commitlintConfig = defu(
params.config ?? {},
{ rules: { "scope-enum": getRuleFromScopeEnum(allowedScopes) } },
DEFAULT_COMMITLINT_CONFIG
);
const report = await lint(commitMessage, commitlintConfig.rules, {
parserOpts: commitlintConfig.parserOpts,
helpUrl: commitlintConfig.helpUrl
});
if (!matchCommit || report.errors.length || report.warnings.length) {
writeSuccess(`Commit was processing completed successfully!`, config);
} else {
let errorMessage = " Oh no! Your commit message: \n-------------------------------------------------------------------\n" + commitMessage + "\n-------------------------------------------------------------------\n\n Does not follow the commit message convention specified by Storm Software.";
errorMessage += "\ntype(scope): subject \n BLANK LINE \n body";
errorMessage += "\n";
errorMessage += `
Possible types: ${allowedTypes}`;
errorMessage += `
Possible scopes: ${allowedScopes} (if unsure use "monorepo")`;
errorMessage += "\n\nEXAMPLE: \nfeat(my-lib): add an option to generate lazy-loadable modules\nfix(monorepo)!: breaking change should have exclamation mark\n";
errorMessage += `
CommitLint Errors: ${report.errors.length ? report.errors.map((error) => ` - ${error.message}`).join("\n") : "None"}`;
errorMessage += `
CommitLint Warnings: ${report.warnings.length ? report.warnings.map((warning) => ` - ${warning.message}`).join("\n") : "None"}`;
errorMessage += "\n\nPlease fix the commit message and rerun storm-commit.";
errorMessage += `
More details about the Storm Software commit message specification can be found at: ${commitlintConfig.helpUrl}`;
throw new Error(errorMessage);
}
return report.input;
};
// src/commit/config.ts
var DEFAULT_COMMIT_CONFIG = {
settings: DEFAULT_COMMIT_SETTINGS,
messages: DEFAULT_COMMIT_PROMPT_MESSAGES,
questions: DEFAULT_COMMIT_QUESTIONS,
types: COMMIT_TYPES
};
// src/commit/commit-state.ts
function getGitDir() {
const devNull = process.platform === "win32" ? " nul" : "/dev/null";
const dir = execSync(`git rev-parse --absolute-git-dir 2>${devNull}`).toString().trim();
return dir;
}
function getGitRootDir() {
const devNull = process.platform === "win32" ? " nul" : "/dev/null";
const dir = execSync(`git rev-parse --show-toplevel 2>${devNull}`).toString().trim();
return dir;
}
async function resolveCommitOptions(config, workspaceConfig) {
return {
utils: { getScopeEnum: getScopeEnumUtil({ config: workspaceConfig }) },
parserPreset: "conventional-changelog-conventionalcommits",
prompt: {
settings: config.settings,
messages: config.messages,
questions: config.questions
}
};
}
async function resolveDefaultCommitOptions(config) {
return resolveCommitOptions(DEFAULT_COMMIT_CONFIG, config);
}
async function createState(config, commitizenFile = "@storm-software/git-tools/commit/config") {
let root;
try {
root = getGitRootDir();
} catch (_2) {
throw new Error("Could not find Git root folder.");
}
let state;
if (commitizenFile === "@storm-software/git-tools/commit/config") {
state = {
config: await resolveDefaultCommitOptions(config),
root,
answers: {}
};
} else {
writeInfo(`Using custom commit config file: ${commitizenFile}`, config);
let commitizenConfig = await import(commitizenFile);
if (commitizenConfig?.default) {
commitizenConfig = commitizenConfig?.default;
}
state = {
config: await resolveCommitOptions(
defu(commitizenConfig ?? {}, DEFAULT_COMMIT_CONFIG),
config
),
root,
answers: {}
};
}
if (state.config.prompt.questions.type && state.config.prompt.questions.type.enum) {
state.config.prompt.questions.type.enum = Object.keys(state.config.prompt.questions.type.enum).reduce(
(ret, key) => {
if (state.config.prompt.questions.type.enum) {
ret[key] = {
...state.config.prompt.questions.type.enum[key],
title: chalkTemplate`${state.config.prompt.questions.type.enum[key]?.emoji ? `${state.config.prompt.questions.type.enum[key]?.emoji} ` : ""}{bold ${key}} ${state.config.prompt.questions.type.enum[key]?.title && state.config.prompt.questions.type.enum[key]?.title !== key ? `- ${state.config.prompt.questions.type.enum[key]?.title}` : ""}${state.config.prompt.questions.type.enum[key]?.semverBump ? ` (version bump: ${state.config.prompt.questions.type.enum[key]?.semverBump})` : ""}`,
hidden: false
};
}
return ret;
},
{}
);
}
if (!state.config.prompt.questions.scope || !state.config.prompt.questions.scope.enum || Object.keys(state.config.prompt.questions.scope.enum).length === 0) {
const scopes = await getScopeEnum({
config
});
for (const scope of scopes) {
if (scope === "monorepo") {
state.config.prompt.questions.scope.enum[scope] = {
title: chalkTemplate`{bold monorepo} - workspace root`,
description: "The base workspace package (workspace root)",
hidden: false,
projectRoot: "/"
};
} else {
let projectGraph;
try {
projectGraph = readCachedProjectGraph();
} catch {
await createProjectGraphAsync$1();
projectGraph = readCachedProjectGraph();
}
if (!projectGraph) {
throw new Error(
"Failed to load the project graph. Please run `nx reset`, then run the `storm-git commit` command again."
);
}
const projectConfigurations = readProjectsConfigurationFromProjectGraph$1(projectGraph);
if (!projectConfigurations?.projects?.[scope]) {
throw new Error(
`Failed to load the project configuration for project ${scope}. Please run \`nx reset\`, then run the \`storm-git commit\` command again.`
);
}
const project = projectConfigurations.projects[scope];
if (project) {
let description = `${project.name} - ${project.root}`;
const packageJsonPath = joinPaths(project.root, "package.json");
if (existsSync$1(packageJsonPath)) {
const packageJsonFile = await readFile$1(packageJsonPath, "utf8");
const packageJson = JSON.parse(packageJsonFile);
description = packageJson.description || description;
}
state.config.prompt.questions.scope.enum[scope] = {
title: chalkTemplate`{bold ${project.name}} - ${project.root}`,
description,
hidden: false,
projectRoot: project.root
};
}
}
}
}
state.answers = Object.keys(state.config.prompt.questions).reduce(
(ret, key) => {
ret[key] = "";
return ret;
},
{}
);
return state;
}
var MAX_LINE_WIDTH = 72;
var formatCommitMessage = (state) => {
const { config, answers } = state;
const wrapOptions = {
indent: "",
trim: true,
width: MAX_LINE_WIDTH
};
if (typeof answers.type !== "string") {
throw new Error("Invalid commit type.");
}
if (typeof answers.scope !== "string") {
throw new Error("Invalid commit scope.");
}
if (typeof answers.subject !== "string") {
throw new Error("Invalid subject type.");
}
const emoji = answers.type?.[answers.type]?.emoji ? answers.type[answers.type].emoji : "";
const scope = answers.scope ? answers.scope.trim() : "";
const subject = answers.subject?.trim();
const type = answers.type;
const format2 = config.prompt.settings.format || "{type}({scope}): {emoji}{subject}";
const body = answers.body && typeof answers.body === "string" ? wrap(answers.body || "", wrapOptions) : "";
const breaking = answers.breakingBody && typeof answers.breakingBody === "string" ? wrap(answers.breakingBody || "", wrapOptions) : "";
const issues = answers.issuesBody && typeof answers.issuesBody === "string" ? wrap(answers.issuesBody || "", wrapOptions) : "";
const head = format2.replace(
/\{emoji\}/g,
config.prompt.settings.disableEmoji ? "" : `${emoji} `
).replace(/\{scope\}/g, scope).replace(/\{subject\}/g, subject || "").replace(/\{type\}/g, type || "");
let msg = head;
if (body) {
msg += `
${body}`;
}
if (breaking) {
const breakingEmoji = config.prompt.settings.disableEmoji ? "" : config.prompt.settings.breakingChangePrefix;
msg += `
BREAKING CHANGE: ${breakingEmoji}${breaking}`;
}
if (issues) {
const closedIssueEmoji = config.prompt.settings.disableEmoji ? "" : config.prompt.settings.closedIssuePrefix;
msg += `
${closedIssueEmoji}${config.prompt.settings.closedIssueMessage}${issues}`;
}
return msg;
};
// src/commit/run.ts
var runCommit = async (commitizenFile = "@storm-software/git-tools/commit/config", dryRun = false) => {
const config = await getConfig();
const state = await createState(config, commitizenFile);
if (dryRun) {
writeInfo("Running in dry mode.", config);
}
console.log(chalkTemplate`
{bold.#999999 ----------------------------------------}
{bold.#FFFFFF ⚡ Storm Software Git-Tools - Commit}
{#CCCCCC Please provide the requested details below...}
`);
state.answers = await askQuestions(state);
const message = formatCommitMessage(state);
const commitMsgFile = joinPaths(getGitDir(), "COMMIT_EDITMSG");
console.log(chalkTemplate`
{bold.#999999 ----------------------------------------}
{bold.#FFFFFF Commit message} - {#DDDDDD ${message}}
{bold.#FFFFFF Git-Commit File} - {#DDDDDD ${commitMsgFile}}
`);
await runCommitLint(config, { message });
const commandItems = ["git", "commit", "-S"];
commandItems.push(...["--file", commitMsgFile]);
const command = shellescape(commandItems);
if (dryRun) {
writeDebug(
`Skipping execution [dry-run]: ${command.replace(commitMsgFile, ".git/COMMIT_EDITMSG")}`,
config
);
writeDebug(`Message [dry-run]: ${message}`, config);
} else {
await fs.writeFile(commitMsgFile, message);
run(config, command);
}
};
var askQuestions = async (state) => {
let index = 0;
for (const key of Object.keys(state.config.prompt.questions)) {
if (state.config.prompt.questions[key] && !state.config.prompt.questions[key].hidden && (!state.config.prompt.questions[key].when || state.config.prompt.questions[key].when(state.answers))) {
state.answers[key] = await askQuestion(
index,
state.config.prompt.questions[key]
);
index++;
}
}
return state.answers;
};
var askQuestion = (index, question) => {
const message = chalkTemplate`{bold ${index + 1}. ${question.title}} - ${question.description}
`;
if (question.type === "select" && question.enum && Object.keys(question.enum).length > 1) {
return select({
message,
choices: Object.keys(question.enum).filter((key) => !question.enum?.[key]?.hidden).map((key) => ({
name: question.enum?.[key]?.title || key,
value: key,
description: question.enum?.[key]?.description || ""
})),
default: String(question.defaultValue || "")
});
} else if (question.type === "confirm") {
return confirm({
message,
default: Boolean(question.defaultValue)
});
} else {
let validate = void 0;
if (question.minLength !== void 0 || question.maxLength !== void 0) {
validate = (value) => {
if (question.minLength !== void 0 && value.length < question.minLength) {
return `Minimum length is ${question.minLength} characters.`;
}
if (question.maxLength !== void 0 && value.length > question.maxLength) {
return `Maximum length is ${question.maxLength} characters.`;
}
return true;
};
}
return input({
message,
required: !!(question.minLength !== void 0 && question.minLength > 0),
default: String(question.defaultValue || ""),
validate
});
}
};
function findFileName(filePath) {
return filePath?.split(
filePath?.includes(Path.sep) ? Path.sep : filePath?.includes("/") ? "/" : "\\"
)?.pop() ?? "";
}
function findFilePath(filePath) {
return filePath.replace(findFileName(filePath), "");
}
var start = "<!-- START doctoc -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->";
var end = "<!-- END doctoc -->";
var skipTag = "<!-- DOCTOC SKIP -->";
function matchesStart(line) {
return /<!-- START doctoc /.test(line);
}
function matchesEnd(line) {
return /<!-- END doctoc /.test(line);
}
function notNull(x) {
return x !== null;
}
function addAnchor(mode, header) {
header.anchor = anchor(header.name, mode, header.instance);
return header;
}
function getMarkdownHeaders(lines, maxHeaderLevel) {
function extractText(header) {
return header.children.map(function(x) {
if (x.type === Syntax.Link) {
return extractText(x);
} else if (x.type === Syntax.Image) {
return "*";
} else {
return x.raw;
}
}).join("");
}
return parse(lines.join("\n")).children.filter(function(x) {
return x.type === Syntax.Header;
}).map(function(x) {
return !maxHeaderLevel || x.depth <= maxHeaderLevel ? { rank: x.depth, name: extractText(x), line: x.loc.start.line } : null;
}).filter(notNull);
}
function countHeaders(headers) {
const instances = {};
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
const name = header.name;
if (Object.prototype.hasOwnProperty.call(instances, name)) {
instances[name]++;
} else {
instances[name] = 0;
}
header.instance = instances[name];
}
return headers;
}
function getLinesToToc(lines, currentToc, info, processAll) {
if (processAll || !currentToc) return lines;
let tocableStart = 0;
if (info.hasEnd) tocableStart = info.endIdx + 1;
return lines.slice(tocableStart);
}
function determineTitle(title, notitle, lines, info) {
const defaultTitle = "**Table of Contents** ";
if (notitle) return "";
if (title) return title;
return info.hasStart ? lines[info.startIdx + 2] : defaultTitle;
}
function transform(content, mode, maxHeaderLevel, title, notitle, entryPrefix, processAll, updateOnly) {
if (content.indexOf(skipTag) !== -1) return { transformed: false };
mode = mode || "github.com";
entryPrefix = entryPrefix || "-";
const maxHeaderLevelHtml = maxHeaderLevel || 4;
const lines = content.split("\n"), info = updateSection.parse(lines, matchesStart, matchesEnd);
if (!info.hasStart && updateOnly) {
return { transformed: false };
}
const inferredTitle = determineTitle(title, notitle, lines, info);
const titleSeparator = inferredTitle ? "\n\n" : "\n";
const currentToc = info.hasStart && lines.slice(info.startIdx, info.endIdx + 1).join("\n"), linesToToc = getLinesToToc(lines, currentToc, info, processAll);
const headers = getMarkdownHeaders(linesToToc, maxHeaderLevel).concat(
getHtmlHeaders(linesToToc, maxHeaderLevelHtml)
);
headers.sort((a, b) => {
if (!a && b) {
return -1;
}
if (a && !b) {
return 1;
}
if (!a && !b) {
return 0;
}
return a.line - b.line;
});
const allHeaders = countHeaders(headers), lowestRank = _(allHeaders).chain().pluck("rank").min().value(), linkedHeaders = _(allHeaders).map(addAnchor.bind(null, mode));
if (linkedHeaders.length === 0) return { transformed: false };
const indentation = mode === "bitbucket.org" || mode === "gitlab.com" ? " " : " ";
const toc = inferredTitle + titleSeparator + linkedHeaders.map(function(x) {
const indent = _(_.range(x.rank - lowestRank)).reduce(function(acc, x2) {
return acc + indentation;
}, "");
return indent + entryPrefix + " " + x.anchor;
}).join("\n") + "\n";
const wrappedToc = start + "\n" + toc + "\n" + end;
if (currentToc === toc) return { transformed: false };
const data = updateSection(
lines.join("\n"),
wrappedToc,
matchesStart,
matchesEnd,
true
);
return { transformed: true, data, toc, wrappedToc };
}
function addLinenos(lines, headers) {
let current = 0, line;
return headers.map(function(x) {
for (let lineno = current; lineno < lines.length; lineno++) {
line = lines[lineno];
if (new RegExp(x.text[0]).test(line)) {
current = lineno;
x.line = lineno;
x.name = x.text.join("");
return x;
}
}
x.line = ++current;
x.name = x.text.join("");
return x;
});
}
function rankify(headers, max) {
return headers.map(function(x) {
x.rank = parseInt(x.tag.slice(1), 10);
return x;
}).filter(function(x) {
return x.rank <= max;
});
}
function getHtmlHeaders(lines, maxHeaderLevel) {
const source = parse(lines.join("\n")).children.filter(function(node) {
return node.type === Syntax.Html;
}).map(function(node) {
return node.raw;
}).join("\n");
let headers = [];
const grabbing = [];
let text = [];
const parser = new Parser(
{
onopentag: function(name, attr) {
if (grabbing[grabbing.length - 1] === "pre") return;
if (name === "pre" || /h\d/.test(name)) {
grabbing.push(name);
}
},
ontext: function(text_) {
if (grabbing.length === 0 || grabbing[grabbing.length - 1] === "pre")
return;
text.push(text_);
},
onclosetag: function(name) {
if (grabbing.length === 0) return;
if (grabbing[grabbing.length - 1] === name) {
const tag = grabbing.pop();
headers.push({ text, tag });
text = [];
}
}
},
{ decodeEntities: true }
);
parser.write(source);
parser.end();
headers = addLinenos(lines, headers);
headers = rankify(headers, maxHeaderLevel);
return headers;
}
// src/readme/doctoc.ts
async function transformAndSave(files, mode = "github.com", maxHeaderLevel = 3, title = "## Table of Contents", noTitle = false, entryPrefix = void 0, processAll = false, updateOnly = false) {
if (processAll) {
console.log(
"--all flag is enabled. Including headers before the TOC location."
);
}
if (updateOnly) {
console.log(
"--update-only flag is enabled. Only updating files that already have a TOC."
);
}
console.log("\n==================\n");
const transformed = files.map((x) => {
const result = transform(
readFileSync(x.path, "utf8"),
mode,
maxHeaderLevel,
title,
noTitle,
entryPrefix,
processAll,
updateOnly
);
result.path = x.path;
return result;
});
const changed = transformed.filter((x) => x.transformed);
const unchanged = transformed.filter((x) => {
return !x.transformed;
});
for (const x of unchanged) {
console.log('"%s" is up to date', x.path);
}
for (const x of changed) {
console.log('"%s" will be updated', x.path);
writeFileSync(x.path, x.data, "utf8");