diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 28f5aa6c..2d58c737 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,14 +10,12 @@ jobs: fail-fast: false matrix: node-version: - - 14 - - 12 - - 10 + - 19 + - 18 + - 16 steps: - - uses: actions/checkout@v2 - with: - submodules: true - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: git config --global user.name "Github Actions" diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 18f98e71..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "integration-test"] - path = integration-test - url = https://github.com/bunysae/np_integration_test diff --git a/integration-test b/integration-test deleted file mode 160000 index b2492e19..00000000 --- a/integration-test +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b2492e190f3864f96955a7acb4349bb9722530b9 diff --git a/private-packages.png b/media/private-packages.png similarity index 100% rename from private-packages.png rename to media/private-packages.png diff --git a/screenshot-ui.png b/media/screenshot-ui.png similarity index 100% rename from screenshot-ui.png rename to media/screenshot-ui.png diff --git a/screenshot.gif b/media/screenshot.gif similarity index 100% rename from screenshot.gif rename to media/screenshot.gif diff --git a/package.json b/package.json index 22158766..219d79d7 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,16 @@ "license": "MIT", "repository": "sindresorhus/np", "funding": "https://github.com/sindresorhus/np?sponsor=1", + "type": "module", "bin": "source/cli.js", "engines": { - "node": ">=10", - "npm": ">=6.8.0", + "node": ">=16.6.0", + "npm": ">=7.19.0", "git": ">=2.11.0", "yarn": ">=1.7.0" }, "scripts": { - "test": "xo && FORCE_HYPERLINK=1 ava" + "test": "xo && ava && ava test/integration.js --no-worker-threads" }, "files": [ "source" @@ -30,63 +31,60 @@ "commit" ], "dependencies": { - "@samverschueren/stream-to-observable": "^0.3.1", - "any-observable": "^0.5.1", - "async-exit-hook": "^2.0.1", - "chalk": "^4.1.0", - "cosmiconfig": "^7.0.0", - "del": "^6.0.0", - "escape-goat": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "execa": "^5.0.0", + "chalk": "^5.2.0", + "cosmiconfig": "^8.1.3", + "del": "^7.0.0", + "escape-goat": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "execa": "^7.1.1", + "exit-hook": "^3.2.0", "github-url-from-git": "^1.5.0", - "has-yarn": "^2.1.0", - "hosted-git-info": "^3.0.7", - "ignore-walk": "^3.0.3", - "import-local": "^3.0.2", - "inquirer": "^7.3.3", - "is-installed-globally": "^0.3.2", - "is-interactive": "^1.0.0", - "is-scoped": "^2.1.0", - "issue-regex": "^3.1.0", + "has-yarn": "^3.0.0", + "hosted-git-info": "^6.1.1", + "ignore-walk": "^6.0.2", + "import-local": "^3.1.0", + "inquirer": "^9.1.5", + "is-installed-globally": "^0.4.0", + "is-interactive": "^2.0.0", + "is-scoped": "^3.0.0", + "issue-regex": "^4.1.0", "listr": "^0.14.3", "listr-input": "^0.2.1", - "log-symbols": "^4.0.0", - "meow": "^8.1.0", - "minimatch": "^3.0.4", - "new-github-release-url": "^1.0.0", - "npm-name": "^6.0.1", - "onetime": "^5.1.2", - "open": "^7.3.0", - "ow": "^0.21.0", - "p-memoize": "^4.0.1", - "p-timeout": "^4.1.0", - "pkg-dir": "^5.0.0", - "read-pkg-up": "^7.0.1", - "rxjs": "^6.6.3", - "semver": "^7.3.4", - "split": "^1.0.1", - "symbol-observable": "^3.0.0", - "terminal-link": "^2.1.1", - "update-notifier": "^5.0.1" + "log-symbols": "^5.1.0", + "meow": "^11.0.0", + "minimatch": "^8.0.2", + "new-github-release-url": "^2.0.0", + "npm-name": "^7.1.0", + "onetime": "^6.0.0", + "open": "^9.1.0", + "ow": "^1.1.1", + "p-memoize": "^7.1.1", + "p-timeout": "^6.1.1", + "pkg-dir": "^7.0.0", + "read-pkg-up": "^9.1.0", + "rxjs": "^7.8.0", + "semver": "^7.3.8", + "symbol-observable": "^4.0.0", + "terminal-link": "^3.0.0", + "update-notifier": "^6.0.2" }, "devDependencies": { - "ava": "^2.3.0", - "execa_test_double": "^4.0.1", - "mockery": "^2.1.0", - "proxyquire": "^2.1.3", - "sinon": "^9.2.2", - "xo": "^0.36.1" + "ava": "^5.2.0", + "common-tags": "^1.8.2", + "esmock": "^2.2.0", + "fs-extra": "^11.1.1", + "sinon": "^15.0.3", + "xo": "^0.53.1" }, "ava": { + "environmentVariables": { + "FORCE_HYPERLINK": "1" + }, "files": [ - "!test/fixtures", - "!integration-test" - ] - }, - "xo": { - "ignores": [ - "integration-test" + "!test/integration.js" + ], + "nodeArguments": [ + "--loader=esmock" ] } } diff --git a/readme.md b/readme.md index de2d6736..f04dcf7a 100644 --- a/readme.md +++ b/readme.md @@ -22,7 +22,7 @@ --- - + ## Why @@ -54,8 +54,8 @@ ## Prerequisite -- Node.js 10 or later -- npm 6.8.0 or later +- Node.js 16 or later +- npm 7.19.0 or later - Git 2.11 or later ## Install @@ -104,11 +104,11 @@ $ np --help Run `np` without arguments to launch the interactive UI that guides you through publishing a new version. - + ## Config -`np` can be configured both locally and globally. When using the global `np` binary, you can configure any of the CLI flags in either a `.np-config.js`, `.np-config.cjs` or `.np-config.json` file in the home directory. When using the local `np` binary, for example, in a `npm run` script, you can configure `np` by setting the flags in either a top-level `np` field in `package.json` or in a `.np-config.js`, `.np-config.cjs` or `.np-config.json` file in the project directory. If it exists, the local installation will always take precedence. This ensures any local config matches the version of `np` it was designed for. +`np` can be configured both globally and locally. When using the global `np` binary, you can configure any of the CLI flags in either a `.np-config.js` (as CJS), `.np-config.cjs`, `.np-config.mjs`, or `.np-config.json` file in the home directory. When using the local `np` binary, for example, in a `npm run` script, you can configure `np` by setting the flags in either a top-level `np` field in `package.json` or in one of the aforementioned file types in the project directory. If it exists, the local installation will always take precedence. This ensures any local config matches the version of `np` it was designed for. Currently, these are the flags you can configure: @@ -156,6 +156,14 @@ module.exports = { }; ``` +`.np-config.mjs` +```js +export default { + yarn: false, + contents: 'dist' +}; +``` + _**Note:** The global config only applies when using the global `np` binary, and is never inherited when using a local binary._ ## Tips @@ -226,7 +234,7 @@ $ yarn config set version-sign-git-tag true ### Private packages - + You can use `np` for packages that aren't publicly published to npm (perhaps installed from a private git repo). diff --git a/source/cli-implementation.js b/source/cli-implementation.js index 2e541542..3b394f92 100755 --- a/source/cli-implementation.js +++ b/source/cli-implementation.js @@ -1,25 +1,25 @@ #!/usr/bin/env node -'use strict'; // eslint-disable-next-line import/no-unassigned-import -require('symbol-observable'); // Important: This needs to be first to prevent weird Observable incompatibilities -const logSymbols = require('log-symbols'); -const meow = require('meow'); -const updateNotifier = require('update-notifier'); -const hasYarn = require('has-yarn'); -const config = require('./config'); -const git = require('./git-util'); -const {isPackageNameAvailable} = require('./npm/util'); -const version = require('./version'); -const util = require('./util'); -const ui = require('./ui'); -const np = require('.'); +import 'symbol-observable'; // Important: This needs to be first to prevent weird Observable incompatibilities +import logSymbols from 'log-symbols'; +import meow from 'meow'; +import updateNotifier from 'update-notifier'; +import hasYarn from 'has-yarn'; +import {gracefulExit} from 'exit-hook'; +import config from './config.js'; +import * as git from './git-util.js'; +import {isPackageNameAvailable} from './npm/util.js'; +import Version from './version.js'; +import * as util from './util.js'; +import ui from './ui.js'; +import np from './index.js'; const cli = meow(` Usage $ np Version can be: - ${version.SEMVER_INCREMENTS.join(' | ')} | 1.2.3 + ${Version.SEMVER_INCREMENTS.join(' | ')} | 1.2.3 Options --any-branch Allow publishing from any branch @@ -45,60 +45,61 @@ const cli = meow(` $ np 1.0.2-beta.3 --tag=beta $ np 1.0.2-beta.3 --tag=beta --contents=dist `, { + importMeta: import.meta, booleanDefault: undefined, flags: { anyBranch: { - type: 'boolean' + type: 'boolean', }, branch: { - type: 'string' + type: 'string', }, cleanup: { - type: 'boolean' + type: 'boolean', }, tests: { - type: 'boolean' + type: 'boolean', }, yolo: { - type: 'boolean' + type: 'boolean', }, publish: { - type: 'boolean' + type: 'boolean', }, releaseDraft: { - type: 'boolean' + type: 'boolean', }, releaseDraftOnly: { - type: 'boolean' + type: 'boolean', }, tag: { - type: 'string' + type: 'string', }, yarn: { - type: 'boolean' + type: 'boolean', }, contents: { - type: 'string' + type: 'string', }, preview: { - type: 'boolean' + type: 'boolean', }, testScript: { - type: 'string' + type: 'string', }, '2fa': { - type: 'boolean' + type: 'boolean', }, message: { - type: 'string' - } - } + type: 'string', + }, + }, }); updateNotifier({pkg: cli.pkg}).notify(); -(async () => { - const {pkg, pkgPath} = util.readPkg(); +try { + const {pkg, pkgPath} = await util.readPkg(); const defaultFlags = { cleanup: true, @@ -106,7 +107,7 @@ updateNotifier({pkg: cli.pkg}).notify(); publish: true, releaseDraft: true, yarn: hasYarn(), - '2fa': true + '2fa': true, }; const localConfig = await config(); @@ -114,7 +115,7 @@ updateNotifier({pkg: cli.pkg}).notify(); const flags = { ...defaultFlags, ...localConfig, - ...cli.flags + ...cli.flags, }; // Workaround for unintended auto-casing behavior from `meow`. @@ -126,7 +127,7 @@ updateNotifier({pkg: cli.pkg}).notify(); const availability = flags.publish ? await isPackageNameAvailable(pkg) : { isAvailable: false, - isUnknown: false + isUnknown: false, }; // Use current (latest) version when 'releaseDraftOnly', otherwise use the first argument. @@ -138,22 +139,22 @@ updateNotifier({pkg: cli.pkg}).notify(); availability, version, runPublish, - branch + branch, }, {pkg, pkgPath}); if (!options.confirm) { - process.exit(0); + gracefulExit(); } console.log(); // Prints a newline for readability const newPkg = await np(options.version, options); if (options.preview || options.releaseDraftOnly) { - return; + gracefulExit(); } console.log(`\n ${newPkg.name} ${newPkg.version} published 🎉`); -})().catch(error => { +} catch (error) { console.error(`\n${logSymbols.error} ${error.message}`); - process.exit(1); -}); + gracefulExit(1); +} diff --git a/source/cli.js b/source/cli.js index f79dcc86..8c71be61 100755 --- a/source/cli.js +++ b/source/cli.js @@ -1,9 +1,10 @@ #!/usr/bin/env node -'use strict'; -const {debuglog} = require('util'); -const importLocal = require('import-local'); -const isInstalledGlobally = require('is-installed-globally'); +import {fileURLToPath} from 'node:url'; +import {debuglog} from 'node:util'; +import importLocal from 'import-local'; +import isInstalledGlobally from 'is-installed-globally'; +const __filename = fileURLToPath(import.meta.url); const log = debuglog('np'); // Prefer the local installation @@ -12,6 +13,5 @@ if (!importLocal(__filename)) { log('Using global install of np.'); } - // eslint-disable-next-line import/no-unassigned-import - require('./cli-implementation'); + await import('./cli-implementation.js'); } diff --git a/source/config.js b/source/config.js index b88fcfd3..cd78f7b8 100644 --- a/source/config.js +++ b/source/config.js @@ -1,21 +1,32 @@ -'use strict'; -const os = require('os'); -const isInstalledGlobally = require('is-installed-globally'); -const pkgDir = require('pkg-dir'); -const {cosmiconfig} = require('cosmiconfig'); +import os from 'node:os'; +import isInstalledGlobally from 'is-installed-globally'; +import {packageDirectory} from 'pkg-dir'; +import {cosmiconfig} from 'cosmiconfig'; -module.exports = async () => { - const searchDir = isInstalledGlobally ? os.homedir() : await pkgDir(); - const searchPlaces = ['.np-config.json', '.np-config.js', '.np-config.cjs']; +// TODO: remove when cosmiconfig/cosmiconfig#283 lands +const loadESM = async filepath => { + const module = await import(filepath); + return module.default ?? module; +}; + +const getConfig = async () => { + const searchDir = isInstalledGlobally ? os.homedir() : await packageDirectory(); + const searchPlaces = ['.np-config.json', '.np-config.js', '.np-config.cjs', '.np-config.mjs']; if (!isInstalledGlobally) { searchPlaces.push('package.json'); } const explorer = cosmiconfig('np', { searchPlaces, - stopDir: searchDir + stopDir: searchDir, + loaders: { + '.js': loadESM, + '.mjs': loadESM, + }, }); const {config} = (await explorer.search(searchDir)) || {}; return config; }; + +export default getConfig; diff --git a/source/git-tasks.js b/source/git-tasks.js index 8f7376f6..8106685a 100644 --- a/source/git-tasks.js +++ b/source/git-tasks.js @@ -1,21 +1,20 @@ -'use strict'; -const Listr = require('listr'); -const git = require('./git-util'); +import Listr from 'listr'; +import * as git from './git-util.js'; -module.exports = options => { +const gitTasks = options => { const tasks = [ { title: 'Check current branch', - task: () => git.verifyCurrentBranchIsReleaseBranch(options.branch) + task: () => git.verifyCurrentBranchIsReleaseBranch(options.branch), }, { title: 'Check local working tree', - task: () => git.verifyWorkingTreeIsClean() + task: () => git.verifyWorkingTreeIsClean(), }, { title: 'Check remote history', - task: () => git.verifyRemoteHistoryIsClean() - } + task: () => git.verifyRemoteHistoryIsClean(), + }, ]; if (options.anyBranch) { @@ -24,3 +23,5 @@ module.exports = options => { return new Listr(tasks); }; + +export default gitTasks; diff --git a/source/git-util.js b/source/git-util.js index 693c88dd..917655ea 100644 --- a/source/git-util.js +++ b/source/git-util.js @@ -1,24 +1,23 @@ -'use strict'; -const path = require('path'); -const execa = require('execa'); -const escapeStringRegexp = require('escape-string-regexp'); -const ignoreWalker = require('ignore-walk'); -const pkgDir = require('pkg-dir'); -const {verifyRequirementSatisfied} = require('./version'); - -exports.latestTag = async () => { +import path from 'node:path'; +import {execa} from 'execa'; +import escapeStringRegexp from 'escape-string-regexp'; +import ignoreWalker from 'ignore-walk'; +import {packageDirectorySync} from 'pkg-dir'; +import Version from './version.js'; + +export const latestTag = async () => { const {stdout} = await execa('git', ['describe', '--abbrev=0', '--tags']); return stdout; }; -exports.root = async () => { +export const root = async () => { const {stdout} = await execa('git', ['rev-parse', '--show-toplevel']); return stdout; }; -exports.newFilesSinceLastRelease = async () => { +export const newFilesSinceLastRelease = async () => { try { - const {stdout} = await execa('git', ['diff', '--name-only', '--diff-filter=A', await this.latestTag(), 'HEAD']); + const {stdout} = await execa('git', ['diff', '--name-only', '--diff-filter=A', await latestTag(), 'HEAD']); if (stdout.trim().length === 0) { return []; } @@ -28,15 +27,15 @@ exports.newFilesSinceLastRelease = async () => { } catch { // Get all files under version control return ignoreWalker({ - path: pkgDir.sync(), - ignoreFiles: ['.gitignore'] + path: packageDirectorySync(), + ignoreFiles: ['.gitignore'], }); } }; -exports.readFileFromLastRelease = async file => { - const filePathFromRoot = path.relative(await exports.root(), file); - const {stdout: oldFile} = await execa('git', ['show', `${await this.latestTag()}:${filePathFromRoot}`]); +export const readFileFromLastRelease = async file => { + const filePathFromRoot = path.relative(await root(), file); + const {stdout: oldFile} = await execa('git', ['show', `${await latestTag()}:${filePathFromRoot}`]); return oldFile; }; @@ -45,8 +44,8 @@ const firstCommit = async () => { return stdout; }; -exports.previousTagOrFirstCommit = async () => { - const tags = await exports.tagList(); +export const previousTagOrFirstCommit = async () => { + const tags = await tagList(); if (tags.length === 0) { return; @@ -58,7 +57,7 @@ exports.previousTagOrFirstCommit = async () => { try { // Return the tag before the latest one. - const latest = await exports.latestTag(); + const latest = await latestTag(); const index = tags.indexOf(latest); return tags[index - 1]; } catch { @@ -67,11 +66,11 @@ exports.previousTagOrFirstCommit = async () => { } }; -exports.latestTagOrFirstCommit = async () => { +export const latestTagOrFirstCommit = async () => { let latest; try { // In case a previous tag exists, we use it to compare the current repo status to. - latest = await exports.latestTag(); + latest = await latestTag(); } catch { // Otherwise, we fallback to using the first commit for comparison. latest = await firstCommit(); @@ -80,32 +79,32 @@ exports.latestTagOrFirstCommit = async () => { return latest; }; -exports.hasUpstream = async () => { - const escapedCurrentBranch = escapeStringRegexp(await exports.currentBranch()); +export const hasUpstream = async () => { + const escapedCurrentBranch = escapeStringRegexp(await getCurrentBranch()); const {stdout} = await execa('git', ['status', '--short', '--branch', '--porcelain']); return new RegExp(String.raw`^## ${escapedCurrentBranch}\.\.\..+\/${escapedCurrentBranch}`).test(stdout); }; -exports.currentBranch = async () => { +export const getCurrentBranch = async () => { const {stdout} = await execa('git', ['symbolic-ref', '--short', 'HEAD']); return stdout; }; -exports.verifyCurrentBranchIsReleaseBranch = async releaseBranch => { - const currentBranch = await exports.currentBranch(); +export const verifyCurrentBranchIsReleaseBranch = async releaseBranch => { + const currentBranch = await getCurrentBranch(); if (currentBranch !== releaseBranch) { throw new Error(`Not on \`${releaseBranch}\` branch. Use --any-branch to publish anyway, or set a different release branch using --branch.`); } }; -exports.tagList = async () => { +export const tagList = async () => { // Returns the list of tags, sorted by creation date in ascending order. const {stdout} = await execa('git', ['tag', '--sort=creatordate']); return stdout.split('\n'); }; -exports.isHeadDetached = async () => { +export const isHeadDetached = async () => { try { // Command will fail with code 1 if the HEAD is detached. await execa('git', ['symbolic-ref', '--quiet', 'HEAD']); @@ -115,7 +114,7 @@ exports.isHeadDetached = async () => { } }; -exports.isWorkingTreeClean = async () => { +export const isWorkingTreeClean = async () => { try { const {stdout: status} = await execa('git', ['status', '--porcelain']); if (status !== '') { @@ -128,13 +127,13 @@ exports.isWorkingTreeClean = async () => { } }; -exports.verifyWorkingTreeIsClean = async () => { - if (!(await exports.isWorkingTreeClean())) { +export const verifyWorkingTreeIsClean = async () => { + if (!(await isWorkingTreeClean())) { throw new Error('Unclean working tree. Commit or stash changes first.'); } }; -exports.isRemoteHistoryClean = async () => { +export const isRemoteHistoryClean = async () => { let history; try { // Gracefully handle no remote set up. const {stdout} = await execa('git', ['rev-list', '--count', '--left-only', '@{u}...HEAD']); @@ -148,13 +147,13 @@ exports.isRemoteHistoryClean = async () => { return true; }; -exports.verifyRemoteHistoryIsClean = async () => { - if (!(await exports.isRemoteHistoryClean())) { +export const verifyRemoteHistoryIsClean = async () => { + if (!(await isRemoteHistoryClean())) { throw new Error('Remote history differs. Please pull changes.'); } }; -exports.verifyRemoteIsValid = async () => { +export const verifyRemoteIsValid = async () => { try { await execa('git', ['ls-remote', 'origin', 'HEAD']); } catch (error) { @@ -162,11 +161,11 @@ exports.verifyRemoteIsValid = async () => { } }; -exports.fetch = async () => { +export const fetch = async () => { await execa('git', ['fetch']); }; -exports.tagExistsOnRemote = async tagName => { +export const tagExistsOnRemote = async tagName => { try { const {stdout: revInfo} = await execa('git', ['rev-parse', '--quiet', '--verify', `refs/tags/${tagName}`]); @@ -192,7 +191,7 @@ async function hasLocalBranch(branch) { 'show-ref', '--verify', '--quiet', - `refs/heads/${branch}` + `refs/heads/${branch}`, ]); return true; } catch { @@ -200,7 +199,7 @@ async function hasLocalBranch(branch) { } } -exports.defaultBranch = async () => { +export const defaultBranch = async () => { for (const branch of ['main', 'master', 'gh-pages']) { // eslint-disable-next-line no-await-in-loop if (await hasLocalBranch(branch)) { @@ -209,24 +208,24 @@ exports.defaultBranch = async () => { } throw new Error( - 'Could not infer the default Git branch. Please specify one with the --branch flag or with a np config.' + 'Could not infer the default Git branch. Please specify one with the --branch flag or with a np config.', ); }; -exports.verifyTagDoesNotExistOnRemote = async tagName => { - if (await exports.tagExistsOnRemote(tagName)) { +export const verifyTagDoesNotExistOnRemote = async tagName => { + if (await tagExistsOnRemote(tagName)) { throw new Error(`Git tag \`${tagName}\` already exists.`); } }; -exports.commitLogFromRevision = async revision => { +export const commitLogFromRevision = async revision => { const {stdout} = await execa('git', ['log', '--format=%s %h', `${revision}..HEAD`]); return stdout; }; -exports.pushGraceful = async remoteIsOnGitHub => { +export const pushGraceful = async remoteIsOnGitHub => { try { - await exports.push(); + await push(); } catch (error) { if (remoteIsOnGitHub && error.stderr && error.stderr.includes('GH006')) { // Try to push tags only, when commits can't be pushed due to branch protection @@ -238,15 +237,15 @@ exports.pushGraceful = async remoteIsOnGitHub => { } }; -exports.push = async () => { +export const push = async () => { await execa('git', ['push', '--follow-tags']); }; -exports.deleteTag = async tagName => { +export const deleteTag = async tagName => { await execa('git', ['tag', '--delete', tagName]); }; -exports.removeLastCommit = async () => { +export const removeLastCommit = async () => { await execa('git', ['reset', '--hard', 'HEAD~1']); }; @@ -256,13 +255,13 @@ const gitVersion = async () => { return match && match.groups.version; }; -exports.verifyRecentGitVersion = async () => { +export const verifyRecentGitVersion = async () => { const installedVersion = await gitVersion(); - verifyRequirementSatisfied('git', installedVersion); + Version.verifyRequirementSatisfied('git', installedVersion); }; -exports.checkIfFileGitIgnored = async pathToFile => { +export const checkIfFileGitIgnored = async pathToFile => { try { const {stdout} = await execa('git', ['check-ignore', pathToFile]); return Boolean(stdout); diff --git a/source/index.js b/source/index.js index 7d25820c..a3663b01 100644 --- a/source/index.js +++ b/source/index.js @@ -1,43 +1,34 @@ -'use strict'; -require('any-observable/register/rxjs-all'); -const fs = require('fs'); -const path = require('path'); -const execa = require('execa'); -const del = require('del'); -const Listr = require('listr'); -const split = require('split'); -const {merge, throwError} = require('rxjs'); -const {catchError, filter, finalize} = require('rxjs/operators'); -const streamToObservable = require('@samverschueren/stream-to-observable'); -const readPkgUp = require('read-pkg-up'); -const hasYarn = require('has-yarn'); -const pkgDir = require('pkg-dir'); -const hostedGitInfo = require('hosted-git-info'); -const onetime = require('onetime'); -const exitHook = require('async-exit-hook'); -const logSymbols = require('log-symbols'); -const prerequisiteTasks = require('./prerequisite-tasks'); -const gitTasks = require('./git-tasks'); -const publish = require('./npm/publish'); -const enable2fa = require('./npm/enable-2fa'); -const npm = require('./npm/util'); -const releaseTaskHelper = require('./release-task-helper'); -const util = require('./util'); -const git = require('./git-util'); +import fs from 'node:fs'; +import path from 'node:path'; +import {execa} from 'execa'; +import {deleteAsync} from 'del'; +import Listr from 'listr'; +import {merge, throwError, catchError, filter, finalize} from 'rxjs'; +import {readPackageUp} from 'read-pkg-up'; +import hasYarn from 'has-yarn'; +import {packageDirectorySync} from 'pkg-dir'; +import hostedGitInfo from 'hosted-git-info'; +import onetime from 'onetime'; +import {asyncExitHook} from 'exit-hook'; +import logSymbols from 'log-symbols'; +import prerequisiteTasks from './prerequisite-tasks.js'; +import gitTasks from './git-tasks.js'; +import publish from './npm/publish.js'; +import enable2fa from './npm/enable-2fa.js'; +import * as npm from './npm/util.js'; +import releaseTaskHelper from './release-task-helper.js'; +import * as util from './util.js'; +import * as git from './git-util.js'; const exec = (cmd, args) => { // Use `Observable` support if merged https://github.com/sindresorhus/execa/pull/26 const cp = execa(cmd, args); - return merge( - streamToObservable(cp.stdout.pipe(split())), - streamToObservable(cp.stderr.pipe(split())), - cp - ).pipe(filter(Boolean)); + return merge(cp.stdout, cp.stderr, cp).pipe(filter(Boolean)); }; -// eslint-disable-next-line default-param-last -module.exports = async (input = 'patch', options) => { +// eslint-disable-next-line complexity +const np = async (input = 'patch', options) => { if (!hasYarn() && options.yarn) { throw new Error('Could not use Yarn without yarn.lock file'); } @@ -47,14 +38,14 @@ module.exports = async (input = 'patch', options) => { options.cleanup = false; } - const {pkg} = util.readPkg(options.contents); + const {pkg} = await util.readPkg(options.contents); const runTests = options.tests && !options.yolo; const runCleanup = options.cleanup && !options.yolo; const pkgManager = options.yarn === true ? 'yarn' : 'npm'; const pkgManagerName = options.yarn === true ? 'Yarn' : 'npm'; - const rootDir = pkgDir.sync(); + const rootDir = packageDirectorySync(); const hasLockFile = fs.existsSync(path.resolve(rootDir, options.yarn ? 'yarn.lock' : 'package-lock.json')) || fs.existsSync(path.resolve(rootDir, 'npm-shrinkwrap.json')); - const isOnGitHub = options.repoUrl && (hostedGitInfo.fromUrl(options.repoUrl) || {}).type === 'github'; + const isOnGitHub = options.repoUrl && hostedGitInfo.fromUrl(options.repoUrl)?.type === 'github'; const testScript = options.testScript || 'test'; const testCommand = options.testScript ? ['run', testScript] : [testScript]; @@ -75,8 +66,8 @@ module.exports = async (input = 'patch', options) => { const versionInLatestTag = latestTag.slice(tagVersionPrefix.length); try { - if (versionInLatestTag === util.readPkg().pkg.version && - versionInLatestTag !== pkg.version) { // Verify that the package's version has been bumped before deleting the last tag and commit. + // Verify that the package's version has been bumped before deleting the last tag and commit. + if (versionInLatestTag === util.readPkg().version && versionInLatestTag !== pkg.version) { await git.deleteTag(latestTag); await git.removeLastCommit(); } @@ -87,35 +78,31 @@ module.exports = async (input = 'patch', options) => { } }); - // The default parameter is a workaround for https://github.com/Tapppi/async-exit-hook/issues/9 - exitHook((callback = () => {}) => { - if (options.preview) { - callback(); - } else if (publishStatus === 'FAILED') { - (async () => { - await rollback(); - callback(); - })(); - } else if (publishStatus === 'SUCCESS') { - callback(); + asyncExitHook(async () => { + if (options.preview || publishStatus === 'SUCCESS') { + return; + } + + if (publishStatus === 'FAILED') { + await rollback(); } else { console.log('\nAborted!'); - callback(); } - }); + }, {minimumWait: 2000}); const tasks = new Listr([ { title: 'Prerequisite check', enabled: () => options.runPublish, - task: () => prerequisiteTasks(input, pkg, options) + task: () => prerequisiteTasks(input, pkg, options), }, { title: 'Git', - task: () => gitTasks(options) - } + task: () => gitTasks(options), + }, ], { - showSubtasks: false + showSubtasks: false, + renderer: options.renderer ?? 'default', }); if (runCleanup) { @@ -123,13 +110,13 @@ module.exports = async (input = 'patch', options) => { { title: 'Cleanup', enabled: () => !hasLockFile, - task: () => del('node_modules') + task: () => deleteAsync('node_modules'), }, { title: 'Installing dependencies using Yarn', enabled: () => options.yarn === true, - task: () => { - return exec('yarn', ['install', '--frozen-lockfile', '--production=false']).pipe( + task: () => ( + exec('yarn', ['install', '--frozen-lockfile', '--production=false']).pipe( catchError(async error => { if ((!error.stderr.startsWith('error Your lockfile needs to be updated'))) { return; @@ -140,18 +127,18 @@ module.exports = async (input = 'patch', options) => { } throw new Error('yarn.lock file is outdated. Run yarn, commit the updated lockfile and try again.'); - }) - ); - } + }), + ) + ), }, { title: 'Installing dependencies using npm', enabled: () => options.yarn === false, - task: () => { + task() { const args = hasLockFile ? ['ci'] : ['install', '--no-package-lock', '--no-production']; return exec('npm', [...args, '--engine-strict']); - } - } + }, + }, ]); } @@ -160,7 +147,7 @@ module.exports = async (input = 'patch', options) => { { title: 'Running tests using npm', enabled: () => options.yarn === false, - task: () => exec('npm', testCommand) + task: () => exec('npm', testCommand), }, { title: 'Running tests using Yarn', @@ -171,10 +158,10 @@ module.exports = async (input = 'patch', options) => { return []; } - return throwError(error); - }) - ) - } + return throwError(() => error); + }), + ), + }, ]); } @@ -182,7 +169,7 @@ module.exports = async (input = 'patch', options) => { { title: 'Bumping version using Yarn', enabled: () => options.yarn === true, - skip: () => { + skip() { if (options.preview) { let previewText = `[Preview] Command not executed: yarn version --new-version ${input}`; @@ -193,7 +180,7 @@ module.exports = async (input = 'patch', options) => { return `${previewText}.`; } }, - task: () => { + task() { const args = ['version', '--new-version', input]; if (options.message) { @@ -201,12 +188,12 @@ module.exports = async (input = 'patch', options) => { } return exec('yarn', args); - } + }, }, { title: 'Bumping version using npm', enabled: () => options.yarn === false, - skip: () => { + skip() { if (options.preview) { let previewText = `[Preview] Command not executed: npm version ${input}`; @@ -217,7 +204,7 @@ module.exports = async (input = 'patch', options) => { return `${previewText}.`; } }, - task: () => { + task() { const args = ['version', input]; if (options.message) { @@ -225,21 +212,21 @@ module.exports = async (input = 'patch', options) => { } return exec('npm', args); - } - } + }, + }, ]); if (options.runPublish) { tasks.add([ { title: `Publishing package using ${pkgManagerName}`, - skip: () => { + skip() { if (options.preview) { const args = publish.getPackagePublishArguments(options); return `[Preview] Command not executed: ${pkgManager} ${args.join(' ')}.`; } }, - task: (context, task) => { + task(context, task) { let hasError = false; return publish(context, pkgManager, task, options) @@ -251,10 +238,10 @@ module.exports = async (input = 'patch', options) => { }), finalize(() => { publishStatus = hasError ? 'FAILED' : 'SUCCESS'; - }) + }), ); - } - } + }, + }, ]); const isExternalRegistry = npm.isExternalRegistry(pkg); @@ -262,14 +249,14 @@ module.exports = async (input = 'patch', options) => { tasks.add([ { title: 'Enabling two-factor authentication', - skip: () => { + skip() { if (options.preview) { const args = enable2fa.getEnable2faArgs(pkg.name, options); return `[Preview] Command not executed: npm ${args.join(' ')}.`; } }, - task: (context, task) => enable2fa(task, pkg.name, {otp: context.otp}) - } + task: (context, task) => enable2fa(task, pkg.name, {otp: context.otp}), + }, ]); } } else { @@ -278,7 +265,7 @@ module.exports = async (input = 'patch', options) => { tasks.add({ title: 'Pushing tags', - skip: async () => { + async skip() { if (!(await git.hasUpstream())) { return 'Upstream branch not found; not pushing.'; } @@ -291,21 +278,21 @@ module.exports = async (input = 'patch', options) => { return 'Couldn\'t publish package to npm; not pushing.'; } }, - task: async () => { + async task() { pushedObjects = await git.pushGraceful(isOnGitHub); - } + }, }); if (options.releaseDraft) { tasks.add({ title: 'Creating release draft on GitHub', enabled: () => isOnGitHub === true, - skip: () => { + skip() { if (options.preview) { return '[Preview] GitHub Releases draft will not be opened in preview mode.'; } }, - task: () => releaseTaskHelper(options, pkg) + task: () => releaseTaskHelper(options, pkg), }); } @@ -315,6 +302,8 @@ module.exports = async (input = 'patch', options) => { console.error(`\n${logSymbols.error} ${pushedObjects.reason}`); } - const {packageJson: newPkg} = await readPkgUp(); + const {packageJson: newPkg} = await readPackageUp(); return newPkg; }; + +export default np; diff --git a/source/npm/enable-2fa.js b/source/npm/enable-2fa.js index a9e2ef34..03c29ff5 100644 --- a/source/npm/enable-2fa.js +++ b/source/npm/enable-2fa.js @@ -1,10 +1,8 @@ -'use strict'; -const execa = require('execa'); -const {from} = require('rxjs'); -const {catchError} = require('rxjs/operators'); -const handleNpmError = require('./handle-npm-error'); +import {execa} from 'execa'; +import {from, catchError} from 'rxjs'; +import handleNpmError from './handle-npm-error.js'; -const getEnable2faArgs = (packageName, options) => { +export const getEnable2faArgs = (packageName, options) => { const args = ['access', '2fa-required', packageName]; if (options && options.otp) { @@ -16,9 +14,10 @@ const getEnable2faArgs = (packageName, options) => { const enable2fa = (packageName, options) => execa('npm', getEnable2faArgs(packageName, options)); -module.exports = (task, packageName, options) => +const tryEnable2fa = (task, packageName, options) => { from(enable2fa(packageName, options)).pipe( - catchError(error => handleNpmError(error, task, otp => enable2fa(packageName, {otp}))) + catchError(error => handleNpmError(error, task, otp => enable2fa(packageName, {otp}))), ); +}; -module.exports.getEnable2faArgs = getEnable2faArgs; +export default tryEnable2fa; diff --git a/source/npm/handle-npm-error.js b/source/npm/handle-npm-error.js index fa191c51..7ec39c88 100644 --- a/source/npm/handle-npm-error.js +++ b/source/npm/handle-npm-error.js @@ -1,7 +1,6 @@ -const listrInput = require('listr-input'); -const chalk = require('chalk'); -const {throwError} = require('rxjs'); -const {catchError} = require('rxjs/operators'); +import listrInput from 'listr-input'; +import chalk from 'chalk'; +import {throwError, catchError} from 'rxjs'; const handleNpmError = (error, task, message, executor) => { if (typeof message === 'function') { @@ -15,13 +14,13 @@ const handleNpmError = (error, task, message, executor) => { task.title = `${title} ${chalk.yellow('(waiting for input…)')}`; return listrInput('Enter OTP:', { - done: otp => { + done(otp) { task.title = title; return executor(otp); }, - autoSubmit: value => value.length === 6 + autoSubmit: value => value.length === 6, }).pipe( - catchError(error => handleNpmError(error, task, 'OTP was incorrect, try again:', executor)) + catchError(error => handleNpmError(error, task, 'OTP was incorrect, try again:', executor)), ); } @@ -31,7 +30,7 @@ const handleNpmError = (error, task, message, executor) => { throw new Error('You cannot publish a privately scoped package without a paid plan. Did you mean to publish publicly?'); } - return throwError(error); + return throwError(() => error); }; -module.exports = handleNpmError; +export default handleNpmError; diff --git a/source/npm/publish.js b/source/npm/publish.js index 0a4ed4ee..85a73ddb 100644 --- a/source/npm/publish.js +++ b/source/npm/publish.js @@ -1,10 +1,8 @@ -'use strict'; -const execa = require('execa'); -const {from} = require('rxjs'); -const {catchError} = require('rxjs/operators'); -const handleNpmError = require('./handle-npm-error'); +import {execa} from 'execa'; +import {from, catchError} from 'rxjs'; +import handleNpmError from './handle-npm-error.js'; -const getPackagePublishArguments = options => { +export const getPackagePublishArguments = options => { const args = ['publish']; if (options.contents) { @@ -28,13 +26,14 @@ const getPackagePublishArguments = options => { const pkgPublish = (pkgManager, options) => execa(pkgManager, getPackagePublishArguments(options)); -module.exports = (context, pkgManager, task, options) => +const publish = (context, pkgManager, task, options) => { from(pkgPublish(pkgManager, options)).pipe( catchError(error => handleNpmError(error, task, otp => { context.otp = otp; return pkgPublish(pkgManager, {...options, otp}); - })) + })), ); +}; -module.exports.getPackagePublishArguments = getPackagePublishArguments; +export default publish; diff --git a/source/npm/util.js b/source/npm/util.js index 2d594761..0d6582a5 100644 --- a/source/npm/util.js +++ b/source/npm/util.js @@ -1,16 +1,15 @@ -'use strict'; -const fs = require('fs'); -const path = require('path'); -const execa = require('execa'); -const pTimeout = require('p-timeout'); -const {default: ow} = require('ow'); -const npmName = require('npm-name'); -const chalk = require('chalk'); -const pkgDir = require('pkg-dir'); -const ignoreWalker = require('ignore-walk'); -const minimatch = require('minimatch'); -const {verifyRequirementSatisfied} = require('../version'); -const semver = require('semver'); +import fs from 'node:fs'; +import path from 'node:path'; +import {execa} from 'execa'; +import pTimeout from 'p-timeout'; +import ow from 'ow'; +import npmName from 'npm-name'; +import chalk from 'chalk'; +import {packageDirectorySync} from 'pkg-dir'; +import ignoreWalker from 'ignore-walk'; +import minimatch from 'minimatch'; +import semver from 'semver'; +import Version from '../version.js'; // According to https://docs.npmjs.com/files/package.json#files // npm's default behavior is to ignore these files. @@ -32,10 +31,10 @@ const filesIgnoredByDefault = [ 'npm-debug.log', 'package-lock.json', '.git/**/*', - '.git' + '.git', ]; -exports.checkConnection = () => pTimeout( +export const checkConnection = () => pTimeout( (async () => { try { await execa('npm', ['ping']); @@ -43,12 +42,13 @@ exports.checkConnection = () => pTimeout( } catch { throw new Error('Connection to npm registry failed'); } - })(), - 15000, - 'Connection to npm registry timed out' + })(), { + milliseconds: 15_000, + message: 'Connection to npm registry timed out', + }, ); -exports.username = async ({externalRegistry}) => { +export const username = async ({externalRegistry}) => { const args = ['whoami']; if (externalRegistry) { @@ -59,19 +59,19 @@ exports.username = async ({externalRegistry}) => { const {stdout} = await execa('npm', args); return stdout; } catch (error) { - throw new Error(/ENEEDAUTH/.test(error.stderr) ? - 'You must be logged in. Use `npm login` and try again.' : - 'Authentication error. Use `npm whoami` to troubleshoot.'); + throw new Error(/ENEEDAUTH/.test(error.stderr) + ? 'You must be logged in. Use `npm login` and try again.' + : 'Authentication error. Use `npm whoami` to troubleshoot.'); } }; -exports.collaborators = async pkg => { +export const collaborators = async pkg => { const packageName = pkg.name; ow(packageName, ow.string); - const npmVersion = await exports.version(); + const npmVersion = await version(); const args = semver.satisfies(npmVersion, '>=9.0.0') ? ['access', 'list', 'collaborators', packageName, '--json'] : ['access', 'ls-collaborators', packageName]; - if (exports.isExternalRegistry(pkg)) { + if (isExternalRegistry(pkg)) { args.push('--registry', pkg.publishConfig.registry); } @@ -88,7 +88,7 @@ exports.collaborators = async pkg => { } }; -exports.prereleaseTags = async packageName => { +export const prereleaseTags = async packageName => { ow(packageName, ow.string); let tags = []; @@ -120,16 +120,16 @@ exports.prereleaseTags = async packageName => { return tags; }; -exports.isPackageNameAvailable = async pkg => { +export const isPackageNameAvailable = async pkg => { const args = [pkg.name]; const availability = { isAvailable: false, - isUnknown: false + isUnknown: false, }; - if (exports.isExternalRegistry(pkg)) { + if (isExternalRegistry(pkg)) { args.push({ - registryUrl: pkg.publishConfig.registry + registryUrl: pkg.publishConfig.registry, }); } @@ -142,19 +142,19 @@ exports.isPackageNameAvailable = async pkg => { return availability; }; -exports.isExternalRegistry = pkg => typeof pkg.publishConfig === 'object' && typeof pkg.publishConfig.registry === 'string'; +export const isExternalRegistry = pkg => typeof pkg.publishConfig === 'object' && typeof pkg.publishConfig.registry === 'string'; -exports.version = async () => { +export const version = async () => { const {stdout} = await execa('npm', ['--version']); return stdout; }; -exports.verifyRecentNpmVersion = async () => { - const npmVersion = await exports.version(); - verifyRequirementSatisfied('npm', npmVersion); +export const verifyRecentNpmVersion = async () => { + const npmVersion = await version(); + Version.verifyRequirementSatisfied('npm', npmVersion); }; -exports.checkIgnoreStrategy = ({files}) => { +export const checkIgnoreStrategy = ({files}) => { if (!files && !npmignoreExistsInPackageRootDir()) { console.log(` \n${chalk.bold.yellow('Warning:')} No ${chalk.bold.cyan('files')} field specified in ${chalk.bold.magenta('package.json')} nor is a ${chalk.bold.magenta('.npmignore')} file present. Having one of those will prevent you from accidentally publishing development-specific files along with your package's source code to npm. @@ -163,7 +163,7 @@ exports.checkIgnoreStrategy = ({files}) => { }; function npmignoreExistsInPackageRootDir() { - const rootDir = pkgDir.sync(); + const rootDir = packageDirectorySync(); return fs.existsSync(path.resolve(rootDir, '.npmignore')); } @@ -172,10 +172,11 @@ function excludeGitAndNodeModulesPaths(singlePath) { } async function getFilesIgnoredByDotnpmignore(pkg, fileList) { - const allowList = (await ignoreWalker({ - path: pkgDir.sync(), - ignoreFiles: ['.npmignore'] - })).filter(singlePath => excludeGitAndNodeModulesPaths(singlePath)); + let allowList = await ignoreWalker({ + path: packageDirectorySync(), + ignoreFiles: ['.npmignore'], + }); + allowList = allowList.filter(singlePath => excludeGitAndNodeModulesPaths(singlePath)); return fileList.filter(minimatch.filter(getIgnoredFilesGlob(allowList, pkg.directories), {matchBase: true, dot: true})); } @@ -185,20 +186,20 @@ function filterFileList(globArray, fileList) { } const globString = globArray.length > 1 ? `{${globArray.filter(singlePath => excludeGitAndNodeModulesPaths(singlePath))}}` : globArray[0]; - return fileList.filter(minimatch.filter(globString, {matchBase: true, dot: true})); // eslint-disable-line unicorn/no-fn-reference-in-iterator + return fileList.filter(minimatch.filter(globString, {matchBase: true, dot: true})); // eslint-disable-line unicorn/no-array-callback-reference, unicorn/no-array-method-this-argument } async function getFilesIncludedByDotnpmignore(pkg, fileList) { const allowList = await ignoreWalker({ - path: pkgDir.sync(), - ignoreFiles: ['.npmignore'] + path: packageDirectorySync(), + ignoreFiles: ['.npmignore'], }); return filterFileList(allowList, fileList); } function getFilesNotIncludedInFilesProperty(pkg, fileList) { const globArrayForFilesAndDirectories = [...pkg.files]; - const rootDir = pkgDir.sync(); + const rootDir = packageDirectorySync(); for (const glob of pkg.files) { try { if (fs.statSync(path.resolve(rootDir, glob)).isDirectory()) { @@ -213,7 +214,7 @@ function getFilesNotIncludedInFilesProperty(pkg, fileList) { function getFilesIncludedInFilesProperty(pkg, fileList) { const globArrayForFilesAndDirectories = [...pkg.files]; - const rootDir = pkgDir.sync(); + const rootDir = packageDirectorySync(); for (const glob of pkg.files) { try { if (fs.statSync(path.resolve(rootDir, glob)).isDirectory()) { @@ -236,7 +237,7 @@ function getDefaultIncludedFilesGlob(mainFile) { 'HISTORY*', 'LICENSE*', 'LICENCE*', - 'NOTICE*' + 'NOTICE*', ]; if (mainFile) { filesAlwaysIncluded.push(mainFile); @@ -261,7 +262,7 @@ function getIgnoredFilesGlob(globArrayFromFilesProperty, packageDirectories) { } // Get all files which will be ignored by either `.npmignore` or the `files` property in `package.json` (if defined). -exports.getNewAndUnpublishedFiles = async (pkg, newFiles = []) => { +export const getNewAndUnpublishedFiles = async (pkg, newFiles = []) => { if (pkg.files) { return getFilesNotIncludedInFilesProperty(pkg, newFiles); } @@ -273,7 +274,7 @@ exports.getNewAndUnpublishedFiles = async (pkg, newFiles = []) => { return []; }; -exports.getFirstTimePublishedFiles = async (pkg, newFiles = []) => { +export const getFirstTimePublishedFiles = async (pkg, newFiles = []) => { let result; if (pkg.files) { result = getFilesIncludedInFilesProperty(pkg, newFiles); @@ -286,11 +287,10 @@ exports.getFirstTimePublishedFiles = async (pkg, newFiles = []) => { return result.filter(minimatch.filter(`!{${filesIgnoredByDefault}}`, {matchBase: true, dot: true})).filter(minimatch.filter(getDefaultIncludedFilesGlob(pkg.main), {nocase: true, matchBase: true})); }; -exports.getRegistryUrl = async (pkgManager, pkg) => { +export const getRegistryUrl = async (pkgManager, pkg) => { const args = ['config', 'get', 'registry']; - if (exports.isExternalRegistry(pkg)) { - args.push('--registry'); - args.push(pkg.publishConfig.registry); + if (isExternalRegistry(pkg)) { + args.push('--registry', pkg.publishConfig.registry); } const {stdout} = await execa(pkgManager, args); diff --git a/source/prerequisite-tasks.js b/source/prerequisite-tasks.js index 56b12aa7..50006c8d 100644 --- a/source/prerequisite-tasks.js +++ b/source/prerequisite-tasks.js @@ -1,12 +1,12 @@ -'use strict'; -const Listr = require('listr'); -const execa = require('execa'); -const version = require('./version'); -const git = require('./git-util'); -const npm = require('./npm/util'); -const {getTagVersionPrefix} = require('./util'); +import process from 'node:process'; +import Listr from 'listr'; +import {execa} from 'execa'; +import Version from './version.js'; +import * as git from './git-util.js'; +import * as npm from './npm/util.js'; +import {getTagVersionPrefix} from './util.js'; -module.exports = (input, pkg, options) => { +const prerequisiteTasks = (input, pkg, options) => { const isExternalRegistry = npm.isExternalRegistry(pkg); let newVersion = null; @@ -14,26 +14,26 @@ module.exports = (input, pkg, options) => { { title: 'Ping npm registry', enabled: () => !pkg.private && !isExternalRegistry, - task: async () => npm.checkConnection() + task: async () => npm.checkConnection(), }, { title: 'Check npm version', - task: async () => npm.verifyRecentNpmVersion() + task: async () => npm.verifyRecentNpmVersion(), }, { title: 'Check yarn version', enabled: () => options.yarn === true, - task: async () => { + async task() { const {stdout: yarnVersion} = await execa('yarn', ['--version']); - version.verifyRequirementSatisfied('yarn', yarnVersion); - } + Version.verifyRequirementSatisfied('yarn', yarnVersion); + }, }, { title: 'Verify user is authenticated', enabled: () => process.env.NODE_ENV !== 'test' && !pkg.private, - task: async () => { + async task() { const username = await npm.username({ - externalRegistry: isExternalRegistry ? pkg.publishConfig.registry : false + externalRegistry: isExternalRegistry ? pkg.publishConfig.registry : false, }); const collaborators = await npm.collaborators(pkg); @@ -46,41 +46,43 @@ module.exports = (input, pkg, options) => { if (!permissions || !permissions.includes('write')) { throw new Error('You do not have write permissions required to publish this package.'); } - } + }, }, { title: 'Check git version', - task: async () => git.verifyRecentGitVersion() + task: async () => git.verifyRecentGitVersion(), }, { title: 'Check git remote', - task: async () => git.verifyRemoteIsValid() + task: async () => git.verifyRemoteIsValid(), }, { title: 'Validate version', - task: () => { - newVersion = version.getAndValidateNewVersionFrom(input, pkg.version); - } + task() { + newVersion = Version.getAndValidateNewVersionFrom(input, pkg.version); + }, }, { title: 'Check for pre-release version', - task: () => { - if (!pkg.private && version(newVersion).isPrerelease() && !options.tag) { + task() { + if (!pkg.private && new Version(newVersion).isPrerelease() && !options.tag) { throw new Error('You must specify a dist-tag using --tag when publishing a pre-release version. This prevents accidentally tagging unstable versions as "latest". https://docs.npmjs.com/cli/dist-tag'); } - } + }, }, { title: 'Check git tag existence', - task: async () => { + async task() { await git.fetch(); const tagPrefix = await getTagVersionPrefix(options); await git.verifyTagDoesNotExistOnRemote(`${tagPrefix}${newVersion}`); - } - } + }, + }, ]; return new Listr(tasks); }; + +export default prerequisiteTasks; diff --git a/source/pretty-version-diff.js b/source/pretty-version-diff.js index 15eb60b8..059fa219 100644 --- a/source/pretty-version-diff.js +++ b/source/pretty-version-diff.js @@ -1,9 +1,8 @@ -'use strict'; -const chalk = require('chalk'); -const version = require('./version'); +import chalk from 'chalk'; +import Version from './version.js'; -module.exports = (oldVersion, inc) => { - const newVersion = version(oldVersion).getNewVersionFrom(inc).split('.'); +const prettyVersionDiff = (oldVersion, inc) => { + const newVersion = new Version(oldVersion).getNewVersionFrom(inc).split('.'); oldVersion = oldVersion.split('.'); let firstVersionChange = false; const output = []; @@ -23,3 +22,5 @@ module.exports = (oldVersion, inc) => { return output.join(chalk.reset.dim('.')); }; + +export default prettyVersionDiff; diff --git a/source/release-task-helper.js b/source/release-task-helper.js index 53d93d6f..2e71aca9 100644 --- a/source/release-task-helper.js +++ b/source/release-task-helper.js @@ -1,13 +1,12 @@ -'use strict'; -const open = require('open'); -const newGithubReleaseUrl = require('new-github-release-url'); -const {getTagVersionPrefix, getPreReleasePrefix} = require('./util'); -const version = require('./version'); +import open from 'open'; +import newGithubReleaseUrl from 'new-github-release-url'; +import {getTagVersionPrefix, getPreReleasePrefix} from './util.js'; +import Version from './version.js'; -module.exports = async (options, pkg) => { - const newVersion = version(pkg.version).getNewVersionFrom(options.version); +const releaseTaskHelper = async (options, pkg) => { + const newVersion = new Version(pkg.version).getNewVersionFrom(options.version); let tag = await getTagVersionPrefix(options) + newVersion; - const isPreRelease = version(options.version).isPrerelease(); + const isPreRelease = new Version(options.version).isPrerelease(); if (isPreRelease) { tag += await getPreReleasePrefix(options); } @@ -16,8 +15,10 @@ module.exports = async (options, pkg) => { repoUrl: options.repoUrl, tag, body: options.releaseNotes(tag), - isPrerelease: isPreRelease + isPrerelease: isPreRelease, }); await open(url); }; + +export default releaseTaskHelper; diff --git a/source/ui.js b/source/ui.js index 247cbd39..3cbfb213 100644 --- a/source/ui.js +++ b/source/ui.js @@ -1,15 +1,14 @@ -'use strict'; -const inquirer = require('inquirer'); -const chalk = require('chalk'); -const githubUrlFromGit = require('github-url-from-git'); -const {htmlEscape} = require('escape-goat'); -const isScoped = require('is-scoped'); -const isInteractive = require('is-interactive'); -const util = require('./util'); -const git = require('./git-util'); -const {prereleaseTags, checkIgnoreStrategy, getRegistryUrl, isExternalRegistry} = require('./npm/util'); -const version = require('./version'); -const prettyVersionDiff = require('./pretty-version-diff'); +import inquirer from 'inquirer'; +import chalk from 'chalk'; +import githubUrlFromGit from 'github-url-from-git'; +import {htmlEscape} from 'escape-goat'; +import isScoped from 'is-scoped'; +import isInteractive from 'is-interactive'; +import * as util from './util.js'; +import * as git from './git-util.js'; +import {prereleaseTags, checkIgnoreStrategy, getRegistryUrl, isExternalRegistry} from './npm/util.js'; +import Version from './version.js'; +import prettyVersionDiff from './pretty-version-diff.js'; const printCommitLog = async (repoUrl, registryUrl, fromLatestTag, releaseBranch) => { const revision = fromLatestTag ? await git.latestTagOrFirstCommit() : await git.previousTagOrFirstCommit(); @@ -23,7 +22,7 @@ const printCommitLog = async (repoUrl, registryUrl, fromLatestTag, releaseBranch return { hasCommits: false, hasUnreleasedCommits: false, - releaseNotes: () => {} + releaseNotes() {}, }; } @@ -35,7 +34,7 @@ const printCommitLog = async (repoUrl, registryUrl, fromLatestTag, releaseBranch const splitIndex = commit.lastIndexOf(' '); return { message: commit.slice(0, splitIndex), - id: commit.slice(splitIndex + 1) + id: commit.slice(splitIndex + 1), }; }); @@ -66,7 +65,7 @@ const printCommitLog = async (repoUrl, registryUrl, fromLatestTag, releaseBranch }).join('\n'); const releaseNotes = nextTag => commits.map(commit => - `- ${htmlEscape(commit.message)} ${commit.id}` + `- ${htmlEscape(commit.message)} ${commit.id}`, ).join('\n') + `\n\n${repoUrl}/compare/${revision}...${nextTag}`; const commitRange = util.linkifyCommitRange(repoUrl, commitRangeText); @@ -75,7 +74,7 @@ const printCommitLog = async (repoUrl, registryUrl, fromLatestTag, releaseBranch return { hasCommits: true, hasUnreleasedCommits, - releaseNotes + releaseNotes, }; }; @@ -115,13 +114,14 @@ const checkNewFilesAndDependencies = async (pkg, pkgPath) => { type: 'confirm', name: 'confirm', message: `${messages.join('\n')}\nContinue?`, - default: false + default: false, }]); return answers.confirm; }; -module.exports = async (options, {pkg, pkgPath}) => { +// eslint-disable-next-line complexity +const ui = async (options, {pkg, pkgPath}) => { const oldVersion = pkg.version; const extraBaseUrls = ['gitlab.com']; const repoUrl = pkg.repository && githubUrlFromGit(pkg.repository.url, {extraBaseUrls}); @@ -136,7 +136,7 @@ module.exports = async (options, {pkg, pkgPath}) => { if (!answerIgnoredFiles) { return { ...options, - confirm: answerIgnoredFiles + confirm: answerIgnoredFiles, }; } } @@ -144,56 +144,115 @@ module.exports = async (options, {pkg, pkgPath}) => { if (options.releaseDraftOnly) { console.log(`\nCreate a release draft on GitHub for ${chalk.bold.magenta(pkg.name)} ${chalk.dim(`(current: ${oldVersion})`)}\n`); } else { - const newVersion = options.version ? version.getAndValidateNewVersionFrom(options.version, oldVersion) : undefined; + const newVersion = options.version ? Version.getAndValidateNewVersionFrom(options.version, oldVersion) : undefined; const versionText = chalk.dim(`(current: ${oldVersion}${newVersion ? `, next: ${prettyVersionDiff(oldVersion, newVersion)}` : ''}${chalk.dim(')')}`); console.log(`\nPublish a new version of ${chalk.bold.magenta(pkg.name)} ${versionText}\n`); } - const prompts = [ - { + const useLatestTag = !options.releaseDraftOnly; + const {hasCommits, hasUnreleasedCommits, releaseNotes} = await printCommitLog(repoUrl, registryUrl, useLatestTag, releaseBranch); + + if (hasUnreleasedCommits && options.releaseDraftOnly) { + const answers = await inquirer.prompt({ + confirm: { + type: 'confirm', + message: 'Unreleased commits found. They won\'t be included in the release draft. Continue?', + default: false, + }, + }); + + if (!answers.confirm) { + return { + ...options, + ...answers, + }; + } + } + + if (options.version) { + return { + ...options, + confirm: true, + repoUrl, + releaseNotes, + }; + } + + if (!hasCommits) { + const answers = await inquirer.prompt({ + confirm: { + type: 'confirm', + message: 'No commits found since previous release, continue?', + default: false, + }, + }); + + if (!answers.confirm) { + return { + ...options, + ...answers, + }; + } + } + + if (options.availability.isUnknown) { + const answers = await inquirer.prompt({ + confirm: { + type: 'confirm', + when: isScoped(pkg.name) && options.runPublish, + message: `Failed to check availability of scoped repo name ${chalk.bold.magenta(pkg.name)}. Do you want to try and publish it anyway?`, + default: false, + }, + }); + + if (!answers.confirm) { + return { + ...options, + ...answers, + }; + } + } + + const answers = await inquirer.prompt({ + version: { type: 'list', - name: 'version', message: 'Select semver increment or specify new version', - pageSize: version.SEMVER_INCREMENTS.length + 2, - choices: version.SEMVER_INCREMENTS + pageSize: Version.SEMVER_INCREMENTS.length + 2, + choices: [...Version.SEMVER_INCREMENTS .map(inc => ({ name: `${inc} ${prettyVersionDiff(oldVersion, inc)}`, - value: inc - })) - .concat([ - new inquirer.Separator(), - { - name: 'Other (specify)', - value: null - } - ]), - filter: input => version.isValidInput(input) ? version(oldVersion).getNewVersionFrom(input) : input + value: inc, + })), + new inquirer.Separator(), + { + name: 'Other (specify)', + value: null, + }], + filter: input => Version.isValidInput(input) ? new Version(oldVersion).getNewVersionFrom(input) : input, }, - { + customVersion: { type: 'input', - name: 'customVersion', message: 'Version', when: answers => !answers.version, - filter: input => version.isValidInput(input) ? version(pkg.version).getNewVersionFrom(input) : input, - validate: input => { - if (!version.isValidInput(input)) { + filter: input => Version.isValidInput(input) ? new Version(pkg.version).getNewVersionFrom(input) : input, + validate(input) { + if (!Version.isValidInput(input)) { return 'Please specify a valid semver, for example, `1.2.3`. See https://semver.org'; } - if (version(oldVersion).isLowerThanOrEqualTo(input)) { + if (new Version(oldVersion).isLowerThanOrEqualTo(input)) { return `Version must be greater than ${oldVersion}`; } return true; - } + }, }, - { + tag: { type: 'list', - name: 'tag', message: 'How should this pre-release version be tagged in npm?', - when: answers => options.runPublish && (version.isPrereleaseOrIncrement(answers.customVersion) || version.isPrereleaseOrIncrement(answers.version)) && !options.tag, - choices: async () => { + when: answers => options.runPublish && (Version.isPrereleaseOrIncrement(answers.customVersion) || Version.isPrereleaseOrIncrement(answers.version)) && !options.tag, + async choices() { const existingPrereleaseTags = await prereleaseTags(pkg.name); return [ @@ -201,17 +260,16 @@ module.exports = async (options, {pkg, pkgPath}) => { new inquirer.Separator(), { name: 'Other (specify)', - value: null - } + value: null, + }, ]; - } + }, }, - { + customTag: { type: 'input', - name: 'customTag', message: 'Tag', - when: answers => options.runPublish && (version.isPrereleaseOrIncrement(answers.customVersion) || version.isPrereleaseOrIncrement(answers.version)) && !options.tag && !answers.tag, - validate: input => { + when: answers => options.runPublish && (Version.isPrereleaseOrIncrement(answers.customVersion) || Version.isPrereleaseOrIncrement(answers.version)) && !options.tag && !answers.tag, + validate(input) { if (input.length === 0) { return 'Please specify a tag, for example, `next`.'; } @@ -221,79 +279,15 @@ module.exports = async (options, {pkg, pkgPath}) => { } return true; - } + }, }, - { + publishScoped: { type: 'confirm', - name: 'publishScoped', when: isScoped(pkg.name) && options.availability.isAvailable && !options.availability.isUnknown && options.runPublish && (pkg.publishConfig && pkg.publishConfig.access !== 'restricted') && !isExternalRegistry(pkg), message: `This scoped repo ${chalk.bold.magenta(pkg.name)} hasn't been published. Do you want to publish it publicly?`, - default: false - } - ]; - - const useLatestTag = !options.releaseDraftOnly; - const {hasCommits, hasUnreleasedCommits, releaseNotes} = await printCommitLog(repoUrl, registryUrl, useLatestTag, releaseBranch); - - if (hasUnreleasedCommits && options.releaseDraftOnly) { - const answers = await inquirer.prompt([{ - type: 'confirm', - name: 'confirm', - message: 'Unreleased commits found. They won\'t be included in the release draft. Continue?', - default: false - }]); - - if (!answers.confirm) { - return { - ...options, - ...answers - }; - } - } - - if (options.version) { - return { - ...options, - confirm: true, - repoUrl, - releaseNotes - }; - } - - if (!hasCommits) { - const answers = await inquirer.prompt([{ - type: 'confirm', - name: 'confirm', - message: 'No commits found since previous release, continue?', - default: false - }]); - - if (!answers.confirm) { - return { - ...options, - ...answers - }; - } - } - - if (options.availability.isUnknown) { - const answers = await inquirer.prompt([{ - type: 'confirm', - name: 'confirm', - when: isScoped(pkg.name) && options.runPublish, - message: `Failed to check availability of scoped repo name ${chalk.bold.magenta(pkg.name)}. Do you want to try and publish it anyway?`, - default: false - }]); - - if (!answers.confirm) { - return { - ...options, - ...answers - }; - } - } - - const answers = await inquirer.prompt(prompts); + default: false, + }, + }); return { ...options, @@ -302,6 +296,8 @@ module.exports = async (options, {pkg, pkgPath}) => { publishScoped: answers.publishScoped, confirm: true, repoUrl, - releaseNotes + releaseNotes, }; }; + +export default ui; diff --git a/source/util.js b/source/util.js index 0b683fc6..b76a4080 100644 --- a/source/util.js +++ b/source/util.js @@ -1,30 +1,28 @@ -'use strict'; -const readPkgUp = require('read-pkg-up'); -const issueRegex = require('issue-regex'); -const terminalLink = require('terminal-link'); -const execa = require('execa'); -const pMemoize = require('p-memoize'); -const {default: ow} = require('ow'); -const pkgDir = require('pkg-dir'); -const chalk = require('chalk'); -const gitUtil = require('./git-util'); -const npmUtil = require('./npm/util'); - -exports.readPkg = packagePath => { - packagePath = packagePath ? pkgDir.sync(packagePath) : pkgDir.sync(); - +import {readPackageUp} from 'read-pkg-up'; +import issueRegex from 'issue-regex'; +import terminalLink from 'terminal-link'; +import {execa} from 'execa'; +import pMemoize from 'p-memoize'; +import ow from 'ow'; +import chalk from 'chalk'; +import {packageDirectory} from 'pkg-dir'; +import * as gitUtil from './git-util.js'; +import * as npmUtil from './npm/util.js'; + +export const readPkg = async packagePath => { + packagePath = packagePath ? await packageDirectory(packagePath) : await packageDirectory(); if (!packagePath) { throw new Error('No `package.json` found. Make sure the current directory is a valid package.'); } - const {packageJson, path} = readPkgUp.sync({ - cwd: packagePath + const {packageJson, path} = await readPackageUp({ + cwd: packagePath, }); return {pkg: packageJson, pkgPath: path}; }; -exports.linkifyIssues = (url, message) => { +export const linkifyIssues = (url, message) => { if (!(url && terminalLink.isSupported)) { return message; } @@ -40,7 +38,7 @@ exports.linkifyIssues = (url, message) => { }); }; -exports.linkifyCommit = (url, commit) => { +export const linkifyCommit = (url, commit) => { if (!(url && terminalLink.isSupported)) { return commit; } @@ -48,7 +46,7 @@ exports.linkifyCommit = (url, commit) => { return terminalLink(commit, `${url}/commit/${commit}`); }; -exports.linkifyCommitRange = (url, commitRange) => { +export const linkifyCommitRange = (url, commitRange) => { if (!(url && terminalLink.isSupported)) { return commitRange; } @@ -56,7 +54,7 @@ exports.linkifyCommitRange = (url, commitRange) => { return terminalLink(commitRange, `${url}/compare/${commitRange}`); }; -exports.getTagVersionPrefix = pMemoize(async options => { +export const getTagVersionPrefix = pMemoize(async options => { ow(options, ow.object.hasKeys('yarn')); try { @@ -72,14 +70,14 @@ exports.getTagVersionPrefix = pMemoize(async options => { } }); -exports.joinList = list => chalk.reset(list.map(item => `- ${item}`).join('\n')); +export const joinList = list => chalk.reset(list.map(item => `- ${item}`).join('\n')); -exports.getNewFiles = async pkg => { +export const getNewFiles = async pkg => { const listNewFiles = await gitUtil.newFilesSinceLastRelease(); return {unpublished: await npmUtil.getNewAndUnpublishedFiles(pkg, listNewFiles), firstTime: await npmUtil.getFirstTimePublishedFiles(pkg, listNewFiles)}; }; -exports.getNewDependencies = async (newPkg, pkgPath) => { +export const getNewDependencies = async (newPkg, pkgPath) => { let oldPkg = await gitUtil.readFileFromLastRelease(pkgPath); oldPkg = JSON.parse(oldPkg); @@ -94,7 +92,7 @@ exports.getNewDependencies = async (newPkg, pkgPath) => { return newDependencies; }; -exports.getPreReleasePrefix = pMemoize(async options => { +export const getPreReleasePrefix = pMemoize(async options => { ow(options, ow.object.hasKeys('yarn')); try { diff --git a/source/version.js b/source/version.js index a0504466..56710fac 100644 --- a/source/version.js +++ b/source/version.js @@ -1,7 +1,9 @@ -'use strict'; -const semver = require('semver'); +import semver from 'semver'; +import {readPackageUp} from 'read-pkg-up'; -class Version { +const {packageJson: pkg} = await readPackageUp(); + +export default class Version { constructor(version) { this.version = version; } @@ -11,66 +13,64 @@ class Version { } satisfies(range) { - module.exports.validate(this.version); + Version.validate(this.version); return semver.satisfies(this.version, range, { - includePrerelease: true + includePrerelease: true, }); } getNewVersionFrom(input) { - module.exports.validate(this.version); - if (!module.exports.isValidInput(input)) { - throw new Error(`Version should be either ${module.exports.SEMVER_INCREMENTS.join(', ')}, or a valid semver version.`); + Version.validate(this.version); + if (!Version.isValidInput(input)) { + throw new Error(`Version should be either ${Version.SEMVER_INCREMENTS.join(', ')}, or a valid semver version.`); } - return module.exports.SEMVER_INCREMENTS.includes(input) ? semver.inc(this.version, input) : input; + return Version.SEMVER_INCREMENTS.includes(input) ? semver.inc(this.version, input) : input; } isGreaterThanOrEqualTo(otherVersion) { - module.exports.validate(this.version); - module.exports.validate(otherVersion); + Version.validate(this.version); + Version.validate(otherVersion); return semver.gte(otherVersion, this.version); } isLowerThanOrEqualTo(otherVersion) { - module.exports.validate(this.version); - module.exports.validate(otherVersion); + Version.validate(this.version); + Version.validate(otherVersion); return semver.lte(otherVersion, this.version); } -} - -module.exports = version => new Version(version); -module.exports.SEMVER_INCREMENTS = ['patch', 'minor', 'major', 'prepatch', 'preminor', 'premajor', 'prerelease']; -module.exports.PRERELEASE_VERSIONS = ['prepatch', 'preminor', 'premajor', 'prerelease']; + static SEMVER_INCREMENTS = ['patch', 'minor', 'major', 'prepatch', 'preminor', 'premajor', 'prerelease']; + static PRERELEASE_VERSIONS = ['prepatch', 'preminor', 'premajor', 'prerelease']; -module.exports.isPrereleaseOrIncrement = input => module.exports(input).isPrerelease() || module.exports.PRERELEASE_VERSIONS.includes(input); + static isPrereleaseOrIncrement = input => new Version(input).isPrerelease() || Version.PRERELEASE_VERSIONS.includes(input); -const isValidVersion = input => Boolean(semver.valid(input)); + static isValidVersion = input => Boolean(semver.valid(input)); -module.exports.isValidInput = input => module.exports.SEMVER_INCREMENTS.includes(input) || isValidVersion(input); + static isValidInput = input => Version.SEMVER_INCREMENTS.includes(input) || Version.isValidVersion(input); -module.exports.validate = version => { - if (!isValidVersion(version)) { - throw new Error('Version should be a valid semver version.'); + static validate(version) { + if (!Version.isValidVersion(version)) { + throw new Error('Version should be a valid semver version.'); + } } -}; -module.exports.verifyRequirementSatisfied = (dependency, version) => { - const depRange = require('../package.json').engines[dependency]; - if (!module.exports(version).satisfies(depRange)) { - throw new Error(`Please upgrade to ${dependency}${depRange}`); + static verifyRequirementSatisfied(dependency, version) { + const depRange = pkg.engines[dependency]; + if (!new Version(version).satisfies(depRange)) { + throw new Error(`Please upgrade to ${dependency}${depRange}`); + } } -}; -module.exports.getAndValidateNewVersionFrom = (input, version) => { - const newVersion = module.exports(version).getNewVersionFrom(input); + static getAndValidateNewVersionFrom(input, version) { + const newVersion = new Version(version).getNewVersionFrom(input); - if (module.exports(version).isLowerThanOrEqualTo(newVersion)) { - throw new Error(`New version \`${newVersion}\` should be higher than current version \`${version}\``); - } + if (new Version(version).isLowerThanOrEqualTo(newVersion)) { + throw new Error(`New version \`${newVersion}\` should be higher than current version \`${version}\``); + } - return newVersion; -}; + return newVersion; + } +} diff --git a/test/_utils.js b/test/_utils.js new file mode 100644 index 00000000..1f9a47f3 --- /dev/null +++ b/test/_utils.js @@ -0,0 +1,50 @@ +import esmock from 'esmock'; +import {execa} from 'execa'; +import {SilentRenderer} from './fixtures/listr-renderer.js'; + +export const _stubExeca = source => async (t, commands) => esmock(source, {}, { + execa: { + async execa(...args) { + const results = await Promise.all(commands.map(async result => { + const argsMatch = await t.try(tt => { + const [command, ...commandArgs] = result.command.split(' '); + tt.deepEqual(args, [command, commandArgs]); + }); + + if (argsMatch.passed) { + argsMatch.discard(); + + if (!result.exitCode || result.exitCode === 0) { + return result; + } + + throw result; + } + + argsMatch.discard(); + })); + + const result = results.filter(Boolean).at(0); + return result ?? execa(...args); + }, + }, +}); + +export const run = async listr => { + listr.setRenderer(SilentRenderer); + await listr.run(); +}; + +export const assertTaskFailed = (t, taskTitle) => { + const task = SilentRenderer.tasks.find(task => task.title === taskTitle); + t.true(task.hasFailed(), `'${taskTitle}' did not fail!`); +}; + +export const assertTaskDisabled = (t, taskTitle) => { + const task = SilentRenderer.tasks.find(task => task.title === taskTitle); + t.true(!task.isEnabled(), `'${taskTitle}' was enabled!`); +}; + +export const assertTaskDoesntExist = (t, taskTitle) => { + t.true(SilentRenderer.tasks.every(task => task.title !== taskTitle), `'${taskTitle}' exists!`); +}; diff --git a/test/config.js b/test/config.js index 3c577f69..0800c14d 100644 --- a/test/config.js +++ b/test/config.js @@ -1,90 +1,100 @@ -import path from 'path'; +import path from 'node:path'; import test from 'ava'; import sinon from 'sinon'; -import proxyquire from 'proxyquire'; +import esmock from 'esmock'; -const fixtureBasePath = path.resolve('test', 'fixtures', 'config'); +const testedModulePath = '../source/config.js'; + +const getFixture = fixture => path.resolve('test', 'fixtures', 'config', fixture); +const getFixtures = fixtures => fixtures.map(fixture => getFixture(fixture)); const getConfigsWhenGlobalBinaryIsUsed = async homedirStub => { - const pathsPkgDir = [path.resolve(fixtureBasePath, 'pkg-dir'), - path.resolve(fixtureBasePath, 'local1'), - path.resolve(fixtureBasePath, 'local2'), - path.resolve(fixtureBasePath, 'local3')]; - - const promises = []; - pathsPkgDir.forEach(pathPkgDir => { - promises.push(proxyquire('../source/config', { + const pathsPkgDir = getFixtures(['pkg-dir', 'local1', 'local2', 'local3']); + + const promises = pathsPkgDir.map(async pathPkgDir => { + const getConfig = await esmock(testedModulePath, { 'is-installed-globally': true, - 'pkg-dir': async () => { - return pathPkgDir; - }, - os: { - homedir: homedirStub - } - })()); + 'pkg-dir': {packageDirectory: async () => pathPkgDir}, + 'node:os': {homedir: homedirStub}, + }); + return getConfig(); }); + return Promise.all(promises); }; const getConfigsWhenLocalBinaryIsUsed = async pathPkgDir => { - const homedirs = [path.resolve(fixtureBasePath, 'homedir1'), - path.resolve(fixtureBasePath, 'homedir2'), - path.resolve(fixtureBasePath, 'homedir3')]; + const homedirs = getFixtures(['homedir1', 'homedir2', 'homedir3']); - const promises = []; - homedirs.forEach(homedir => { - promises.push(proxyquire('../source/config', { + const promises = homedirs.map(async homedir => { + const getConfig = await esmock(testedModulePath, { 'is-installed-globally': false, - 'pkg-dir': async () => { - return pathPkgDir; - }, - os: { - homedir: () => { - return homedir; - } - } - })()); + 'pkg-dir': {packageDirectory: async () => pathPkgDir}, + 'node:os': {homedir: () => homedir}, + }); + return getConfig(); }); + return Promise.all(promises); }; -test('returns config from home directory when global binary is used and `.np-config-json` exists in home directory', async t => { - const homedirStub = sinon.stub(); - homedirStub.returns(path.resolve(fixtureBasePath, 'homedir1')); +const useGlobalBinary = test.macro(async (t, homedir, source) => { + const homedirStub = sinon.stub().returns(getFixture(homedir)); const configs = await getConfigsWhenGlobalBinaryIsUsed(homedirStub); - configs.forEach(config => t.deepEqual(config, {source: 'homedir/.np-config.json'})); -}); -test('returns config from home directory when global binary is used and `.np-config.js` exists in home directory', async t => { - const homedirStub = sinon.stub(); - homedirStub.returns(path.resolve(fixtureBasePath, 'homedir2')); - const configs = await getConfigsWhenGlobalBinaryIsUsed(homedirStub); - configs.forEach(config => t.deepEqual(config, {source: 'homedir/.np-config.js'})); + for (const config of configs) { + t.deepEqual(config, {source}); + } }); -test('returns config from home directory when global binary is used and `.np-config.cjs` exists in home directory', async t => { - const homedirStub = sinon.stub(); - homedirStub.returns(path.resolve(fixtureBasePath, 'homedir3')); - const configs = await getConfigsWhenGlobalBinaryIsUsed(homedirStub); - configs.forEach(config => t.deepEqual(config, {source: 'homedir/.np-config.cjs'})); -}); +const useLocalBinary = test.macro(async (t, pkgDir, source) => { + const configs = await getConfigsWhenLocalBinaryIsUsed(getFixture(pkgDir)); -test('returns config from package directory when local binary is used and `package.json` exists in package directory', async t => { - const configs = await getConfigsWhenLocalBinaryIsUsed(path.resolve(fixtureBasePath, 'pkg-dir')); - configs.forEach(config => t.deepEqual(config, {source: 'package.json'})); + for (const config of configs) { + t.deepEqual(config, {source}); + } }); -test('returns config from package directory when local binary is used and `.np-config.json` exists in package directory', async t => { - const configs = await getConfigsWhenLocalBinaryIsUsed(path.resolve(fixtureBasePath, 'local1')); - configs.forEach(config => t.deepEqual(config, {source: 'packagedir/.np-config.json'})); -}); +test('returns config from home directory when global binary is used and .np-config-json exists in home directory', + useGlobalBinary, 'homedir1', 'homedir/.np-config.json', +); -test('returns config from package directory when local binary is used and `.np-config.js` exists in package directory', async t => { - const configs = await getConfigsWhenLocalBinaryIsUsed(path.resolve(fixtureBasePath, 'local2')); - configs.forEach(config => t.deepEqual(config, {source: 'packagedir/.np-config.js'})); -}); +test('returns config from home directory when global binary is used and `.np-config.js` as CJS exists in home directory', + useGlobalBinary, 'homedir2', 'homedir/.np-config.js', +); -test('returns config from package directory when local binary is used and `.np-config.cjs` exists in package directory', async t => { - const configs = await getConfigsWhenLocalBinaryIsUsed(path.resolve(fixtureBasePath, 'local3')); - configs.forEach(config => t.deepEqual(config, {source: 'packagedir/.np-config.cjs'})); -}); +test('returns config from home directory when global binary is used and `.np-config.cjs` exists in home directory', + useGlobalBinary, 'homedir3', 'homedir/.np-config.cjs', +); + +test.failing('returns config from home directory when global binary is used and `.np-config.js` as ESM exists in home directory', + useGlobalBinary, 'homedir4', 'homedir/.np-config.js', +); + +test('returns config from home directory when global binary is used and `.np-config.mjs` exists in home directory', + useGlobalBinary, 'homedir5', 'homedir/.np-config.mjs', +); + +test('returns config from package directory when local binary is used and `package.json` exists in package directory', + useLocalBinary, 'pkg-dir', 'package.json', +); + +test('returns config from package directory when local binary is used and `.np-config.json` exists in package directory', + useLocalBinary, 'local1', 'packagedir/.np-config.json', +); + +test('returns config from package directory when local binary is used and `.np-config.js` as CJS exists in package directory', + useLocalBinary, 'local2', 'packagedir/.np-config.js', +); + +test('returns config from package directory when local binary is used and `.np-config.cjs` exists in package directory', + useLocalBinary, 'local3', 'packagedir/.np-config.cjs', +); + +test('returns config from package directory when local binary is used and `.np-config.js` as ESM exists in package directory', + useLocalBinary, 'local4', 'packagedir/.np-config.js', +); + +test('returns config from package directory when local binary is used and `.np-config.mjs` exists in package directory', + useLocalBinary, 'local5', 'packagedir/.np-config.mjs', +); diff --git a/test/fixtures/config/homedir4/.np-config.js b/test/fixtures/config/homedir4/.np-config.js new file mode 100644 index 00000000..a91f20d0 --- /dev/null +++ b/test/fixtures/config/homedir4/.np-config.js @@ -0,0 +1,3 @@ +export default { + source: 'homedir/.np-config.js' +}; diff --git a/test/fixtures/config/homedir5/.np-config.mjs b/test/fixtures/config/homedir5/.np-config.mjs new file mode 100644 index 00000000..7565b8fb --- /dev/null +++ b/test/fixtures/config/homedir5/.np-config.mjs @@ -0,0 +1,3 @@ +export default { + source: 'homedir/.np-config.mjs' +}; diff --git a/test/fixtures/config/local4/.np-config.js b/test/fixtures/config/local4/.np-config.js new file mode 100644 index 00000000..41bc0e49 --- /dev/null +++ b/test/fixtures/config/local4/.np-config.js @@ -0,0 +1,3 @@ +export default { + source: 'packagedir/.np-config.js' +}; diff --git a/test/fixtures/config/local4/package.json b/test/fixtures/config/local4/package.json new file mode 100644 index 00000000..6509d65a --- /dev/null +++ b/test/fixtures/config/local4/package.json @@ -0,0 +1,4 @@ +{ + "name": "use-type-module-for-config-fixtures", + "type": "module" +} diff --git a/test/fixtures/config/local5/.np-config.mjs b/test/fixtures/config/local5/.np-config.mjs new file mode 100644 index 00000000..90b0f8f5 --- /dev/null +++ b/test/fixtures/config/local5/.np-config.mjs @@ -0,0 +1,3 @@ +export default { + source: 'packagedir/.np-config.mjs' +}; diff --git a/test/fixtures/config/package.json b/test/fixtures/config/package.json new file mode 100644 index 00000000..7ad6eeb0 --- /dev/null +++ b/test/fixtures/config/package.json @@ -0,0 +1,3 @@ +{ + "name": "override-type-module-for-config-fixtures" +} diff --git a/test/fixtures/listr-renderer.js b/test/fixtures/listr-renderer.js index ee5982e6..9a9f2581 100644 --- a/test/fixtures/listr-renderer.js +++ b/test/fixtures/listr-renderer.js @@ -1,6 +1,6 @@ let tasks; -class SilentRenderer { +export class SilentRenderer { constructor(_tasks) { tasks = _tasks; } @@ -13,9 +13,11 @@ class SilentRenderer { return true; } - render() { } + static clearTasks() { + tasks = []; + } - end() { } -} + render() {} -module.exports.SilentRenderer = SilentRenderer; + end() {} +} diff --git a/test/git-tasks.js b/test/git-tasks.js index 56f5422a..d390d0c5 100644 --- a/test/git-tasks.js +++ b/test/git-tasks.js @@ -1,133 +1,144 @@ import test from 'ava'; -import execaStub from 'execa_test_double'; -import mockery from 'mockery'; -import {SilentRenderer} from './fixtures/listr-renderer'; - -let testedModule; - -const run = async listr => { - listr.setRenderer(SilentRenderer); - await listr.run(); -}; - -test.before(() => { - mockery.registerMock('execa', execaStub.execa); - mockery.enable({ - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - }); - testedModule = require('../source/git-tasks'); -}); +import {SilentRenderer} from './fixtures/listr-renderer.js'; +import { + _stubExeca, + run, + assertTaskFailed, + assertTaskDoesntExist, +} from './_utils.js'; + +/** @type {(...args: ReturnType<_stubExeca>) => Promise} */ +const stubExeca = _stubExeca('../source/git-tasks.js'); -test.beforeEach(() => { - execaStub.resetStub(); +test.afterEach(() => { + SilentRenderer.clearTasks(); }); test.serial('should fail when release branch is not specified, current branch is not the release branch, and publishing from any branch not permitted', async t => { - execaStub.createStub([ - { - command: 'git symbolic-ref --short HEAD', - exitCode: 0, - stdout: 'feature' - } - ]); - await t.throwsAsync(run(testedModule({branch: 'master'})), - {message: 'Not on `master` branch. Use --any-branch to publish anyway, or set a different release branch using --branch.'}); - t.true(SilentRenderer.tasks.some(task => task.title === 'Check current branch' && task.hasFailed())); + const gitTasks = await stubExeca(t, [{ + command: 'git symbolic-ref --short HEAD', + exitCode: 0, + stdout: 'feature', + }]); + + await t.throwsAsync( + run(gitTasks({branch: 'master'})), + {message: 'Not on `master` branch. Use --any-branch to publish anyway, or set a different release branch using --branch.'}, + ); + + assertTaskFailed(t, 'Check current branch'); }); test.serial('should fail when current branch is not the specified release branch and publishing from any branch not permitted', async t => { - execaStub.createStub([ - { - command: 'git symbolic-ref --short HEAD', - exitCode: 0, - stdout: 'feature' - } - ]); - await t.throwsAsync(run(testedModule({branch: 'release'})), - {message: 'Not on `release` branch. Use --any-branch to publish anyway, or set a different release branch using --branch.'}); - t.true(SilentRenderer.tasks.some(task => task.title === 'Check current branch' && task.hasFailed())); + const gitTasks = await stubExeca(t, [{ + command: 'git symbolic-ref --short HEAD', + exitCode: 0, + stdout: 'feature', + }]); + + await t.throwsAsync( + run(gitTasks({branch: 'release'})), + {message: 'Not on `release` branch. Use --any-branch to publish anyway, or set a different release branch using --branch.'}, + ); + + assertTaskFailed(t, 'Check current branch'); }); test.serial('should not fail when current branch not master and publishing from any branch permitted', async t => { - execaStub.createStub([ + const gitTasks = await stubExeca(t, [ { command: 'git symbolic-ref --short HEAD', exitCode: 0, - stdout: 'feature' + stdout: 'feature', }, { command: 'git status --porcelain', exitCode: 0, - stdout: '' + stdout: '', }, { command: 'git rev-list --count --left-only @{u}...HEAD', exitCode: 0, - stdout: '' - } + stdout: '', + }, ]); - await run(testedModule({anyBranch: true})); - t.false(SilentRenderer.tasks.some(task => task.title === 'Check current branch')); + + await t.notThrowsAsync( + run(gitTasks({anyBranch: true})), + ); + + assertTaskDoesntExist(t, 'Check current branch'); }); test.serial('should fail when local working tree modified', async t => { - execaStub.createStub([ + const gitTasks = await stubExeca(t, [ { command: 'git symbolic-ref --short HEAD', exitCode: 0, - stdout: 'master' + stdout: 'master', }, { command: 'git status --porcelain', exitCode: 0, - stdout: 'M source/git-tasks.js' - } + stdout: 'M source/git-tasks.js', + }, ]); - await t.throwsAsync(run(testedModule({branch: 'master'})), {message: 'Unclean working tree. Commit or stash changes first.'}); - t.true(SilentRenderer.tasks.some(task => task.title === 'Check local working tree' && task.hasFailed())); + + await t.throwsAsync( + run(gitTasks({branch: 'master'})), + {message: 'Unclean working tree. Commit or stash changes first.'}, + ); + + assertTaskFailed(t, 'Check local working tree'); }); test.serial('should fail when remote history differs', async t => { - execaStub.createStub([ + const gitTasks = await stubExeca(t, [ { command: 'git symbolic-ref --short HEAD', exitCode: 0, - stdout: 'master' + stdout: 'master', }, { command: 'git status --porcelain', exitCode: 0, - stdout: '' + stdout: '', }, { command: 'git rev-list --count --left-only @{u}...HEAD', exitCode: 0, - stdout: '1' - } + stdout: '1', + }, ]); - await t.throwsAsync(run(testedModule({branch: 'master'})), {message: 'Remote history differs. Please pull changes.'}); - t.true(SilentRenderer.tasks.some(task => task.title === 'Check remote history' && task.hasFailed())); + + await t.throwsAsync( + run(gitTasks({branch: 'master'})), + {message: 'Remote history differs. Please pull changes.'}, + ); + + assertTaskFailed(t, 'Check remote history'); }); test.serial('checks should pass when publishing from master, working tree is clean and remote history not different', async t => { - execaStub.createStub([ + const gitTasks = await stubExeca(t, [ { command: 'git symbolic-ref --short HEAD', exitCode: 0, - stdout: 'master' + stdout: 'master', }, { command: 'git status --porcelain', exitCode: 0, - stdout: '' + stdout: '', }, { command: 'git rev-list --count --left-only @{u}...HEAD', exitCode: 0, - stdout: '' - } + stdout: '', + }, ]); - await t.notThrowsAsync(run(testedModule({branch: 'master'}))); + + await t.notThrowsAsync( + run(gitTasks({branch: 'master'})), + ); }); diff --git a/test/hyperlinks.js b/test/hyperlinks.js index 963a9663..c5879174 100644 --- a/test/hyperlinks.js +++ b/test/hyperlinks.js @@ -1,7 +1,7 @@ import test from 'ava'; import sinon from 'sinon'; import terminalLink from 'terminal-link'; -import {linkifyIssues, linkifyCommit, linkifyCommitRange} from '../source/util'; +import {linkifyIssues, linkifyCommit, linkifyCommitRange} from '../source/util.js'; const MOCK_REPO_URL = 'https://github.com/unicorn/rainbow'; const MOCK_COMMIT_HASH = '5063f8a'; diff --git a/test/index.js b/test/index.js index d011b7be..dbf61745 100644 --- a/test/index.js +++ b/test/index.js @@ -1,7 +1,7 @@ import test from 'ava'; import sinon from 'sinon'; -import proxyquire from 'proxyquire'; -import np from '../source'; +import esmock from 'esmock'; +import np from '../source/index.js'; const defaultOptions = { cleanup: true, @@ -10,53 +10,55 @@ const defaultOptions = { runPublish: true, availability: { isAvailable: false, - isUnknown: false - } + isUnknown: false, + }, + renderer: 'silent', }; -test('version is invalid', async t => { - const message = 'Version should be either patch, minor, major, prepatch, preminor, premajor, prerelease, or a valid semver version.'; - await t.throwsAsync(np('foo', defaultOptions), message); - await t.throwsAsync(np('4.x.3', defaultOptions), message); +const npFails = test.macro(async (t, inputs, message) => { + await t.throwsAsync( + Promise.all(inputs.map(input => np(input, defaultOptions))), + {message}, + ); }); -test('version is pre-release', async t => { - const message = 'You must specify a dist-tag using --tag when publishing a pre-release version. This prevents accidentally tagging unstable versions as "latest". https://docs.npmjs.com/cli/dist-tag'; - await t.throwsAsync(np('premajor', defaultOptions), message); - await t.throwsAsync(np('preminor', defaultOptions), message); - await t.throwsAsync(np('prepatch', defaultOptions), message); - await t.throwsAsync(np('prerelease', defaultOptions), message); - await t.throwsAsync(np('10.0.0-0', defaultOptions), message); - await t.throwsAsync(np('10.0.0-beta', defaultOptions), message); -}); +test('version is invalid', npFails, + ['foo', '4.x.3'], + 'Version should be either patch, minor, major, prepatch, preminor, premajor, prerelease, or a valid semver version.', +); -test('errors on too low version', async t => { - await t.throwsAsync(np('1.0.0', defaultOptions), /New version `1\.0\.0` should be higher than current version `\d+\.\d+\.\d+`/); - await t.throwsAsync(np('1.0.0-beta', defaultOptions), /New version `1\.0\.0-beta` should be higher than current version `\d+\.\d+\.\d+`/); -}); +test('version is pre-release', npFails, + ['premajor', 'preminor', 'prepatch', 'prerelease', '10.0.0-0', '10.0.0-beta'], + 'You must specify a dist-tag using --tag when publishing a pre-release version. This prevents accidentally tagging unstable versions as "latest". https://docs.npmjs.com/cli/dist-tag', +); + +test('errors on too low version', npFails, + ['1.0.0', '1.0.0-beta'], + /New version `1\.0\.0(?:-beta)?` should be higher than current version `\d+\.\d+\.\d+`/, +); test('skip enabling 2FA if the package exists', async t => { const enable2faStub = sinon.stub(); - const np = proxyquire('../source', { - del: sinon.stub(), - execa: sinon.stub().returns({pipe: sinon.stub()}), - './prerequisite-tasks': sinon.stub(), - './git-tasks': sinon.stub(), - './git-util': { + const npMock = await esmock('../source/index.js', { + del: {deleteAsync: sinon.stub()}, + execa: {execa: sinon.stub().returns({pipe: sinon.stub()})}, + '../source/prerequisite-tasks.js': sinon.stub(), + '../source/git-tasks.js': sinon.stub(), + '../source/git-util.js': { hasUpstream: sinon.stub().returns(true), - pushGraceful: sinon.stub() + pushGraceful: sinon.stub(), }, - './npm/enable-2fa': enable2faStub, - './npm/publish': sinon.stub().returns({pipe: sinon.stub()}) - }); + '../source/npm/enable-2fa.js': enable2faStub, + '../source/npm/publish.js': sinon.stub().returns({pipe: sinon.stub()}), + }, {}); - await t.notThrowsAsync(np('1.0.0', { + await t.notThrowsAsync(npMock('1.0.0', { ...defaultOptions, availability: { isAvailable: false, - isUnknown: false - } + isUnknown: false, + }, })); t.true(enable2faStub.notCalled); @@ -65,26 +67,26 @@ test('skip enabling 2FA if the package exists', async t => { test('skip enabling 2FA if the `2fa` option is false', async t => { const enable2faStub = sinon.stub(); - const np = proxyquire('../source', { - del: sinon.stub(), - execa: sinon.stub().returns({pipe: sinon.stub()}), - './prerequisite-tasks': sinon.stub(), - './git-tasks': sinon.stub(), - './git-util': { + const npMock = await esmock('../source/index.js', { + del: {deleteAsync: sinon.stub()}, + execa: {execa: sinon.stub().returns({pipe: sinon.stub()})}, + '../source/prerequisite-tasks.js': sinon.stub(), + '../source/git-tasks.js': sinon.stub(), + '../source/git-util.js': { hasUpstream: sinon.stub().returns(true), - pushGraceful: sinon.stub() + pushGraceful: sinon.stub(), }, - './npm/enable-2fa': enable2faStub, - './npm/publish': sinon.stub().returns({pipe: sinon.stub()}) + '../source/npm/enable-2fa.js': enable2faStub, + '../source/npm/publish.js': sinon.stub().returns({pipe: sinon.stub()}), }); - await t.notThrowsAsync(np('1.0.0', { + await t.notThrowsAsync(npMock('1.0.0', { ...defaultOptions, availability: { isAvailable: true, - isUnknown: false + isUnknown: false, }, - '2fa': false + '2fa': false, })); t.true(enable2faStub.notCalled); diff --git a/test/integration.js b/test/integration.js index a26daab7..83d43d6e 100644 --- a/test/integration.js +++ b/test/integration.js @@ -1,11 +1,104 @@ -const test = require('ava'); -const execa = require('execa'); +/* eslint-disable ava/no-ignored-test-files */ +import process from 'node:process'; +import path from 'node:path'; +import fs from 'fs-extra'; +import test from 'ava'; +import {$} from 'execa'; +import {deleteAsync} from 'del'; +import * as gitUtil from '../source/git-util.js'; +import * as util from '../source/util.js'; + +test.before(async t => { + await fs.emptyDir('integration'); + process.chdir('integration'); + + await $`git init`; + await t.throwsAsync(gitUtil.latestTag(), undefined, 'prerequisites not met: repository should not contain any tags'); + + await fs.createFile('temp'); + await $`git add .`; + await $`git commit -m 'init'`; + await deleteAsync('temp'); +}); test.after.always(async () => { - await execa('git', ['submodule', 'update', '--remote']); + process.chdir('..'); + await deleteAsync('integration'); +}); + +test.afterEach.always(async t => { + if (typeof t.context.teardown === 'function') { + await t.context.teardown(); + } }); -test('Integration tests', async t => { - await execa('npx', ['ava'], {cwd: 'integration-test'}); - t.pass(); +test.serial('files to package with tags added', async t => { + await $`git tag v0.0.0`; + await fs.createFile('new'); + await fs.createFile('index.js'); + await $`git add new index.js`; + await $`git commit -m "added"`; + + t.context.teardown = async () => { + await $`git rm new`; + await $`git rm index.js`; + await $`git tag -d v0.0.0`; + await $`git commit -m "deleted"`; + }; + + t.deepEqual( + await util.getNewFiles({files: ['*.js']}), + {unpublished: ['new'], firstTime: ['index.js']}, + ); +}); + +test.serial.failing('file `new` to package without tags added', async t => { + await fs.createFile('new'); + await fs.createFile('index.js'); + + t.context.teardown = async () => { + await deleteAsync(['new', 'index.js']); + }; + + t.deepEqual( + await util.getNewFiles({files: ['index.js']}), + {unpublished: ['new'], firstTime: ['index.js']}, + ); +}); + +test.serial('files with long pathnames added', async t => { + const longPath = path.join('veryLonggggggDirectoryName', 'veryLonggggggDirectoryName'); + const filePath1 = path.join(longPath, 'file1'); + const filePath2 = path.join(longPath, 'file2'); + + await $`git tag v0.0.0`; + await fs.mkdir(longPath, {recursive: true}); + await fs.createFile(filePath1); + await fs.createFile(filePath2); + await $`git add ${filePath1} ${filePath2}`; + await $`git commit -m "added"`; + + t.context.teardown = async () => { + await $`git rm -r ${longPath}`; + await $`git tag -d v0.0.0`; + await $`git commit -m "deleted"`; + }; + + t.deepEqual( + await util.getNewFiles({files: ['*.js']}), + {unpublished: [filePath1, filePath2], firstTime: []}, + ); +}); + +test.serial('no new files added', async t => { + await $`git tag v0.0.0`; + + t.context.teardown = async () => { + await $`git tag -d v0.0.0`; + }; + + t.deepEqual( + await util.getNewFiles({files: ['*.js']}), + {unpublished: [], firstTime: []}, + ); }); diff --git a/test/npmignore.js b/test/npmignore.js index 8bb40c97..4f02492a 100644 --- a/test/npmignore.js +++ b/test/npmignore.js @@ -1,6 +1,6 @@ -import path from 'path'; +import path from 'node:path'; import test from 'ava'; -import proxyquire from 'proxyquire'; +import esmock from 'esmock'; const newFiles = [ 'source/ignore.txt', @@ -8,167 +8,111 @@ const newFiles = [ '.hg', 'test/file.txt', 'readme.md', - 'README.txt' + 'README.txt', ]; -test('ignored files using file-attribute in package.json with one file', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures', 'package') - } - }); - t.deepEqual(await testedModule.getNewAndUnpublishedFiles({files: ['pay_attention.txt']}, newFiles), ['source/ignore.txt']); -}); - -test('ignored file using file-attribute in package.json with directory', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures', 'package') - } - }); - t.deepEqual(await testedModule.getNewAndUnpublishedFiles({files: ['source']}, newFiles), []); -}); - -test('ignored test files using files attribute and directory structure in package.json', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures', 'package') - } - }); - t.deepEqual(await testedModule.getNewAndUnpublishedFiles({files: ['source'], directories: {test: 'test-tap'}}, newFiles), ['test/file.txt']); - t.deepEqual(await testedModule.getNewAndUnpublishedFiles({files: ['source'], directories: {test: ['test-tap']}}, newFiles), ['test/file.txt']); -}); - -test('ignored files using .npmignore', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures', 'npmignore') - } - }); - t.deepEqual(await testedModule.getNewAndUnpublishedFiles({name: 'npmignore'}, newFiles), ['source/ignore.txt']); -}); - -test('ignored test files using files attribute and .npmignore', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures', 'npmignore') - } - }); - t.deepEqual(await testedModule.getNewAndUnpublishedFiles({directories: {test: 'test-tap'}}, newFiles), ['source/ignore.txt', 'test/file.txt']); - t.deepEqual(await testedModule.getNewAndUnpublishedFiles({directories: {test: ['test-tap']}}, newFiles), ['source/ignore.txt', 'test/file.txt']); -}); - -test('ignored files - dot files using files attribute', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures', 'package') - } - }); - t.deepEqual(await testedModule.getNewAndUnpublishedFiles({files: ['source']}, ['test/.dotfile']), []); -}); - -test('ignored files - dot files using .npmignore', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures', 'npmignore') - } - }); - t.deepEqual(await testedModule.getNewAndUnpublishedFiles({}, ['test/.dot']), []); -}); - -test('ignored files - ignore strategy is not used', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures') - } - }); - t.deepEqual(await testedModule.getNewAndUnpublishedFiles({name: 'no ignore strategy'}, newFiles), []); -}); - -test('first time published files using file-attribute in package.json with one file', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures', 'package') - } - }); - t.deepEqual(await testedModule.getFirstTimePublishedFiles({files: ['pay_attention.txt']}, newFiles), ['source/pay_attention.txt']); -}); - -test('first time published files using file-attribute in package.json with directory', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures', 'package') - } - }); - t.deepEqual(await testedModule.getFirstTimePublishedFiles({files: ['source']}, newFiles), ['source/ignore.txt', 'source/pay_attention.txt']); -}); - -test('first time published files using .npmignore', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures', 'npmignore') - } - }); - t.deepEqual(await testedModule.getFirstTimePublishedFiles({name: 'npmignore'}, newFiles), ['source/pay_attention.txt']); -}); - -test('first time published dot files using files attribute', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures', 'package') - } - }); - t.deepEqual(await testedModule.getFirstTimePublishedFiles({files: ['source']}, ['source/.dotfile']), ['source/.dotfile']); -}); - -test('first time published dot files using .npmignore', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures', 'npmignore') - } - }); - t.deepEqual(await testedModule.getFirstTimePublishedFiles({}, ['source/.dotfile']), ['source/.dotfile']); -}); - -test('first time published files - ignore strategy is not used', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures') - } - }); - t.deepEqual(await testedModule.getFirstTimePublishedFiles({name: 'no ignore strategy'}, newFiles), ['source/ignore.txt', 'source/pay_attention.txt', 'test/file.txt']); -}); - -test('first time published files - empty files property', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures', 'package') - } - }); - t.deepEqual(await testedModule.getFirstTimePublishedFiles({files: []}, newFiles), []); -}); - -test('first time published files - .npmignore excludes everything', async t => { - const testedModule = proxyquire('../source/npm/util', { - 'pkg-dir': - { - sync: () => path.resolve('test', 'fixtures', 'npmignore') - } - }); - t.deepEqual(await testedModule.getFirstTimePublishedFiles({name: 'excluded everything'}, ['source/ignore.txt']), []); -}); +const mockPkgDir = test.macro(async (t, paths, impl) => { + const testedModule = await esmock('../source/npm/util.js', { + 'pkg-dir': {packageDirectorySync: () => path.resolve(...paths)}, + }); + + await impl(t, testedModule); +}); + +test.serial('ignored files using file-attribute in package.json with one file', mockPkgDir, ['test', 'fixtures', 'package'], + async (t, {getNewAndUnpublishedFiles}) => { + t.deepEqual(await getNewAndUnpublishedFiles({files: ['pay_attention.txt']}, newFiles), ['source/ignore.txt']); + }, +); + +test.serial('ignored file using file-attribute in package.json with directory', mockPkgDir, ['test', 'fixtures', 'package'], + async (t, {getNewAndUnpublishedFiles}) => { + t.deepEqual(await getNewAndUnpublishedFiles({files: ['source']}, newFiles), []); + }, +); + +test.serial('ignored test files using files attribute and directory structure in package.json', mockPkgDir, ['test', 'fixtures', 'package'], + async (t, {getNewAndUnpublishedFiles}) => { + t.deepEqual(await getNewAndUnpublishedFiles({files: ['source'], directories: {test: 'test-tap'}}, newFiles), ['test/file.txt']); + t.deepEqual(await getNewAndUnpublishedFiles({files: ['source'], directories: {test: ['test-tap']}}, newFiles), ['test/file.txt']); + }, +); + +test.serial('ignored files using .npmignore', mockPkgDir, ['test', 'fixtures', 'npmignore'], + async (t, {getNewAndUnpublishedFiles}) => { + t.deepEqual(await getNewAndUnpublishedFiles({name: 'npmignore'}, newFiles), ['source/ignore.txt']); + }, +); + +test.serial('ignored test files using files attribute and .npmignore', mockPkgDir, ['test', 'fixtures', 'npmignore'], + async (t, {getNewAndUnpublishedFiles}) => { + t.deepEqual(await getNewAndUnpublishedFiles({directories: {test: 'test-tap'}}, newFiles), ['source/ignore.txt', 'test/file.txt']); + t.deepEqual(await getNewAndUnpublishedFiles({directories: {test: ['test-tap']}}, newFiles), ['source/ignore.txt', 'test/file.txt']); + }, +); + +test.serial('ignored files - dot files using files attribute', mockPkgDir, ['test', 'fixtures', 'package'], + async (t, {getNewAndUnpublishedFiles}) => { + t.deepEqual(await getNewAndUnpublishedFiles({files: ['source']}, ['test/.dotfile']), []); + }, +); + +test.serial('ignored files - dot files using .npmignore', mockPkgDir, ['test', 'fixtures', 'npmignore'], + async (t, {getNewAndUnpublishedFiles}) => { + t.deepEqual(await getNewAndUnpublishedFiles({}, ['test/.dot']), []); + }, +); + +test.serial('ignored files - ignore strategy is not used', mockPkgDir, ['test', 'fixtures'], + async (t, {getNewAndUnpublishedFiles}) => { + t.deepEqual(await getNewAndUnpublishedFiles({name: 'no ignore strategy'}, newFiles), []); + }, +); + +test.serial('first time published files using file-attribute in package.json with one file', mockPkgDir, ['test', 'fixtures', 'package'], + async (t, {getFirstTimePublishedFiles}) => { + t.deepEqual(await getFirstTimePublishedFiles({files: ['pay_attention.txt']}, newFiles), ['source/pay_attention.txt']); + }, +); + +test.serial('first time published files using file-attribute in package.json with directory', mockPkgDir, ['test', 'fixtures', 'package'], + async (t, {getFirstTimePublishedFiles}) => { + t.deepEqual(await getFirstTimePublishedFiles({files: ['source']}, newFiles), ['source/ignore.txt', 'source/pay_attention.txt']); + }, +); + +test.serial('first time published files using .npmignore', mockPkgDir, ['test', 'fixtures', 'npmignore'], + async (t, {getFirstTimePublishedFiles}) => { + t.deepEqual(await getFirstTimePublishedFiles({name: 'npmignore'}, newFiles), ['source/pay_attention.txt']); + }, +); + +test.serial('first time published dot files using files attribute', mockPkgDir, ['test', 'fixtures', 'package'], + async (t, {getFirstTimePublishedFiles}) => { + t.deepEqual(await getFirstTimePublishedFiles({files: ['source']}, ['source/.dotfile']), ['source/.dotfile']); + }, +); + +test.serial('first time published dot files using .npmignore', mockPkgDir, ['test', 'fixtures', 'npmignore'], + async (t, {getFirstTimePublishedFiles}) => { + t.deepEqual(await getFirstTimePublishedFiles({}, ['source/.dotfile']), ['source/.dotfile']); + }, +); + +test.serial('first time published files - ignore strategy is not used', mockPkgDir, ['test', 'fixtures'], + async (t, {getFirstTimePublishedFiles}) => { + t.deepEqual(await getFirstTimePublishedFiles({name: 'no ignore strategy'}, newFiles), ['source/ignore.txt', 'source/pay_attention.txt', 'test/file.txt']); + }, +); + +test.serial('first time published files - empty files property', mockPkgDir, ['test', 'fixtures', 'package'], + async (t, {getFirstTimePublishedFiles}) => { + t.deepEqual(await getFirstTimePublishedFiles({files: []}, newFiles), []); + }, +); + +test.serial('first time published files - .npmignore excludes everything', mockPkgDir, ['test', 'fixtures', 'npmignore'], + async (t, {getFirstTimePublishedFiles}) => { + t.deepEqual(await getFirstTimePublishedFiles({name: 'excluded everything'}, ['source/ignore.txt']), []); + }, +); diff --git a/test/prefix.js b/test/prefix.js index 6ede56a6..7eb5ef8c 100644 --- a/test/prefix.js +++ b/test/prefix.js @@ -1,6 +1,7 @@ import test from 'ava'; -import proxyquire from 'proxyquire'; -import {getTagVersionPrefix} from '../source/util'; +import esmock from 'esmock'; +import {stripIndent} from 'common-tags'; +import {getTagVersionPrefix} from '../source/util.js'; test('get tag prefix', async t => { t.is(await getTagVersionPrefix({yarn: false}), 'v'); @@ -8,11 +9,17 @@ test('get tag prefix', async t => { }); test('no options passed', async t => { - await t.throwsAsync(getTagVersionPrefix(), {message: 'Expected `options` to be of type `object` but received type `undefined`'}); - await t.throwsAsync(getTagVersionPrefix({}), {message: 'Expected object `options` to have keys `["yarn"]`'}); + await t.throwsAsync(getTagVersionPrefix(), {message: stripIndent` + Expected argument to be of type \`object\` but received type \`undefined\` + Expected object to have keys \`["yarn"]\` + `}); + await t.throwsAsync(getTagVersionPrefix({}), {message: 'Expected object to have keys `["yarn"]`'}); }); test.serial('defaults to "v" when command fails', async t => { - proxyquire('../source/util', {execa: Promise.reject}); - t.is(await getTagVersionPrefix({yarn: true}), 'v'); + const testedModule = await esmock('../source/util.js', { + execa: {default: Promise.reject}, + }); + + t.is(await testedModule.getTagVersionPrefix({yarn: true}), 'v'); }); diff --git a/test/preid.js b/test/preid.js index 2bdf5295..b83bcd84 100644 --- a/test/preid.js +++ b/test/preid.js @@ -1,5 +1,6 @@ import test from 'ava'; -import {getPreReleasePrefix} from '../source/util'; +import {stripIndent} from 'common-tags'; +import {getPreReleasePrefix} from '../source/util.js'; test('get preId postfix', async t => { t.is(await getPreReleasePrefix({yarn: false}), ''); @@ -7,6 +8,9 @@ test('get preId postfix', async t => { }); test('no options passed', async t => { - await t.throwsAsync(getPreReleasePrefix(), {message: 'Expected `options` to be of type `object` but received type `undefined`'}); - await t.throwsAsync(getPreReleasePrefix({}), {message: 'Expected object `options` to have keys `["yarn"]`'}); + await t.throwsAsync(getPreReleasePrefix(), {message: stripIndent` + Expected argument to be of type \`object\` but received type \`undefined\` + Expected object to have keys \`["yarn"]\` + `}); + await t.throwsAsync(getPreReleasePrefix({}), {message: 'Expected object to have keys `["yarn"]`'}); }); diff --git a/test/prerequisite-tasks.js b/test/prerequisite-tasks.js index 7c42f8ee..e693e6f8 100644 --- a/test/prerequisite-tasks.js +++ b/test/prerequisite-tasks.js @@ -1,244 +1,295 @@ +import process from 'node:process'; import test from 'ava'; -import execaStub from 'execa_test_double'; -import mockery from 'mockery'; -import version from '../source/version'; -import {SilentRenderer} from './fixtures/listr-renderer'; +import {readPackageUp} from 'read-pkg-up'; +import Version from '../source/version.js'; +import actualPrerequisiteTasks from '../source/prerequisite-tasks.js'; +import {SilentRenderer} from './fixtures/listr-renderer.js'; +import { + _stubExeca, + run, + assertTaskFailed, + assertTaskDisabled, +} from './_utils.js'; -let testedModule; +/** @type {(...args: ReturnType<_stubExeca>) => Promise} */ +const stubExeca = _stubExeca('../source/prerequisite-tasks.js'); +const {packageJson: pkg} = await readPackageUp(); -const run = async listr => { - listr.setRenderer(SilentRenderer); - await listr.run(); -}; - -test.before(() => { - mockery.registerMock('execa', execaStub.execa); - mockery.enable({ - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - }); - testedModule = require('../source/prerequisite-tasks'); -}); - -test.beforeEach(() => { - execaStub.resetStub(); +test.afterEach(() => { + SilentRenderer.clearTasks(); }); test.serial('public-package published on npm registry: should fail when npm registry not pingable', async t => { - execaStub.createStub([{ + const prerequisiteTasks = await stubExeca(t, [{ command: 'npm ping', exitCode: 1, exitCodeName: 'EPERM', stdout: '', - stderr: 'failed' + stderr: 'failed', }]); - await t.throwsAsync(run(testedModule('1.0.0', {name: 'test'}, {})), - {message: 'Connection to npm registry failed'}); - t.true(SilentRenderer.tasks.some(task => task.title === 'Ping npm registry' && task.hasFailed())); + + await t.throwsAsync( + run(prerequisiteTasks('1.0.0', {name: 'test'}, {})), + {message: 'Connection to npm registry failed'}, + ); + + assertTaskFailed(t, 'Ping npm registry'); }); test.serial('private package: should disable task pinging npm registry', async t => { - execaStub.createStub([ - { - command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', - exitCode: 0, - stdout: '' - } - ]); - await run(testedModule('2.0.0', {name: 'test', version: '1.0.0', private: true}, {yarn: false})); - t.true(SilentRenderer.tasks.some(task => task.title === 'Ping npm registry' && !task.isEnabled())); + const prerequisiteTasks = await stubExeca(t, [{ + command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', + exitCode: 0, + stdout: '', + }]); + + await t.notThrowsAsync( + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0', private: true}, {yarn: false})), + ); + + assertTaskDisabled(t, 'Ping npm registry'); }); test.serial('external registry: should disable task pinging npm registry', async t => { - execaStub.createStub([ - { - command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', - exitCode: 0, - stdout: '' - } - ]); - await run(testedModule('2.0.0', {name: 'test', version: '1.0.0', publishConfig: {registry: 'http://my.io'}}, - {yarn: false})); - t.true(SilentRenderer.tasks.some(task => task.title === 'Ping npm registry' && !task.isEnabled())); + const prerequisiteTasks = await stubExeca(t, [{ + command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', + exitCode: 0, + stdout: '', + }]); + + await t.notThrowsAsync( + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0', publishConfig: {registry: 'http://my.io'}}, {yarn: false})), + ); + + assertTaskDisabled(t, 'Ping npm registry'); }); test.serial('should fail when npm version does not match range in `package.json`', async t => { - execaStub.createStub([ + const prerequisiteTasks = await stubExeca(t, [ { command: 'npm --version', exitCode: 0, - stdout: '6.0.0' + stdout: '6.0.0', }, { command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', exitCode: 0, - stdout: '' - } + stdout: '', + }, ]); - const depRange = require('../package.json').engines.npm; - await t.throwsAsync(run(testedModule('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), - {message: `Please upgrade to npm${depRange}`}); - t.true(SilentRenderer.tasks.some(task => task.title === 'Check npm version' && task.hasFailed())); + + const depRange = pkg.engines.npm; + + await t.throwsAsync( + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), + {message: `Please upgrade to npm${depRange}`}, + ); + + assertTaskFailed(t, 'Check npm version'); }); test.serial('should fail when yarn version does not match range in `package.json`', async t => { - execaStub.createStub([ + const prerequisiteTasks = await stubExeca(t, [ { command: 'yarn --version', exitCode: 0, - stdout: '1.0.0' + stdout: '1.0.0', }, { command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', exitCode: 0, - stdout: '' - } + stdout: '', + }, ]); - const depRange = require('../package.json').engines.yarn; - await t.throwsAsync(run(testedModule('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: true})), - {message: `Please upgrade to yarn${depRange}`}); - t.true(SilentRenderer.tasks.some(task => task.title === 'Check yarn version' && task.hasFailed())); + + const depRange = pkg.engines.yarn; + + await t.throwsAsync( + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: true})), + {message: `Please upgrade to yarn${depRange}`}, + ); + + assertTaskFailed(t, 'Check yarn version'); }); test.serial('should fail when user is not authenticated at npm registry', async t => { - execaStub.createStub([ + const prerequisiteTasks = await stubExeca(t, [ { command: 'npm whoami', exitCode: 0, - stdout: 'sindresorhus' + stdout: 'sindresorhus', }, { command: 'npm access ls-collaborators test', exitCode: 0, - stdout: '{"sindresorhus": "read"}' - } + stdout: '{"sindresorhus": "read"}', + }, ]); + process.env.NODE_ENV = 'P'; - await t.throwsAsync(run(testedModule('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), - {message: 'You do not have write permissions required to publish this package.'}); + + await t.throwsAsync( + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), + {message: 'You do not have write permissions required to publish this package.'}, + ); + process.env.NODE_ENV = 'test'; - t.true(SilentRenderer.tasks.some(task => task.title === 'Verify user is authenticated' && task.hasFailed())); + + assertTaskFailed(t, 'Verify user is authenticated'); }); test.serial('should fail when user is not authenticated at external registry', async t => { - execaStub.createStub([ + const prerequisiteTasks = await stubExeca(t, [ { command: 'npm whoami --registry http://my.io', exitCode: 0, - stdout: 'sindresorhus' + stdout: 'sindresorhus', }, { command: 'npm access ls-collaborators test --registry http://my.io', exitCode: 0, - stdout: '{"sindresorhus": "read"}' - } + stdout: '{"sindresorhus": "read"}', + }, + { + command: 'npm access list collaborators test --json --registry http://my.io', + exitCode: 0, + stdout: '{"sindresorhus": "read"}', + }, ]); + process.env.NODE_ENV = 'P'; - await t.throwsAsync(run(testedModule('2.0.0', {name: 'test', version: '1.0.0', publishConfig: {registry: 'http://my.io'}}, {yarn: false})), - {message: 'You do not have write permissions required to publish this package.'}); + + await t.throwsAsync( + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0', publishConfig: {registry: 'http://my.io'}}, {yarn: false})), + {message: 'You do not have write permissions required to publish this package.'}, + ); + process.env.NODE_ENV = 'test'; - t.true(SilentRenderer.tasks.some(task => task.title === 'Verify user is authenticated' && task.hasFailed())); + + assertTaskFailed(t, 'Verify user is authenticated'); }); test.serial('private package: should disable task `verify user is authenticated`', async t => { - execaStub.createStub([ - { - command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', - exitCode: 0, - stdout: '' - } - ]); + const prerequisiteTasks = await stubExeca(t, [{ + command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', + exitCode: 0, + stdout: '', + }]); + process.env.NODE_ENV = 'P'; - await run(testedModule('2.0.0', {name: 'test', version: '1.0.0', private: true}, {yarn: false})); + + await t.notThrowsAsync( + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0', private: true}, {yarn: false})), + ); + process.env.NODE_ENV = 'test'; - t.true(SilentRenderer.tasks.some(task => task.title === 'Verify user is authenticated' && !task.isEnabled())); + + assertTaskDisabled(t, 'Verify user is authenticated'); }); test.serial('should fail when git version does not match range in `package.json`', async t => { - execaStub.createStub([ - { - command: 'git version', - exitCode: 0, - stdout: 'git version 1.0.0' - } - ]); - const depRange = require('../package.json').engines.git; - await t.throwsAsync(run(testedModule('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), - {message: `Please upgrade to git${depRange}`}); - t.true(SilentRenderer.tasks.some(task => task.title === 'Check git version' && task.hasFailed())); + const prerequisiteTasks = await stubExeca(t, [{ + command: 'git version', + exitCode: 0, + stdout: 'git version 1.0.0', + }]); + + const depRange = pkg.engines.git; + + await t.throwsAsync( + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), + {message: `Please upgrade to git${depRange}`}, + ); + + assertTaskFailed(t, 'Check git version'); }); -test.serial('should fail when git remote does not exists', async t => { - execaStub.createStub([ - { - command: 'git ls-remote origin HEAD', - exitCode: 1, - exitCodeName: 'EPERM', - stderr: 'not found' - } - ]); - await t.throwsAsync(run(testedModule('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), - {message: 'not found'}); - t.true(SilentRenderer.tasks.some(task => task.title === 'Check git remote' && task.hasFailed())); +test.serial('should fail when git remote does not exist', async t => { + const prerequisiteTasks = await stubExeca(t, [{ + command: 'git ls-remote origin HEAD', + exitCode: 1, + exitCodeName: 'EPERM', + stderr: 'not found', + }]); + + await t.throwsAsync( + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), + {message: 'not found'}, + ); + + assertTaskFailed(t, 'Check git remote'); }); test.serial('should fail when version is invalid', async t => { - await t.throwsAsync(run(testedModule('DDD', {name: 'test', version: '1.0.0'}, {yarn: false})), - {message: `Version should be either ${version.SEMVER_INCREMENTS.join(', ')}, or a valid semver version.`}); - t.true(SilentRenderer.tasks.some(task => task.title === 'Validate version' && task.hasFailed())); + await t.throwsAsync( + run(actualPrerequisiteTasks('DDD', {name: 'test', version: '1.0.0'}, {yarn: false})), + {message: `Version should be either ${Version.SEMVER_INCREMENTS.join(', ')}, or a valid semver version.`}, + ); + + assertTaskFailed(t, 'Validate version'); }); test.serial('should fail when version is lower as latest version', async t => { - await t.throwsAsync(run(testedModule('0.1.0', {name: 'test', version: '1.0.0'}, {yarn: false})), - {message: 'New version `0.1.0` should be higher than current version `1.0.0`'}); - t.true(SilentRenderer.tasks.some(task => task.title === 'Validate version' && task.hasFailed())); + await t.throwsAsync( + run(actualPrerequisiteTasks('0.1.0', {name: 'test', version: '1.0.0'}, {yarn: false})), + {message: 'New version `0.1.0` should be higher than current version `1.0.0`'}, + ); + + assertTaskFailed(t, 'Validate version'); }); test.serial('should fail when prerelease version of public package without dist tag given', async t => { - await t.throwsAsync(run(testedModule('2.0.0-1', {name: 'test', version: '1.0.0'}, {yarn: false})), - {message: 'You must specify a dist-tag using --tag when publishing a pre-release version. This prevents accidentally tagging unstable versions as "latest". https://docs.npmjs.com/cli/dist-tag'}); - t.true(SilentRenderer.tasks.some(task => task.title === 'Check for pre-release version' && task.hasFailed())); + await t.throwsAsync( + run(actualPrerequisiteTasks('2.0.0-1', {name: 'test', version: '1.0.0'}, {yarn: false})), + {message: 'You must specify a dist-tag using --tag when publishing a pre-release version. This prevents accidentally tagging unstable versions as "latest". https://docs.npmjs.com/cli/dist-tag'}, + ); + + assertTaskFailed(t, 'Check for pre-release version'); }); test.serial('should not fail when prerelease version of public package with dist tag given', async t => { - execaStub.createStub([ - { - command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', - stdout: '' - } - ]); - await t.notThrowsAsync(run(testedModule('2.0.0-1', {name: 'test', version: '1.0.0'}, {yarn: false, tag: 'pre'}))); + const prerequisiteTasks = await stubExeca(t, [{ + command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', + stdout: '', + }]); + + await t.notThrowsAsync( + run(prerequisiteTasks('2.0.0-1', {name: 'test', version: '1.0.0'}, {yarn: false, tag: 'pre'})), + ); }); test.serial('should not fail when prerelease version of private package without dist tag given', async t => { - execaStub.createStub([ - { - command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', - stdout: '' - } - ]); - await t.notThrowsAsync(run(testedModule('2.0.0-1', {name: 'test', version: '1.0.0', private: true}, {yarn: false}))); + const prerequisiteTasks = await stubExeca(t, [{ + command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', + stdout: '', + }]); + + await t.notThrowsAsync( + run(prerequisiteTasks('2.0.0-1', {name: 'test', version: '1.0.0', private: true}, {yarn: false})), + ); }); test.serial('should fail when git tag already exists', async t => { - execaStub.createStub([ - { - command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', - stdout: 'vvb' - } - ]); - await t.throwsAsync(run(testedModule('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), - {message: 'Git tag `v2.0.0` already exists.'}); - t.true(SilentRenderer.tasks.some(task => task.title === 'Check git tag existence' && task.hasFailed())); + const prerequisiteTasks = await stubExeca(t, [{ + command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', + stdout: 'vvb', + }]); + + await t.throwsAsync( + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), + {message: 'Git tag `v2.0.0` already exists.'}, + ); + + assertTaskFailed(t, 'Check git tag existence'); }); test.serial('checks should pass', async t => { - execaStub.createStub([ - { - command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', - stdout: '' - } - ]); - await t.notThrowsAsync(run(testedModule('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false}))); + const prerequisiteTasks = await stubExeca(t, [{ + command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', + stdout: '', + }]); + + await t.notThrowsAsync( + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), + ); }); diff --git a/test/version.js b/test/version.js index 07b8c056..203fb4e2 100644 --- a/test/version.js +++ b/test/version.js @@ -1,139 +1,139 @@ import test from 'ava'; -import version from '../source/version'; +import Version from '../source/version.js'; test('version.SEMVER_INCREMENTS', t => { - t.deepEqual(version.SEMVER_INCREMENTS, ['patch', 'minor', 'major', 'prepatch', 'preminor', 'premajor', 'prerelease']); + t.deepEqual(Version.SEMVER_INCREMENTS, ['patch', 'minor', 'major', 'prepatch', 'preminor', 'premajor', 'prerelease']); }); test('version.PRERELEASE_VERSIONS', t => { - t.deepEqual(version.PRERELEASE_VERSIONS, ['prepatch', 'preminor', 'premajor', 'prerelease']); + t.deepEqual(Version.PRERELEASE_VERSIONS, ['prepatch', 'preminor', 'premajor', 'prerelease']); }); test('version.isValidInput', t => { - t.false(version.isValidInput(null)); - t.false(version.isValidInput('foo')); - t.false(version.isValidInput('1.0.0.0')); - - t.true(version.isValidInput('patch')); - t.true(version.isValidInput('minor')); - t.true(version.isValidInput('major')); - t.true(version.isValidInput('prepatch')); - t.true(version.isValidInput('preminor')); - t.true(version.isValidInput('premajor')); - t.true(version.isValidInput('prerelease')); - t.true(version.isValidInput('1.0.0')); - t.true(version.isValidInput('1.1.0')); - t.true(version.isValidInput('1.0.1')); - t.true(version.isValidInput('1.0.0-beta')); - t.true(version.isValidInput('2.0.0-rc.2')); + t.false(Version.isValidInput(null)); + t.false(Version.isValidInput('foo')); + t.false(Version.isValidInput('1.0.0.0')); + + t.true(Version.isValidInput('patch')); + t.true(Version.isValidInput('minor')); + t.true(Version.isValidInput('major')); + t.true(Version.isValidInput('prepatch')); + t.true(Version.isValidInput('preminor')); + t.true(Version.isValidInput('premajor')); + t.true(Version.isValidInput('prerelease')); + t.true(Version.isValidInput('1.0.0')); + t.true(Version.isValidInput('1.1.0')); + t.true(Version.isValidInput('1.0.1')); + t.true(Version.isValidInput('1.0.0-beta')); + t.true(Version.isValidInput('2.0.0-rc.2')); }); test('version.isPrerelease', t => { - t.false(version('1.0.0').isPrerelease()); - t.false(version('1.1.0').isPrerelease()); - t.false(version('1.0.1').isPrerelease()); + t.false(new Version('1.0.0').isPrerelease()); + t.false(new Version('1.1.0').isPrerelease()); + t.false(new Version('1.0.1').isPrerelease()); - t.true(version('1.0.0-beta').isPrerelease()); - t.true(version('2.0.0-rc.2').isPrerelease()); + t.true(new Version('1.0.0-beta').isPrerelease()); + t.true(new Version('2.0.0-rc.2').isPrerelease()); }); test('version.isPrereleaseOrIncrement', t => { - t.false(version.isPrereleaseOrIncrement('patch')); - t.false(version.isPrereleaseOrIncrement('minor')); - t.false(version.isPrereleaseOrIncrement('major')); - - t.true(version.isPrereleaseOrIncrement('prepatch')); - t.true(version.isPrereleaseOrIncrement('preminor')); - t.true(version.isPrereleaseOrIncrement('premajor')); - t.true(version.isPrereleaseOrIncrement('prerelease')); + t.false(Version.isPrereleaseOrIncrement('patch')); + t.false(Version.isPrereleaseOrIncrement('minor')); + t.false(Version.isPrereleaseOrIncrement('major')); + + t.true(Version.isPrereleaseOrIncrement('prepatch')); + t.true(Version.isPrereleaseOrIncrement('preminor')); + t.true(Version.isPrereleaseOrIncrement('premajor')); + t.true(Version.isPrereleaseOrIncrement('prerelease')); }); test('version.getNewVersionFrom', t => { const message = 'Version should be either patch, minor, major, prepatch, preminor, premajor, prerelease, or a valid semver version.'; - t.throws(() => version('1.0.0').getNewVersionFrom('patchxxx'), message); - t.throws(() => version('1.0.0').getNewVersionFrom('1.0.0.0'), message); + t.throws(() => new Version('1.0.0').getNewVersionFrom('patchxxx'), {message}); + t.throws(() => new Version('1.0.0').getNewVersionFrom('1.0.0.0'), {message}); - t.is(version('1.0.0').getNewVersionFrom('patch'), '1.0.1'); - t.is(version('1.0.0').getNewVersionFrom('minor'), '1.1.0'); - t.is(version('1.0.0').getNewVersionFrom('major'), '2.0.0'); + t.is(new Version('1.0.0').getNewVersionFrom('patch'), '1.0.1'); + t.is(new Version('1.0.0').getNewVersionFrom('minor'), '1.1.0'); + t.is(new Version('1.0.0').getNewVersionFrom('major'), '2.0.0'); - t.is(version('1.0.0-beta').getNewVersionFrom('major'), '1.0.0'); - t.is(version('1.0.0').getNewVersionFrom('prepatch'), '1.0.1-0'); - t.is(version('1.0.1-0').getNewVersionFrom('prepatch'), '1.0.2-0'); + t.is(new Version('1.0.0-beta').getNewVersionFrom('major'), '1.0.0'); + t.is(new Version('1.0.0').getNewVersionFrom('prepatch'), '1.0.1-0'); + t.is(new Version('1.0.1-0').getNewVersionFrom('prepatch'), '1.0.2-0'); - t.is(version('1.0.0-0').getNewVersionFrom('prerelease'), '1.0.0-1'); - t.is(version('1.0.1-0').getNewVersionFrom('prerelease'), '1.0.1-1'); + t.is(new Version('1.0.0-0').getNewVersionFrom('prerelease'), '1.0.0-1'); + t.is(new Version('1.0.1-0').getNewVersionFrom('prerelease'), '1.0.1-1'); }); test('version.validate', t => { const message = 'Version should be a valid semver version.'; - t.throws(() => version.validate('patch'), message); - t.throws(() => version.validate('patchxxx'), message); - t.throws(() => version.validate('1.0.0.0'), message); + t.throws(() => Version.validate('patch'), {message}); + t.throws(() => Version.validate('patchxxx'), {message}); + t.throws(() => Version.validate('1.0.0.0'), {message}); - t.notThrows(() => version.validate('1.0.0')); - t.notThrows(() => version.validate('1.0.0-beta')); - t.notThrows(() => version.validate('1.0.0-0')); + t.notThrows(() => Version.validate('1.0.0')); + t.notThrows(() => Version.validate('1.0.0-beta')); + t.notThrows(() => Version.validate('1.0.0-0')); }); test('version.isGreaterThanOrEqualTo', t => { - t.false(version('1.0.0').isGreaterThanOrEqualTo('0.0.1')); - t.false(version('1.0.0').isGreaterThanOrEqualTo('0.1.0')); + t.false(new Version('1.0.0').isGreaterThanOrEqualTo('0.0.1')); + t.false(new Version('1.0.0').isGreaterThanOrEqualTo('0.1.0')); - t.false(version('1.0.0').isGreaterThanOrEqualTo('1.0.0-0')); - t.false(version('1.0.0').isGreaterThanOrEqualTo('1.0.0-beta')); + t.false(new Version('1.0.0').isGreaterThanOrEqualTo('1.0.0-0')); + t.false(new Version('1.0.0').isGreaterThanOrEqualTo('1.0.0-beta')); - t.true(version('1.0.0').isGreaterThanOrEqualTo('1.0.0')); - t.true(version('1.0.0').isGreaterThanOrEqualTo('1.0.1')); - t.true(version('1.0.0').isGreaterThanOrEqualTo('1.1.0')); - t.true(version('1.0.0').isGreaterThanOrEqualTo('2.0.0')); + t.true(new Version('1.0.0').isGreaterThanOrEqualTo('1.0.0')); + t.true(new Version('1.0.0').isGreaterThanOrEqualTo('1.0.1')); + t.true(new Version('1.0.0').isGreaterThanOrEqualTo('1.1.0')); + t.true(new Version('1.0.0').isGreaterThanOrEqualTo('2.0.0')); - t.true(version('1.0.0').isGreaterThanOrEqualTo('2.0.0-0')); - t.true(version('1.0.0').isGreaterThanOrEqualTo('2.0.0-beta')); + t.true(new Version('1.0.0').isGreaterThanOrEqualTo('2.0.0-0')); + t.true(new Version('1.0.0').isGreaterThanOrEqualTo('2.0.0-beta')); }); test('version.isLowerThanOrEqualTo', t => { - t.true(version('1.0.0').isLowerThanOrEqualTo('0.0.1')); - t.true(version('1.0.0').isLowerThanOrEqualTo('0.1.0')); + t.true(new Version('1.0.0').isLowerThanOrEqualTo('0.0.1')); + t.true(new Version('1.0.0').isLowerThanOrEqualTo('0.1.0')); - t.true(version('1.0.0').isLowerThanOrEqualTo('1.0.0-0')); - t.true(version('1.0.0').isLowerThanOrEqualTo('1.0.0-beta')); - t.true(version('1.0.0').isLowerThanOrEqualTo('1.0.0')); + t.true(new Version('1.0.0').isLowerThanOrEqualTo('1.0.0-0')); + t.true(new Version('1.0.0').isLowerThanOrEqualTo('1.0.0-beta')); + t.true(new Version('1.0.0').isLowerThanOrEqualTo('1.0.0')); - t.false(version('1.0.0').isLowerThanOrEqualTo('1.0.1')); - t.false(version('1.0.0').isLowerThanOrEqualTo('1.1.0')); - t.false(version('1.0.0').isLowerThanOrEqualTo('2.0.0')); + t.false(new Version('1.0.0').isLowerThanOrEqualTo('1.0.1')); + t.false(new Version('1.0.0').isLowerThanOrEqualTo('1.1.0')); + t.false(new Version('1.0.0').isLowerThanOrEqualTo('2.0.0')); - t.false(version('1.0.0').isLowerThanOrEqualTo('2.0.0-0')); - t.false(version('1.0.0').isLowerThanOrEqualTo('2.0.0-beta')); + t.false(new Version('1.0.0').isLowerThanOrEqualTo('2.0.0-0')); + t.false(new Version('1.0.0').isLowerThanOrEqualTo('2.0.0-beta')); }); test('version.satisfies', t => { - t.true(version('2.15.8').satisfies('>=2.15.8 <3.0.0 || >=3.10.1')); - t.true(version('2.99.8').satisfies('>=2.15.8 <3.0.0 || >=3.10.1')); - t.true(version('3.10.1').satisfies('>=2.15.8 <3.0.0 || >=3.10.1')); - t.true(version('6.7.0-next.0').satisfies('<6.8.0')); - t.false(version('3.0.0').satisfies('>=2.15.8 <3.0.0 || >=3.10.1')); - t.false(version('3.10.0').satisfies('>=2.15.8 <3.0.0 || >=3.10.1')); + t.true(new Version('2.15.8').satisfies('>=2.15.8 <3.0.0 || >=3.10.1')); + t.true(new Version('2.99.8').satisfies('>=2.15.8 <3.0.0 || >=3.10.1')); + t.true(new Version('3.10.1').satisfies('>=2.15.8 <3.0.0 || >=3.10.1')); + t.true(new Version('6.7.0-next.0').satisfies('<6.8.0')); + t.false(new Version('3.0.0').satisfies('>=2.15.8 <3.0.0 || >=3.10.1')); + t.false(new Version('3.10.0').satisfies('>=2.15.8 <3.0.0 || >=3.10.1')); }); test('version.getAndValidateNewVersionFrom', t => { - t.is(version.getAndValidateNewVersionFrom('patch', '1.0.0'), '1.0.1'); + t.is(Version.getAndValidateNewVersionFrom('patch', '1.0.0'), '1.0.1'); t.throws( - () => version.getAndValidateNewVersionFrom('patch', '1'), - 'Version should be a valid semver version.' + () => Version.getAndValidateNewVersionFrom('patch', '1'), + {message: 'Version should be a valid semver version.'}, ); t.throws( - () => version.getAndValidateNewVersionFrom('lol', '1.0.0'), - `Version should be either ${version.SEMVER_INCREMENTS.join(', ')}, or a valid semver version.` + () => Version.getAndValidateNewVersionFrom('lol', '1.0.0'), + {message: `Version should be either ${Version.SEMVER_INCREMENTS.join(', ')}, or a valid semver version.`}, ); t.throws( - () => version.getAndValidateNewVersionFrom('1.0.0', '2.0.0'), - 'New version `1.0.0` should be higher than current version `2.0.0`' + () => Version.getAndValidateNewVersionFrom('1.0.0', '2.0.0'), + {message: 'New version `1.0.0` should be higher than current version `2.0.0`'}, ); });