UNPKG

@twyr/announce

Version:

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

338 lines (294 loc) 10.8 kB
/* eslint-disable curly */ /* 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 PublishCommandClass * @classdesc The command class that handles all the publish operations. * * @param {object} mode - Set the current run mode - CLI or API * * @description * The command class that implements the "publish" step of the workflow. * Please see README.md for the details of what this step involves. * */ class PublishCommandClass { // #region Constructor constructor(mode) { this.#execMode = mode; } // #endregion // #region Public Methods /** * @async * @function * @instance * @memberof PublishCommandClass * @name execute * * @param {object} options - Parsed command-line options, or options passed in via API * * @return {null} Nothing. * * @summary The main method to publish the Git Host release to NPM. * * This method does 2 things: * - Gets the URL to the compressed asset for the last/specified release from the Git Host * - Publishes the asset to NPM * */ async execute(options) { // Step 1: Setup sane defaults for the options const mergedOptions = this._mergeOptions(options); // 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(`Publishing the release to NPM:`); await taskList?.run?.({ 'options': mergedOptions, 'execError': null }); } // #endregion // #region Private Methods /** * @function * @instance * @memberof PublishCommandClass * @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); return mergedOptions; } /** * @function * @instance * @memberof PublishCommandClass * @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 PublishCommandClass * @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': 'Initializing Git Client...', 'task': this?._initializeGit?.bind?.(this) }, { 'title': 'Fetching upstream repository info...', 'task': this?._getUpstreamRepositoryInfo?.bind?.(this), 'skip': (ctxt) => { if(ctxt?.options?.git) return false; return `No Git client found.`; } }, { 'title': 'Publishing to npm...', 'task': this?._publishToNpm?.bind?.(this), 'skip': (ctxt) => { if(!ctxt?.options?.npmToken?.trim?.()?.length) return `Cannot publish without an NPM token.`; if(!ctxt?.options?.releaseToBePublished) return `Cannot publish without a release on the Git host.`; if(!ctxt?.options?.releaseToBePublished?.tarball_url) return `Cannot publish a release without a tarball.`; return false; } }], { 'collapse': false }); return taskList; } /** * @function * @instance * @memberof PublishCommandClass * @name _initializeGit * * @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 Creates a Git client instance for the current project repository and sets it on the context. * */ _initializeGit(ctxt, task) { const simpleGit = require('simple-git'); const git = simpleGit?.({ 'baseDir': ctxt?.options?.currentWorkingDirectory }); ctxt?.options?.logger?.info?.(`Initialized Git for the repository @ ${ctxt?.options?.currentWorkingDirectory}`); task.title = `Initialize Git for the repository @ ${ctxt?.options?.currentWorkingDirectory}: Done`; ctxt.options.git = git; } /** * @async * @function * @instance * @memberof PublishCommandClass * @name _getUpstreamRepositoryInfo * * @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 Retrieves the upstream repository information, and sets a POJO with that info into the context. * */ async _getUpstreamRepositoryInfo(ctxt, task) { const gitRemote = await ctxt?.options?.git?.remote?.(['get-url', '--push', ctxt?.options?.upstream]); const hostedGitInfo = require('hosted-git-info'); const repository = hostedGitInfo?.fromUrl?.(gitRemote); repository.project = repository?.project?.replace?.('.git\n', ''); const GitHostWrapper = require(`./../git_host_utilities/${repository?.type}`)?.GitHostWrapper; const gitHostWrapper = new GitHostWrapper(ctxt?.options?.[`${repository?.type}Token`]); const releaseToBePublished = await gitHostWrapper?.fetchReleaseInformation?.(repository, ctxt?.options?.releaseName); if(!releaseToBePublished) throw new Error(`Unknown Release: ${ctxt?.options.releaseName}`); ctxt.options.repository = repository; ctxt.options.releaseToBePublished = releaseToBePublished; ctxt?.options?.logger?.info?.(`Fetch upstream repository info: Done`); task.title = `Fetch upstream repository info: Done`; } /** * @async * @function * @instance * @memberof PublishCommandClass * @name _publishToNpm * * @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 Retrieves the release assets from GitHub, and publishes them to NPM. * */ async _publishToNpm(ctxt, task) { let distTag = null; if((ctxt?.options?.distTag ?? 'version_default') === 'version_default') { if(ctxt?.options?.releaseToBePublished?.prerelease) distTag = 'next'; else distTag = 'latest'; } const publishOptions = ['publish']; publishOptions?.push?.(ctxt?.options?.releaseToBePublished?.tarball_url); publishOptions?.push?.(`--tag ${distTag}`); publishOptions?.push?.(`--access ${ctxt?.options?.access}`); if(ctxt?.options?.dryRun) publishOptions?.push?.('--dry-run'); const execa = require('execa'); const publishProcess = execa?.('npm', publishOptions, { 'all': true }); // publishProcess?.stdout?.pipe?.(process.stdout); // publishProcess?.stderr?.pipe?.(process.stderr); await publishProcess; ctxt?.options?.logger?.info?.(`Publish to NPM: Done`); task.title = `Publish to NPM: Done`; } // #endregion // #region Utility Methods 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 publish = new Commander.Command('publish'); // Get package.json into memory... we'll use it in multiple places here const path = require('path'); const projectPackageJson = path.join((configuration?.publish?.currentWorkingDirectory?.trim?.() ?? process.cwd()), 'package.json'); let pkg = null; try { pkg = require(projectPackageJson); } catch(err) { // Do nothing pkg = null; } if(pkg) { // Get the dynamic template filler - use it for configuration substitution const fillTemplate = require('es6-dynamic-template'); if(configuration?.publish?.currentWorkingDirectory) { configuration.publish.currentWorkingDirectory = fillTemplate?.(configuration?.publish?.currentWorkingDirectory, pkg); } if(configuration?.publish?.releaseName) { configuration.publish.releaseName = fillTemplate?.(configuration?.publish?.releaseName, pkg); } } // Setup the command publish?.alias?.('pub'); publish ?.option?.('--current-working-directory <folder>', 'Path to the current working directory', configuration?.release?.currentWorkingDirectory?.trim?.() ?? process?.cwd?.()) .option('--access <level>', 'Public / Restricted', configuration?.publish?.access?.trim?.() ?? 'public') .option('--dist-tag <tag>', 'Tag to use for the published release', configuration?.publish?.distTag?.trim?.() ?? 'latest') .option('--dry-run', 'Dry run publish', configuration?.publish?.dryRun ?? false) .option('--github-token <token>', 'Token to use for accessing the release on GitHub', configuration?.publish?.githubToken?.trim?.() ?? process.env.GITHUB_TOKEN ?? 'PROCESS.ENV.GITHUB_TOKEN') .option('--gitlab-token <token>', 'Token to use for accessing the release on GitLab', configuration?.publish?.gitlabToken?.trim?.() ?? process.env.GITLAB_TOKEN ?? 'PROCESS.ENV.GITLAB_TOKEN') .option('--npm-token <token>', 'Automation Token to use for publishing the release to NPM', configuration?.publish?.npmToken?.trim?.() ?? process.env.NPM_TOKEN ?? 'PROCESS.ENV.NPM_TOKEN') .option('--release-name <name>', 'Release name on the Git Host for fetching the compressed assets', configuration?.publish?.releaseName?.trim?.() ?? (pkg ? `V${pkg?.version} Release` : 'Release')) .option('--upstream <remote>', 'Git remote to use for accessing the release', configuration?.publish?.upstream ?? 'upstream') ; const commandObj = new PublishCommandClass('cli'); publish?.action?.(commandObj?.execute?.bind?.(commandObj)); // Add it to the mix commanderProcess?.addCommand?.(publish); return; }; // Export the API for usage by downstream programs exports.apiCreator = function apiCreator() { const commandObj = new PublishCommandClass('api'); return { 'name': 'publish', 'method': commandObj.execute.bind(commandObj) }; };