UNPKG

@storm-software/git-tools

Version:

Tools for managing Git repositories within a Nx workspace.

1,484 lines (1,461 loc) 151 kB
#!/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");