UNPKG

@twyr/announce

Version:

CLI Tool and NPM Library for announcing a release on Github / Gitlab / etc. and on NPM

567 lines (495 loc) 20.8 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>commands/prepare-command.js - Announce CLI - API Reference</title> <meta name="description" content="Announce CLI - API Reference" /> <meta name="keywords" content="automation announce changelog cli console git github gitlab npm package publish release release-automation release-helper release-workflow semantic-release semantic-version semver semver-release terminal twyr version" /> <meta name="keyword" content="automation announce changelog cli console git github gitlab npm package publish release release-automation release-helper release-workflow semantic-release semantic-version semver semver-release terminal twyr version" /> <script src="scripts/prettify/prettify.js"></script> <script src="scripts/prettify/lang-css.js"></script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc.css"> <script src="scripts/nav.js" defer></script> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body> <input type="checkbox" id="nav-trigger" class="nav-trigger" /> <label for="nav-trigger" class="navicon-button x"> <div class="navicon"></div> </label> <label for="nav-trigger" class="overlay"></label> <nav class="wrap"> <input type="text" id="nav-search" placeholder="Search" /> <h2><a href="index.html">Home</a></h2> <h2><a href="https://github.com/twyr/announce" target="_blank" class="menu-item" id="github_link">GitHub</a></h2> <h3>Classes</h3> <ul> <li><a href="GitHubWrapper.html">GitHubWrapper</a> <ul class='methods'> <li data-type='method'><a href="GitHubWrapper.html#_fetchData">_fetchData</a></li> <li data-type='method'><a href="GitHubWrapper.html#createRelease">createRelease</a></li> <li data-type='method'><a href="GitHubWrapper.html#fetchCommitAuthorInformation">fetchCommitAuthorInformation</a></li> <li data-type='method'><a href="GitHubWrapper.html#fetchCommitInformation">fetchCommitInformation</a></li> <li data-type='method'><a href="GitHubWrapper.html#fetchReleaseInformation">fetchReleaseInformation</a></li> <li data-type='method'><a href="GitHubWrapper.html#getCommitLink">getCommitLink</a></li> </ul> </li> <li><a href="GitLabWrapper.html">GitLabWrapper</a> <ul class='methods'> <li data-type='method'><a href="GitLabWrapper.html#createRelease">createRelease</a></li> <li data-type='method'><a href="GitLabWrapper.html#fetchCommitAuthorInformation">fetchCommitAuthorInformation</a></li> <li data-type='method'><a href="GitLabWrapper.html#fetchCommitInformation">fetchCommitInformation</a></li> <li data-type='method'><a href="GitLabWrapper.html#fetchReleaseInformation">fetchReleaseInformation</a></li> <li data-type='method'><a href="GitLabWrapper.html#getCommitLink">getCommitLink</a></li> </ul> </li> <li><a href="PrepareCommandClass.html">PrepareCommandClass</a> <ul class='methods'> <li data-type='method'><a href="PrepareCommandClass.html#_bumpVersion">_bumpVersion</a></li> <li data-type='method'><a href="PrepareCommandClass.html#_computeNextVersion">_computeNextVersion</a></li> <li data-type='method'><a href="PrepareCommandClass.html#_getCurrentVersion">_getCurrentVersion</a></li> <li data-type='method'><a href="PrepareCommandClass.html#_getTargetFileList">_getTargetFileList</a></li> <li data-type='method'><a href="PrepareCommandClass.html#_mergeOptions">_mergeOptions</a></li> <li data-type='method'><a href="PrepareCommandClass.html#_setupLogger">_setupLogger</a></li> <li data-type='method'><a href="PrepareCommandClass.html#_setupTasks">_setupTasks</a></li> <li data-type='method'><a href="PrepareCommandClass.html#execute">execute</a></li> </ul> </li> <li><a href="PublishCommandClass.html">PublishCommandClass</a> <ul class='methods'> <li data-type='method'><a href="PublishCommandClass.html#_getUpstreamRepositoryInfo">_getUpstreamRepositoryInfo</a></li> <li data-type='method'><a href="PublishCommandClass.html#_initializeGit">_initializeGit</a></li> <li data-type='method'><a href="PublishCommandClass.html#_mergeOptions">_mergeOptions</a></li> <li data-type='method'><a href="PublishCommandClass.html#_publishToNpm">_publishToNpm</a></li> <li data-type='method'><a href="PublishCommandClass.html#_setupLogger">_setupLogger</a></li> <li data-type='method'><a href="PublishCommandClass.html#_setupTasks">_setupTasks</a></li> <li data-type='method'><a href="PublishCommandClass.html#execute">execute</a></li> </ul> </li> <li><a href="ReleaseCommandClass.html">ReleaseCommandClass</a> <ul class='methods'> <li data-type='method'><a href="ReleaseCommandClass.html#_generateChangelog">_generateChangelog</a></li> <li data-type='method'><a href="ReleaseCommandClass.html#_generateRelease">_generateRelease</a></li> <li data-type='method'><a href="ReleaseCommandClass.html#_initializeGit">_initializeGit</a></li> <li data-type='method'><a href="ReleaseCommandClass.html#_mergeOptions">_mergeOptions</a></li> <li data-type='method'><a href="ReleaseCommandClass.html#_pushUpstream">_pushUpstream</a></li> <li data-type='method'><a href="ReleaseCommandClass.html#_restoreCode">_restoreCode</a></li> <li data-type='method'><a href="ReleaseCommandClass.html#_setupLogger">_setupLogger</a></li> <li data-type='method'><a href="ReleaseCommandClass.html#_setupTasks">_setupTasks</a></li> <li data-type='method'><a href="ReleaseCommandClass.html#_stashOrCommit">_stashOrCommit</a></li> <li data-type='method'><a href="ReleaseCommandClass.html#_summarize">_summarize</a></li> <li data-type='method'><a href="ReleaseCommandClass.html#_tagCode">_tagCode</a></li> <li data-type='method'><a href="ReleaseCommandClass.html#execute">execute</a></li> </ul> </li> </ul> </nav> <div id="main"> <h1 class="page-title">commands/prepare-command.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>/* eslint-disable security-node/detect-non-literal-require-calls */ /* eslint-disable security/detect-non-literal-require */ /* eslint-disable security-node/non-literal-reg-expr */ /* eslint-disable security/detect-non-literal-regexp */ /* eslint-disable no-loop-func */ 'use strict'; /** * Module dependencies, required for ALL Twy'r modules * @ignore */ /** * Module dependencies, required for this module * @ignore */ /** * @class PrepareCommandClass * @classdesc The command class that handles all the prepare operations. * * @param {object} mode - Set the current run mode - CLI or API * * @description * The command class that implements the "prepare" step of the workflow. * Please see README.md for the details of what this step involves. * */ class PrepareCommandClass { // #region Constructor constructor(mode) { this.#execMode = mode; } // #endregion // #region Public Methods /** * @async * @function * @instance * @memberof PrepareCommandClass * @name execute * * @param {object} options - Parsed command-line options, or options passed in via API * * @return {null} Nothing. * * @summary The main method to prepare the codebase for the next release. * * This method does 2 things: * - Generates the next version string based on the current one, the option passed in, and the pre-defined version ladder * - Parses the source files for the current version string, and replaces it with the next one * */ async execute(options) { // Step 1: Setup sane defaults for the options const mergedOptions = this._mergeOptions(options); // console.log(`Merged Options: ${JSON.stringify(mergedOptions, null, '\t')}`); // Step 2: Set up the logger according to the options passed in const logger = this._setupLogger(mergedOptions); mergedOptions.logger = logger; // Step 3: Setup the task list const taskList = this?._setupTasks?.(); // Step 4: Run the tasks in sequence // eslint-disable-next-line security-node/detect-crlf console.log(`Bumping codebase version for the next development cycle:`); await taskList?.run?.({ 'options': mergedOptions, 'execError': null }); } // #endregion // #region Private Methods /** * @function * @instance * @memberof PrepareCommandClass * @name _mergeOptions * * @param {object} options - Parsed command-line options, or options passed in via API * * @return {object} Merged options - input options > configured options. * * @summary Merges options passed in with configured ones - and puts in sane defaults if neither is available. * */ _mergeOptions(options) { const mergedOptions = Object?.assign?.({}, options); mergedOptions.currentWorkingDirectory = mergedOptions?.currentWorkingDirectory ?? process?.cwd?.(); mergedOptions.series = mergedOptions?.series ?? 'current'; mergedOptions.versionLadder = mergedOptions?.versionLadder?.split?.(',')?.map?.((stage) => { return stage?.trim?.(); })?.filter?.((stage) => { return !!stage &amp;&amp; stage?.length; }); mergedOptions.ignoreFolders = mergedOptions?.ignoreFolders?.split?.(',')?.map?.((folder) => { return folder?.trim?.(); })?.filter?.((folder) => { return !!folder &amp;&amp; folder?.length; }); return mergedOptions; } /** * @function * @instance * @memberof PrepareCommandClass * @name _setupLogger * * @param {object} options - merged options object returned by the _mergeOptions method * * @return {object} Logger object with info / error functions. * * @summary Creates a logger in CLI mode or uses the passed in logger object in API mode - and returns it. * */ _setupLogger(options) { if(this.#execMode === 'api') return options?.logger; return null; } /** * @function * @instance * @memberof PrepareCommandClass * @name _setupTasks * * @return {object} Tasks as Listr. * * @summary Setup the list of tasks to be run * */ _setupTasks() { const Listr = require('listr'); const taskList = new Listr([{ 'title': 'Reading current version...', 'task': this?._getCurrentVersion?.bind?.(this) }, { 'title': 'Computing next version...', 'task': this?._computeNextVersion?.bind?.(this) }, { 'title': 'Scanning files to be modified...', 'task': this?._getTargetFileList?.bind?.(this) }, { 'title': 'Bump version...', 'task': this?._bumpVersion?.bind?.(this), 'skip': (ctxt) => { if(ctxt?.options?.targetFiles?.length) return false; return 'No files found for modification'; } }], { 'collapse': false }); return taskList; } /** * @function * @instance * @memberof PrepareCommandClass * @name _getCurrentVersion * * @param {object} ctxt - Task context containing the options object returned by the _mergeOptions method * @param {object} task - Reference to the task that is running * * @return {null} Nothing. * * @summary Returns the version contained in the package.json file. * */ _getCurrentVersion(ctxt, task) { ctxt?.options?.logger?.info?.(task.title); const path = require('path'); const semver = require('semver'); const projectPackageJson = path.join(ctxt?.options?.currentWorkingDirectory, 'package.json'); const { version } = require(projectPackageJson); if(!version) { ctxt?.options?.logger?.error?.(`${projectPackageJson} doesn't contain a version field.`); throw new Error(`${projectPackageJson} doesn't contain a version field.`); } if(!semver.valid(version)) { ctxt?.options?.logger?.error?.(`${projectPackageJson} contains a non-semantic-version format: ${version}.`); throw new Error(`${projectPackageJson} contains a non-semantic-version format: ${version}`); } ctxt?.options?.logger?.info(`Current version is: ${version}`); task.title = `Current version is: ${version}`; ctxt.options.currentVersion = version; } /** * @function * @instance * @memberof PrepareCommandClass * @name _computeNextVersion * * @param {object} ctxt - Task context containing the options object returned by the _mergeOptions method * @param {object} task - Reference to the task that is running * * @return {null} Nothing. * * @summary Computes the next version to be applied based on current version, the series, and the version ladder - and returns the string representation of it. * */ _computeNextVersion(ctxt, task) { ctxt?.options?.logger?.info(task.title); const semver = require('semver'); const parsedVersion = semver?.parse?.(ctxt?.options?.currentVersion); const incArgs = [ctxt?.options?.currentVersion]; switch (ctxt?.options?.series) { case 'current': if(parsedVersion?.prerelease?.length) { incArgs?.push?.('prerelease'); incArgs?.push?.(parsedVersion?.prerelease[0]); } else { incArgs?.push?.('patch'); } break; case 'next': if(parsedVersion?.prerelease?.length) { let preReleaseTag = parsedVersion?.prerelease[0]; const currentStep = ctxt?.options?.versionLadder?.indexOf?.(preReleaseTag); if(currentStep === -1) preReleaseTag = 'patch'; else if(currentStep === ctxt?.options?.versionLadder?.length - 1) preReleaseTag = 'patch'; else preReleaseTag = ctxt?.options?.versionLadder?.[currentStep + 1]; if(preReleaseTag !== 'patch') incArgs?.push?.('prerelease'); incArgs?.push?.(preReleaseTag); } else { incArgs?.push?.('prerelease'); incArgs?.push?.(ctxt?.options?.versionLadder?.[0]); } break; case 'patch': case 'minor': case 'major': incArgs?.push?.(ctxt?.options?.series); break; default: throw new Error(`Unknown series: ${ctxt?.options?.series}`); } const nextVersion = incArgs?.length ? semver?.inc?.(...incArgs) : ctxt?.options?.series; ctxt?.options?.logger?.info(`Next version will be: ${nextVersion}`); task.title = `Next version will be: ${nextVersion}`; ctxt.options.nextVersion = nextVersion; } /** * @async * @function * @instance * @memberof PrepareCommandClass * @name _getTargetFileList * * @param {object} ctxt - Task context containing the options object returned by the _mergeOptions method * @param {object} task - Reference to the task that is running * * @return {null} Nothing. * * @summary Looks at all the files in the project folder/sub-folders, removes files ignored by .gitignore, then removes files in folders marked ignore in the config, and returns the remaining. * */ async _getTargetFileList(ctxt, task) { try { ctxt?.options?.logger?.info?.(task.title); const { 'fdir': FDir } = require('fdir'); const crawler = new FDir()?.withFullPaths?.()?.crawl?.(ctxt?.options?.currentWorkingDirectory); const targetFiles = await crawler?.withPromise?.(); // eslint-disable-next-line node/no-missing-require const path = require('path'); const gitIgnorePath = path.join(ctxt?.options?.currentWorkingDirectory, '.gitignore'); const fileSystem = require('fs/promises'); // eslint-disable-next-line security/detect-non-literal-fs-filename let gitIgnoreFile = await fileSystem?.readFile?.(gitIgnorePath, { 'encoding': 'utf8' }); gitIgnoreFile += `\n\n**/.git\n${ctxt?.options?.ignoreFolders?.join?.('\n')}\n\n`; gitIgnoreFile = gitIgnoreFile ?.split?.('\n') ?.map?.((gitIgnoreLine) => { if(gitIgnoreLine?.trim?.()?.length === 0) return gitIgnoreLine?.trim?.(); if(gitIgnoreLine?.startsWith?.('#')) return gitIgnoreLine; if(gitIgnoreLine?.startsWith?.('**/')) return gitIgnoreLine; if(gitIgnoreLine?.startsWith?.('/')) return `${gitIgnoreLine}\n**${gitIgnoreLine}`; return `${gitIgnoreLine}\n${path?.join?.('**', gitIgnoreLine)}`; }) .filter((gitIgnoreLine) => { return gitIgnoreLine?.length; }) .join('\n\n'); const gitIgnoreParser = require('gitignore-parser'); const gitIgnore = gitIgnoreParser?.compile?.(gitIgnoreFile); ctxt.options.targetFiles = targetFiles?.filter?.(gitIgnore?.accepts); ctxt?.options?.logger?.info?.(`Files to be modified: ${ctxt?.options?.targetFiles?.length}`); task.title = `Files to be modified: ${ctxt?.options?.targetFiles?.length}`; } catch(err) { ctxt?.options?.logger?.error?.(`Problem processing .gitignore: ${err.message}.`); task.title = `Scanning files to be modified: Error`; throw err; } } /** * @async * @function * @instance * @memberof PrepareCommandClass * @name _bumpVersion * * @param {object} ctxt - Task context containing the options object returned by the _mergeOptions method * @param {object} task - Reference to the task that is running * * @return {null} Nothing. * * @summary Replaces the old version string with the new version string. * */ async _bumpVersion(ctxt, task) { ctxt?.options?.logger?.info(task.title); const replaceInFile = require('replace-in-file'); const replaceOptions = { 'files': '', 'from': '', 'to': ctxt?.options?.nextVersion }; const changedFiles = []; let currentCount = 0; let changedCount = 0; // eslint-disable-next-line no-useless-escape const currentVersion = ctxt?.options?.currentVersion?.replace?.(/\./g, `\.`); const targetFiles = ctxt?.options?.targetFiles; for(const targetFile of targetFiles) { currentCount++; task.title = `Processing ${currentCount} / ${targetFiles?.length}, Modified ${changedCount}`; replaceOptions.files = targetFile; const path = require('path'); const targetFileBaseName = path.basename(targetFile).trim(); if(targetFileBaseName.startsWith('package') || targetFileBaseName.startsWith('npm')) replaceOptions.from = new RegExp(currentVersion, 'i'); else replaceOptions.from = new RegExp(currentVersion, 'gi'); const results = await replaceInFile?.(replaceOptions); if(!results?.length) continue; results.forEach((result) => { if(!result?.hasChanged) return; changedCount++; changedFiles?.push?.(result?.file); }); if(this.#execMode === 'cli') await this?._sleep?.(250); } ctxt?.options?.logger?.info?.(`Bumped version in ${changedCount} files: ${changedFiles?.join?.(',')}`); task.title = `Bumped version in ${changedCount} files:`; if(this.#execMode !== 'cli') return; setTimeout(() => { console.info?.(` ${changedFiles?.join?.('\n ')}`); }, 500); } async _sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } // #endregion // #region Private Fields #execMode = null; // #endregion } // Add the command to the cli exports.commandCreator = function commandCreator(commanderProcess, configuration) { const Commander = require('commander'); const prepare = new Commander.Command('prepare'); // Setup the command prepare?.alias?.('prep'); prepare ?.option?.('--current-working-directory &lt;folder>', 'Path to the current working directory', configuration?.prepare?.currentWorkingDirectory?.trim?.() ?? process?.cwd?.()) ?.option?.('--series &lt;type>', 'Specify the series of the next release (current, next, patch, minor, major)', (configuration?.prepare?.series ?? 'current')) ?.option?.('--version-ladder &lt;stages>', 'Specify the series releases used in the project', (configuration?.prepare?.versionLadder ?? 'dev, alpha, beta, rc, patch, minor, major')) ?.option?.('--ignore-folders &lt;folder list>', 'Comma-separated list of folders to ignore when checking for files containing the current version string', (configuration?.prepare?.ignoreFolders ?? '')); const commandObj = new PrepareCommandClass('cli'); prepare?.action?.(commandObj?.execute?.bind?.(commandObj)); // Add it to the mix commanderProcess?.addCommand?.(prepare); return; }; // Export the API for usage by downstream programs exports.apiCreator = function apiCreator() { const commandObj = new PrepareCommandClass('api'); return { 'name': 'prepare', 'method': commandObj.execute.bind(commandObj) }; }; </code></pre> </article> </section> </div> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.6.7</a> using the <a href="https://github.com/clenemt/docdash">docdash</a> theme. </footer> <script> prettyPrint(); </script> <script src="scripts/polyfill.js"></script> <script src="scripts/linenumber.js"></script> <script src="scripts/search.js" defer></script> </body> </html>