From 8119ed80a5cdeec89b9ffe12e16109850bf60e65 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sat, 1 Apr 2023 14:28:02 -0500 Subject: [PATCH 01/22] refactor: move to ESM, update dependencies --- package.json | 86 +++++---- source/cli-implementation.js | 38 ++-- source/cli.js | 12 +- source/config.js | 15 +- source/git-tasks.js | 9 +- source/git-util.js | 85 +++++---- source/index.js | 82 +++++---- source/npm/enable-2fa.js | 16 +- source/npm/handle-npm-error.js | 12 +- source/npm/publish.js | 16 +- source/npm/util.js | 94 +++++----- source/prerequisite-tasks.js | 40 ++-- source/pretty-version-diff.js | 11 +- source/release-task-helper.js | 17 +- source/ui.js | 210 +++++++++++---------- source/util.js | 40 ++-- source/version.js | 58 +++--- test/_utils.js | 51 ++++++ test/config.js | 122 ++++++------- test/fixtures/listr-renderer.js | 12 +- test/git-tasks.js | 116 ++++++------ test/hyperlinks.js | 2 +- test/index.js | 83 +++++---- test/integration.js | 4 +- test/npmignore.js | 270 +++++++++++---------------- test/prefix.js | 19 +- test/preid.js | 10 +- test/prerequisite-tasks.js | 311 ++++++++++++++++++-------------- test/version.js | 148 +++++++-------- 29 files changed, 1029 insertions(+), 960 deletions(-) create mode 100644 test/_utils.js diff --git a/package.json b/package.json index 28c8493b..7917eb4d 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,10 @@ "license": "MIT", "repository": "sindresorhus/np", "funding": "https://github.com/sindresorhus/np?sponsor=1", + "type": "module", "bin": "source/cli.js", "engines": { - "node": ">=10", + "node": ">=14.18", "npm": ">=6.8.0", "git": ">=2.11.0", "yarn": ">=1.7.0" @@ -33,60 +34,65 @@ "@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", "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", + "log-symbols": "^5.1.0", + "meow": "^11.0.0", + "minimatch": "^7.4.3", + "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": "^6.6.3", - "semver": "^7.3.4", + "semver": "^7.3.8", "split": "^1.0.1", - "symbol-observable": "^3.0.0", - "terminal-link": "^2.1.1", - "update-notifier": "^5.0.1" + "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", + "sinon": "^15.0.3", + "xo": "^0.53.1" }, "ava": { "files": [ - "!test/fixtures", "!integration-test" + ], + "nodeArguments": [ + "--loader=esmock" ] }, "xo": { "ignores": [ "integration-test" - ] + ], + "rules": { + "comma-dangle": "off", + "object-shorthand": ["error", "properties"] + } } } diff --git a/source/cli-implementation.js b/source/cli-implementation.js index 9177095e..6f47bdfa 100755 --- a/source/cli-implementation.js +++ b/source/cli-implementation.js @@ -1,25 +1,24 @@ #!/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'; // eslint-disable-line import/no-unassigned-import +import process from 'node:process'; +import logSymbols from 'log-symbols'; +import meow from 'meow'; +import updateNotifier from 'update-notifier'; +import hasYarn from 'has-yarn'; +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,6 +44,7 @@ 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: { @@ -97,7 +97,7 @@ const cli = meow(` updateNotifier({pkg: cli.pkg}).notify(); -(async () => { +try { const pkg = util.readPkg(); const defaultFlags = { @@ -149,11 +149,11 @@ updateNotifier({pkg: cli.pkg}).notify(); const newPkg = await np(options.version, options); if (options.preview || options.releaseDraftOnly) { - return; + process.exit(0); } console.log(`\n ${newPkg.name} ${newPkg.version} published 🎉`); -})().catch(error => { +} catch (error) { console.error(`\n${logSymbols.error} ${error.message}`); process.exit(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..2199e96b 100644 --- a/source/config.js +++ b/source/config.js @@ -1,11 +1,10 @@ -'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 getConfig = async () => { + const searchDir = isInstalledGlobally ? os.homedir() : await packageDirectory(); const searchPlaces = ['.np-config.json', '.np-config.js', '.np-config.cjs']; if (!isInstalledGlobally) { searchPlaces.push('package.json'); @@ -19,3 +18,5 @@ module.exports = async () => { return config; }; + +export default getConfig; diff --git a/source/git-tasks.js b/source/git-tasks.js index 8f7376f6..d6d2c244 100644 --- a/source/git-tasks.js +++ b/source/git-tasks.js @@ -1,8 +1,7 @@ -'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', @@ -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 f9f28d1f..8121c82f 100644 --- a/source/git-util.js +++ b/source/git-util.js @@ -1,16 +1,15 @@ -'use strict'; -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 {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.newFilesSinceLastRelease = async () => { +export const newFilesSinceLastRelease = async () => { try { const {stdout} = await execa('git', ['diff', '--name-only', '--diff-filter=A', await this.latestTag(), 'HEAD']); if (stdout.trim().length === 0) { @@ -22,7 +21,7 @@ exports.newFilesSinceLastRelease = async () => { } catch { // Get all files under version control return ignoreWalker({ - path: pkgDir.sync(), + path: packageDirectorySync(), ignoreFiles: ['.gitignore'] }); } @@ -33,8 +32,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; @@ -46,7 +45,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 { @@ -55,11 +54,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(); @@ -68,32 +67,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']); @@ -103,7 +102,7 @@ exports.isHeadDetached = async () => { } }; -exports.isWorkingTreeClean = async () => { +export const isWorkingTreeClean = async () => { try { const {stdout: status} = await execa('git', ['status', '--porcelain']); if (status !== '') { @@ -116,13 +115,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']); @@ -136,13 +135,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) { @@ -150,11 +149,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}`]); @@ -188,7 +187,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)) { @@ -201,20 +200,20 @@ exports.defaultBranch = async () => { ); }; -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 @@ -226,15 +225,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']); }; @@ -244,13 +243,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 b20dec43..2f219a78 100644 --- a/source/index.js +++ b/source/index.js @@ -1,29 +1,28 @@ -'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 'any-observable/register/rxjs-all.js'; +import fs from 'node:fs'; +import path from 'node:path'; +import {execa} from 'execa'; +import {deleteAsync} from 'del'; +import Listr from 'listr'; +import split from 'split'; +import {merge, throwError} from 'rxjs'; +import {catchError, filter, finalize} from 'rxjs/operators/index.js'; +import streamToObservable from '@samverschueren/stream-to-observable'; +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 exitHook from 'async-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 @@ -36,8 +35,8 @@ const exec = (cmd, args) => { ).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 +46,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 +74,8 @@ module.exports = async (input = 'patch', options) => { const versionInLatestTag = latestTag.slice(tagVersionPrefix.length); try { - if (versionInLatestTag === util.readPkg().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(); } @@ -115,7 +114,8 @@ module.exports = async (input = 'patch', options) => { task: () => gitTasks(options) } ], { - showSubtasks: false + showSubtasks: false, + renderer: options.renderer ?? 'default' }); if (runCleanup) { @@ -123,13 +123,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; @@ -141,8 +141,8 @@ 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', @@ -315,6 +315,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..de13d5c4 100644 --- a/source/npm/enable-2fa.js +++ b/source/npm/enable-2fa.js @@ -1,10 +1,9 @@ -'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} from 'rxjs'; +import {catchError} from 'rxjs/operators/index.js'; +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 +15,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}))) ); +}; -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..ba5b584e 100644 --- a/source/npm/handle-npm-error.js +++ b/source/npm/handle-npm-error.js @@ -1,7 +1,7 @@ -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} from 'rxjs'; +import {catchError} from 'rxjs/operators/index.js'; const handleNpmError = (error, task, message, executor) => { if (typeof message === 'function') { @@ -15,7 +15,7 @@ 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); }, @@ -34,4 +34,4 @@ const handleNpmError = (error, task, message, executor) => { return throwError(error); }; -module.exports = handleNpmError; +export default handleNpmError; diff --git a/source/npm/publish.js b/source/npm/publish.js index 0a4ed4ee..607cd9b4 100644 --- a/source/npm/publish.js +++ b/source/npm/publish.js @@ -1,10 +1,9 @@ -'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} from 'rxjs'; +import {catchError} from 'rxjs/operators/index.js'; +import handleNpmError from './handle-npm-error.js'; -const getPackagePublishArguments = options => { +export const getPackagePublishArguments = options => { const args = ['publish']; if (options.contents) { @@ -28,7 +27,7 @@ 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; @@ -36,5 +35,6 @@ module.exports = (context, pkgManager, task, options) => 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..69464bb3 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. @@ -35,7 +34,7 @@ const filesIgnoredByDefault = [ '.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,14 +120,14 @@ 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 }; - if (exports.isExternalRegistry(pkg)) { + if (isExternalRegistry(pkg)) { args.push({ 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(), + let allowList = await ignoreWalker({ + path: packageDirectorySync(), ignoreFiles: ['.npmignore'] - })).filter(singlePath => excludeGitAndNodeModulesPaths(singlePath)); + }); + allowList = allowList.filter(singlePath => excludeGitAndNodeModulesPaths(singlePath)); return fileList.filter(minimatch.filter(getIgnoredFilesGlob(allowList, pkg.directories), {matchBase: true, dot: true})); } @@ -185,12 +186,12 @@ 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(), + path: packageDirectorySync(), ignoreFiles: ['.npmignore'] }); return filterFileList(allowList, fileList); @@ -198,7 +199,7 @@ async function getFilesIncludedByDotnpmignore(pkg, 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()) { @@ -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 065c6a35..5c955922 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; @@ -23,15 +23,15 @@ module.exports = (input, pkg, options) => { { 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 }); @@ -58,29 +58,29 @@ module.exports = (input, pkg, options) => { }, { title: 'Validate version', - task: () => { - if (!version.isValidInput(input)) { - throw new Error(`Version should be either ${version.SEMVER_INCREMENTS.join(', ')}, or a valid semver version.`); + task() { + if (!Version.isValidInput(input)) { + throw new Error(`Version should be either ${Version.SEMVER_INCREMENTS.join(', ')}, or a valid semver version.`); } - newVersion = version(pkg.version).getNewVersionFrom(input); + newVersion = new Version(pkg.version).getNewVersionFrom(input); - if (version(pkg.version).isLowerThanOrEqualTo(newVersion)) { + if (new Version(pkg.version).isLowerThanOrEqualTo(newVersion)) { throw new Error(`New version \`${newVersion}\` should be higher than current version \`${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); @@ -92,3 +92,5 @@ module.exports = (input, pkg, options) => { 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..22a2af95 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); } @@ -21,3 +20,5 @@ module.exports = async (options, pkg) => { await open(url); }; + +export default releaseTaskHelper; diff --git a/source/ui.js b/source/ui.js index 4d4001ba..15d74d92 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(); @@ -109,7 +108,8 @@ const checkNewFiles = async pkg => { return answers.confirm; }; -module.exports = async (options, pkg) => { +// eslint-disable-next-line complexity +const ui = async (options, pkg) => { const oldVersion = pkg.version; const extraBaseUrls = ['gitlab.com']; const repoUrl = pkg.repository && githubUrlFromGit(pkg.repository.url, {extraBaseUrls}); @@ -135,50 +135,109 @@ module.exports = async (options, pkg) => { console.log(`\nPublish a new version of ${chalk.bold.magenta(pkg.name)} ${chalk.dim(`(current: ${oldVersion})`)}\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 + })), + 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 [ @@ -191,12 +250,11 @@ module.exports = async (options, pkg) => { ]; } }, - { + 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`.'; } @@ -208,77 +266,13 @@ module.exports = async (options, pkg) => { 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); + }); return { ...options, @@ -290,3 +284,5 @@ module.exports = async (options, pkg) => { releaseNotes }; }; + +export default ui; diff --git a/source/util.js b/source/util.js index 79d329c9..e8d4b6d7 100644 --- a/source/util.js +++ b/source/util.js @@ -1,29 +1,27 @@ -'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 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 {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} = readPkgUp.sync({ + const {packageJson} = await readPackageUp({ cwd: packagePath }); return packageJson; }; -exports.linkifyIssues = (url, message) => { +export const linkifyIssues = (url, message) => { if (!(url && terminalLink.isSupported)) { return message; } @@ -39,7 +37,7 @@ exports.linkifyIssues = (url, message) => { }); }; -exports.linkifyCommit = (url, commit) => { +export const linkifyCommit = (url, commit) => { if (!(url && terminalLink.isSupported)) { return commit; } @@ -47,7 +45,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; } @@ -55,7 +53,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 { @@ -71,12 +69,12 @@ exports.getTagVersionPrefix = pMemoize(async options => { } }); -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.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 c12d5425..0043e03c 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,56 +13,54 @@ class Version { } satisfies(range) { - module.exports.validate(this.version); + Version.validate(this.version); return semver.satisfies(this.version, range, { 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}`); + } } -}; +} diff --git a/test/_utils.js b/test/_utils.js new file mode 100644 index 00000000..0ed8eebf --- /dev/null +++ b/test/_utils.js @@ -0,0 +1,51 @@ +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: { + execa: async (...args) => { + for (const result of commands) { + // Console.log(result); + // eslint-disable-next-line no-await-in-loop + 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(); + } + + return 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..2350c581 100644 --- a/test/config.js +++ b/test/config.js @@ -1,90 +1,84 @@ -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.failing('returns config from home directory when global binary is used and `.np-config.js` 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('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.failing('returns config from package directory when local binary is used and `.np-config.js` 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' +); 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..0dc9c243 100644 --- a/test/git-tasks.js +++ b/test/git-tasks.js @@ -1,57 +1,50 @@ 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'; + +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, @@ -68,12 +61,16 @@ test.serial('should not fail when current branch not master and publishing from 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, @@ -85,12 +82,17 @@ test.serial('should fail when local working tree modified', async t => { 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, @@ -107,12 +109,17 @@ test.serial('should fail when remote history differs', async t => { 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, @@ -129,5 +136,8 @@ test.serial('checks should pass when publishing from master, working tree is cle 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..944e1a44 100644 --- a/test/index.js +++ b/test/index.js @@ -1,7 +1,10 @@ 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'; + +// FIXME: +// somehow running this is deleting node_modules const defaultOptions = { cleanup: true, @@ -11,47 +14,49 @@ const defaultOptions = { availability: { isAvailable: 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() }, - './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, @@ -65,20 +70,20 @@ 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() }, - './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, diff --git a/test/integration.js b/test/integration.js index a26daab7..58ea5ff0 100644 --- a/test/integration.js +++ b/test/integration.js @@ -1,5 +1,5 @@ -const test = require('ava'); -const execa = require('execa'); +import test from 'ava'; +import {execa} from 'execa'; test.after.always(async () => { await execa('git', ['submodule', 'update', '--remote']); diff --git a/test/npmignore.js b/test/npmignore.js index 8bb40c97..70dbd905 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', @@ -11,164 +11,108 @@ const newFiles = [ '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..e6c24b24 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..07ec2a4e 100644 --- a/test/prerequisite-tasks.js +++ b/test/prerequisite-tasks.js @@ -1,70 +1,70 @@ +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; +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' }]); - 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, @@ -76,14 +76,19 @@ test.serial('should fail when npm version does not match range in `package.json` 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, @@ -95,14 +100,19 @@ test.serial('should fail when yarn version does not match range in `package.json 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, @@ -114,15 +124,21 @@ test.serial('should fail when user is not authenticated at npm registry', async 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, @@ -134,111 +150,140 @@ test.serial('should fail when user is not authenticated at external registry', a 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())); + 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 48cf4189..42fffbda 100644 --- a/test/version.js +++ b/test/version.js @@ -1,120 +1,120 @@ 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')); }); From 2b86dc49329c1958eecb7d834604d4cebfddffb1 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sat, 1 Apr 2023 14:29:29 -0500 Subject: [PATCH 02/22] chore: move images to `media/` --- private-packages.png => media/private-packages.png | Bin screenshot-ui.png => media/screenshot-ui.png | Bin screenshot.gif => media/screenshot.gif | Bin readme.md | 6 +++--- 4 files changed, 3 insertions(+), 3 deletions(-) rename private-packages.png => media/private-packages.png (100%) rename screenshot-ui.png => media/screenshot-ui.png (100%) rename screenshot.gif => media/screenshot.gif (100%) 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/readme.md b/readme.md index de2d6736..98659bb9 100644 --- a/readme.md +++ b/readme.md @@ -22,7 +22,7 @@ --- - + ## Why @@ -104,7 +104,7 @@ $ np --help Run `np` without arguments to launch the interactive UI that guides you through publishing a new version. - + ## Config @@ -226,7 +226,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). From a814ee468464b8ccd7b81fd90586775407c45cd8 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sat, 1 Apr 2023 14:47:09 -0500 Subject: [PATCH 03/22] chore: merge upstream changes and update to esm --- package.json | 2 +- source/cli-implementation.js | 4 ++-- source/git-util.js | 14 +++++++++++++- source/index.js | 2 +- source/prerequisite-tasks.js | 12 ++---------- source/ui.js | 29 ++++++++++++++++++++++------- source/util.js | 22 ++++++++++++++++++++-- source/version.js | 12 +++++++++++- test/index.js | 3 --- test/version.js | 21 ++++++++++++++++++++- 10 files changed, 92 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 7917eb4d..697f7d5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "np", - "version": "7.6.4", + "version": "7.7.0", "description": "A better `npm publish`", "license": "MIT", "repository": "sindresorhus/np", diff --git a/source/cli-implementation.js b/source/cli-implementation.js index 6f47bdfa..10304617 100755 --- a/source/cli-implementation.js +++ b/source/cli-implementation.js @@ -98,7 +98,7 @@ const cli = meow(` updateNotifier({pkg: cli.pkg}).notify(); try { - const pkg = util.readPkg(); + const {pkg, pkgPath} = await util.readPkg(); const defaultFlags = { cleanup: true, @@ -139,7 +139,7 @@ try { version, runPublish, branch - }, pkg); + }, {pkg, pkgPath}); if (!options.confirm) { process.exit(0); diff --git a/source/git-util.js b/source/git-util.js index 8121c82f..95bb9e65 100644 --- a/source/git-util.js +++ b/source/git-util.js @@ -1,3 +1,4 @@ +import path from 'node:path'; import {execa} from 'execa'; import escapeStringRegexp from 'escape-string-regexp'; import ignoreWalker from 'ignore-walk'; @@ -9,9 +10,14 @@ export const latestTag = async () => { return stdout; }; +export const root = async () => { + const {stdout} = await execa('git', ['rev-parse', '--show-toplevel']); + return stdout; +}; + 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 []; } @@ -27,6 +33,12 @@ export const newFilesSinceLastRelease = async () => { } }; +export const readFileFromLastRelease = async file => { + const filePathFromRoot = path.relative(await root(), file); + const {stdout: oldFile} = await execa('git', ['show', `${await latestTag()}:${filePathFromRoot}`]); + return oldFile; +}; + const firstCommit = async () => { const {stdout} = await execa('git', ['rev-list', '--max-parents=0', 'HEAD']); return stdout; diff --git a/source/index.js b/source/index.js index 2f219a78..7fad960b 100644 --- a/source/index.js +++ b/source/index.js @@ -46,7 +46,7 @@ const np = async (input = 'patch', options) => { options.cleanup = false; } - const pkg = await 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'; diff --git a/source/prerequisite-tasks.js b/source/prerequisite-tasks.js index 5c955922..ca7b562a 100644 --- a/source/prerequisite-tasks.js +++ b/source/prerequisite-tasks.js @@ -58,16 +58,8 @@ const prerequisiteTasks = (input, pkg, options) => { }, { title: 'Validate version', - task() { - if (!Version.isValidInput(input)) { - throw new Error(`Version should be either ${Version.SEMVER_INCREMENTS.join(', ')}, or a valid semver version.`); - } - - newVersion = new Version(pkg.version).getNewVersionFrom(input); - - if (new Version(pkg.version).isLowerThanOrEqualTo(newVersion)) { - throw new Error(`New version \`${newVersion}\` should be higher than current version \`${pkg.version}\``); - } + task: () => { + newVersion = Version.getAndValidateNewVersionFrom(input, pkg.version); } }, { diff --git a/source/ui.js b/source/ui.js index 15d74d92..bf77b8b9 100644 --- a/source/ui.js +++ b/source/ui.js @@ -78,19 +78,31 @@ const printCommitLog = async (repoUrl, registryUrl, fromLatestTag, releaseBranch }; }; -const checkNewFiles = async pkg => { +const checkNewFilesAndDependencies = async (pkg, pkgPath) => { const newFiles = await util.getNewFiles(pkg); - if ((!newFiles.unpublished || newFiles.unpublished.length === 0) && (!newFiles.firstTime || newFiles.firstTime.length === 0)) { + const newDependencies = await util.getNewDependencies(pkg, pkgPath); + + const noNewUnpublishedFiles = !newFiles.unpublished || newFiles.unpublished.length === 0; + const noNewFirstTimeFiles = !newFiles.firstTime || newFiles.firstTime.length === 0; + const noNewFiles = noNewUnpublishedFiles && noNewFirstTimeFiles; + + const noNewDependencies = !newDependencies || newDependencies.length === 0; + + if (noNewFiles && noNewDependencies) { return true; } const messages = []; if (newFiles.unpublished.length > 0) { - messages.push(`The following new files will not be part of your published package:\n${chalk.reset(newFiles.unpublished.map(path => `- ${path}`).join('\n'))}`); + messages.push(`The following new files will not be part of your published package:\n${util.joinList(newFiles.unpublished)}`); } if (newFiles.firstTime.length > 0) { - messages.push(`The following new files will be published the first time:\n${chalk.reset(newFiles.firstTime.map(path => `- ${path}`).join('\n'))}`); + messages.push(`The following new files will be published for the first time:\n${util.joinList(newFiles.firstTime)}`); + } + + if (newDependencies.length > 0) { + messages.push(`The following new dependencies will be part of your published package:\n${util.joinList(newDependencies)}`); } if (!isInteractive()) { @@ -109,7 +121,7 @@ const checkNewFiles = async pkg => { }; // eslint-disable-next-line complexity -const ui = async (options, pkg) => { +const ui = async (options, {pkg, pkgPath}) => { const oldVersion = pkg.version; const extraBaseUrls = ['gitlab.com']; const repoUrl = pkg.repository && githubUrlFromGit(pkg.repository.url, {extraBaseUrls}); @@ -120,7 +132,7 @@ const ui = async (options, pkg) => { if (options.runPublish) { checkIgnoreStrategy(pkg); - const answerIgnoredFiles = await checkNewFiles(pkg); + const answerIgnoredFiles = await checkNewFilesAndDependencies(pkg, pkgPath); if (!answerIgnoredFiles) { return { ...options, @@ -132,7 +144,10 @@ const ui = async (options, pkg) => { if (options.releaseDraftOnly) { console.log(`\nCreate a release draft on GitHub for ${chalk.bold.magenta(pkg.name)} ${chalk.dim(`(current: ${oldVersion})`)}\n`); } else { - console.log(`\nPublish a new version of ${chalk.bold.magenta(pkg.name)} ${chalk.dim(`(current: ${oldVersion})`)}\n`); + 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 useLatestTag = !options.releaseDraftOnly; diff --git a/source/util.js b/source/util.js index e8d4b6d7..fd7cea3a 100644 --- a/source/util.js +++ b/source/util.js @@ -4,6 +4,7 @@ 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'; @@ -14,11 +15,11 @@ export const readPkg = async packagePath => { throw new Error('No `package.json` found. Make sure the current directory is a valid package.'); } - const {packageJson} = await readPackageUp({ + const {packageJson, path} = await readPackageUp({ cwd: packagePath }); - return packageJson; + return {pkg: packageJson, pkgPath: path}; }; export const linkifyIssues = (url, message) => { @@ -69,11 +70,28 @@ export const getTagVersionPrefix = pMemoize(async options => { } }); +export const joinList = list => chalk.reset(list.map(item => `- ${item}`).join('\n')); + export const getNewFiles = async pkg => { const listNewFiles = await gitUtil.newFilesSinceLastRelease(); return {unpublished: await npmUtil.getNewAndUnpublishedFiles(pkg, listNewFiles), firstTime: await npmUtil.getFirstTimePublishedFiles(pkg, listNewFiles)}; }; +export const getNewDependencies = async (newPkg, pkgPath) => { + let oldPkg = await gitUtil.readFileFromLastRelease(pkgPath); + oldPkg = JSON.parse(oldPkg); + + const newDependencies = []; + + for (const dependency of Object.keys(newPkg.dependencies)) { + if (!oldPkg.dependencies[dependency]) { + newDependencies.push(dependency); + } + } + + return newDependencies; +}; + export const getPreReleasePrefix = pMemoize(async options => { ow(options, ow.object.hasKeys('yarn')); diff --git a/source/version.js b/source/version.js index 0043e03c..5dd05ed5 100644 --- a/source/version.js +++ b/source/version.js @@ -22,7 +22,7 @@ export default class Version { getNewVersionFrom(input) { Version.validate(this.version); if (!Version.isValidInput(input)) { - throw new Error(`Version should be either ${Version.SEMVER_INCREMENTS.join(', ')} or a valid semver version.`); + throw new Error(`Version should be either ${Version.SEMVER_INCREMENTS.join(', ')}, or a valid semver version.`); } return Version.SEMVER_INCREMENTS.includes(input) ? semver.inc(this.version, input) : input; @@ -63,4 +63,14 @@ export default class Version { throw new Error(`Please upgrade to ${dependency}${depRange}`); } } + + static getAndValidateNewVersionFrom(input, version) { + const newVersion = new Version(version).getNewVersionFrom(input); + + if (new Version(version).isLowerThanOrEqualTo(newVersion)) { + throw new Error(`New version \`${newVersion}\` should be higher than current version \`${version}\``); + } + + return newVersion; + } } diff --git a/test/index.js b/test/index.js index 944e1a44..27027c75 100644 --- a/test/index.js +++ b/test/index.js @@ -3,9 +3,6 @@ import sinon from 'sinon'; import esmock from 'esmock'; import np from '../source/index.js'; -// FIXME: -// somehow running this is deleting node_modules - const defaultOptions = { cleanup: true, tests: true, diff --git a/test/version.js b/test/version.js index 42fffbda..6a545799 100644 --- a/test/version.js +++ b/test/version.js @@ -49,7 +49,7 @@ test('version.isPrereleaseOrIncrement', t => { }); test('version.getNewVersionFrom', t => { - const message = 'Version should be either patch, minor, major, prepatch, preminor, premajor, prerelease or a valid semver version.'; + const message = 'Version should be either patch, minor, major, prepatch, preminor, premajor, prerelease, or a valid semver version.'; t.throws(() => new Version('1.0.0').getNewVersionFrom('patchxxx'), {message}); t.throws(() => new Version('1.0.0').getNewVersionFrom('1.0.0.0'), {message}); @@ -118,3 +118,22 @@ test('version.satisfies', t => { 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.throws( + () => Version.getAndValidateNewVersionFrom('patch', '1'), + {message: 'Version should be a valid semver version.'} + ); + + t.throws( + () => 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'), + {message: 'New version `1.0.0` should be higher than current version `2.0.0`'} + ); +}); From 9507bb03da6a1f175617082bb652df320a78bea4 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sat, 1 Apr 2023 15:07:06 -0500 Subject: [PATCH 04/22] chore(`integration`): mark test as failing --- test/integration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration.js b/test/integration.js index 58ea5ff0..0b0ac569 100644 --- a/test/integration.js +++ b/test/integration.js @@ -5,7 +5,7 @@ test.after.always(async () => { await execa('git', ['submodule', 'update', '--remote']); }); -test('Integration tests', async t => { +test.failing('Integration tests', async t => { await execa('npx', ['ava'], {cwd: 'integration-test'}); t.pass(); }); From 3d86bba2e6c40606d8b93a2e68439371e0b77169 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sat, 1 Apr 2023 15:08:50 -0500 Subject: [PATCH 05/22] fix(`version`): duplicate test from merge --- test/version.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/test/version.js b/test/version.js index dfe232f0..6a545799 100644 --- a/test/version.js +++ b/test/version.js @@ -137,22 +137,3 @@ test('version.getAndValidateNewVersionFrom', t => { {message: 'New version `1.0.0` should be higher than current version `2.0.0`'} ); }); - -test('version.getAndValidateNewVersionFrom', t => { - t.is(version.getAndValidateNewVersionFrom('patch', '1.0.0'), '1.0.1'); - - t.throws( - () => version.getAndValidateNewVersionFrom('patch', '1'), - '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.` - ); - - t.throws( - () => version.getAndValidateNewVersionFrom('1.0.0', '2.0.0'), - 'New version `1.0.0` should be higher than current version `2.0.0`' - ); -}); From c65cc9e3e5ab29b172750348c5c3ff4ba742e44e Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sat, 1 Apr 2023 15:09:10 -0500 Subject: [PATCH 06/22] chore: bump Node versions for tests --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 28f5aa6c..41502822 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,9 +10,9 @@ jobs: fail-fast: false matrix: node-version: - - 14 - - 12 - - 10 + - 19 + - 18 + - 16 steps: - uses: actions/checkout@v2 with: From 6e124899ef251326222074f464bc67a75589430e Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sat, 1 Apr 2023 15:14:22 -0500 Subject: [PATCH 07/22] fix: task format --- source/prerequisite-tasks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/prerequisite-tasks.js b/source/prerequisite-tasks.js index ca7b562a..9e74bd11 100644 --- a/source/prerequisite-tasks.js +++ b/source/prerequisite-tasks.js @@ -23,7 +23,7 @@ const prerequisiteTasks = (input, pkg, options) => { { title: 'Check yarn version', enabled: () => options.yarn === true, - async task() { + task: async () => { const {stdout: yarnVersion} = await execa('yarn', ['--version']); Version.verifyRequirementSatisfied('yarn', yarnVersion); } @@ -31,7 +31,7 @@ const prerequisiteTasks = (input, pkg, options) => { { title: 'Verify user is authenticated', enabled: () => process.env.NODE_ENV !== 'test' && !pkg.private, - async task() { + task: async () => { const username = await npm.username({ externalRegistry: isExternalRegistry ? pkg.publishConfig.registry : false }); From 91c04fd55d91b77cc848bea9033d2b6893f4df1a Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sat, 1 Apr 2023 15:18:57 -0500 Subject: [PATCH 08/22] fix(`prereq-tasks`): add command for higher `npm` versions --- test/prerequisite-tasks.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/prerequisite-tasks.js b/test/prerequisite-tasks.js index 07ec2a4e..153bccf0 100644 --- a/test/prerequisite-tasks.js +++ b/test/prerequisite-tasks.js @@ -148,6 +148,11 @@ test.serial('should fail when user is not authenticated at external registry', a command: 'npm access ls-collaborators test --registry http://my.io', exitCode: 0, stdout: '{"sindresorhus": "read"}' + }, + { + command: 'npm access list collaborators test --json --registry http://my.io', + exitCode: 0, + stdout: '{"sindresorhus": "read"}' } ]); From 815e3e0c1e1105518df70b20c9f8b2a3db830f84 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sat, 1 Apr 2023 22:03:42 -0500 Subject: [PATCH 09/22] tests(`stubExeca`): improve concurrency --- package.json | 5 ++++- test/_utils.js | 9 ++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 697f7d5e..e362742a 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,10 @@ ], "rules": { "comma-dangle": "off", - "object-shorthand": ["error", "properties"] + "object-shorthand": [ + "error", + "properties" + ] } } } diff --git a/test/_utils.js b/test/_utils.js index 0ed8eebf..1a7b5def 100644 --- a/test/_utils.js +++ b/test/_utils.js @@ -5,9 +5,7 @@ import {SilentRenderer} from './fixtures/listr-renderer.js'; export const _stubExeca = source => async (t, commands) => esmock(source, {}, { execa: { execa: async (...args) => { - for (const result of commands) { - // Console.log(result); - // eslint-disable-next-line no-await-in-loop + 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]); @@ -24,9 +22,10 @@ export const _stubExeca = source => async (t, commands) => esmock(source, {}, { } argsMatch.discard(); - } + })); - return execa(...args); + const result = results.filter(Boolean).at(0); + return result ?? execa(...args); } } }); From 1369cd0fae40e0d28e44431ef0ba72c766b6ee37 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sat, 1 Apr 2023 22:04:08 -0500 Subject: [PATCH 10/22] fix: typo in test title --- test/prerequisite-tasks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/prerequisite-tasks.js b/test/prerequisite-tasks.js index 153bccf0..fff28dad 100644 --- a/test/prerequisite-tasks.js +++ b/test/prerequisite-tasks.js @@ -203,7 +203,7 @@ test.serial('should fail when git version does not match range in `package.json` assertTaskFailed(t, 'Check git version'); }); -test.serial('should fail when git remote does not exists', async t => { +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, From a1907a063be28852134f39fb78307f8333fbd795 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sat, 1 Apr 2023 22:56:03 -0500 Subject: [PATCH 11/22] tests(`tasks`): type stub function --- test/git-tasks.js | 1 + test/prerequisite-tasks.js | 1 + 2 files changed, 2 insertions(+) diff --git a/test/git-tasks.js b/test/git-tasks.js index 0dc9c243..6576a55c 100644 --- a/test/git-tasks.js +++ b/test/git-tasks.js @@ -7,6 +7,7 @@ import { assertTaskDoesntExist } from './_utils.js'; +/** @type {(...args: ReturnType<_stubExeca>) => Promise} */ const stubExeca = _stubExeca('../source/git-tasks.js'); test.afterEach(() => { diff --git a/test/prerequisite-tasks.js b/test/prerequisite-tasks.js index fff28dad..d0a72dca 100644 --- a/test/prerequisite-tasks.js +++ b/test/prerequisite-tasks.js @@ -11,6 +11,7 @@ import { assertTaskDisabled } from './_utils.js'; +/** @type {(...args: ReturnType<_stubExeca>) => Promise} */ const stubExeca = _stubExeca('../source/prerequisite-tasks.js'); const {packageJson: pkg} = await readPackageUp(); From 210ec0ebdbad9f8993ec02b259555b1557ce4e1d Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sun, 2 Apr 2023 13:12:46 +0900 Subject: [PATCH 12/22] Update main.yml --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 41502822..12f53e50 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,10 +14,10 @@ jobs: - 18 - 16 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: submodules: true - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: git config --global user.name "Github Actions" From 4c89ef47d9574c502560d1dd2254e5a934395895 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sat, 1 Apr 2023 23:28:39 -0500 Subject: [PATCH 13/22] chore: target Node 16 --- package.json | 4 ++-- readme.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e362742a..f5af1f3a 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "type": "module", "bin": "source/cli.js", "engines": { - "node": ">=14.18", - "npm": ">=6.8.0", + "node": ">=16.6.0", + "npm": ">=7.19.0", "git": ">=2.11.0", "yarn": ">=1.7.0" }, diff --git a/readme.md b/readme.md index 98659bb9..d8d4a732 100644 --- a/readme.md +++ b/readme.md @@ -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 From b3d2b2ba809e14d61c9c6c66598c2382fbc99b8c Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sun, 2 Apr 2023 14:37:14 -0500 Subject: [PATCH 14/22] feat(`config`): support ESM configs if extension is `.mjs` or `"type": "module"` --- source/config.js | 14 ++++++++++++-- test/config.js | 20 ++++++++++++++++++-- test/fixtures/config/homedir4/.np-config.js | 3 +++ test/fixtures/config/homedir5/.np-config.mjs | 3 +++ test/fixtures/config/local4/.np-config.js | 3 +++ test/fixtures/config/local4/package.json | 4 ++++ test/fixtures/config/local5/.np-config.mjs | 3 +++ test/fixtures/config/package.json | 3 +++ 8 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/config/homedir4/.np-config.js create mode 100644 test/fixtures/config/homedir5/.np-config.mjs create mode 100644 test/fixtures/config/local4/.np-config.js create mode 100644 test/fixtures/config/local4/package.json create mode 100644 test/fixtures/config/local5/.np-config.mjs create mode 100644 test/fixtures/config/package.json diff --git a/source/config.js b/source/config.js index 2199e96b..a7b7cd03 100644 --- a/source/config.js +++ b/source/config.js @@ -3,16 +3,26 @@ import isInstalledGlobally from 'is-installed-globally'; import {packageDirectory} from 'pkg-dir'; import {cosmiconfig} from 'cosmiconfig'; +// 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']; + 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)) || {}; diff --git a/test/config.js b/test/config.js index 2350c581..f9e4a703 100644 --- a/test/config.js +++ b/test/config.js @@ -59,7 +59,7 @@ test('returns config from home directory when global binary is used and .np-conf useGlobalBinary, 'homedir1', 'homedir/.np-config.json' ); -test.failing('returns config from home directory when global binary is used and `.np-config.js` exists in home directory', +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' ); @@ -67,6 +67,14 @@ test('returns config from home directory when global binary is used and `.np-con 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' ); @@ -75,10 +83,18 @@ test('returns config from package directory when local binary is used and `.np-c useLocalBinary, 'local1', 'packagedir/.np-config.json' ); -test.failing('returns config from package directory when local binary is used and `.np-config.js` exists in package directory', +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" +} From d113991bdb07ddff9018ed2d62eaf7c7ebae76dc Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sun, 2 Apr 2023 14:43:08 -0500 Subject: [PATCH 15/22] docs(`config`): document `.js` and `.mjs` uses --- readme.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index d8d4a732..f04dcf7a 100644 --- a/readme.md +++ b/readme.md @@ -108,7 +108,7 @@ Run `np` without arguments to launch the interactive UI that guides you through ## 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 From b3e4c5168ce7fada8526f1b38d2a4447b13389eb Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sun, 2 Apr 2023 14:56:48 -0500 Subject: [PATCH 16/22] chore: drop `async-exit-hook` for `exit-hook` --- package.json | 2 +- source/cli-implementation.js | 8 ++++---- source/index.js | 23 +++++++++-------------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index f5af1f3a..fce7a317 100644 --- a/package.json +++ b/package.json @@ -33,13 +33,13 @@ "dependencies": { "@samverschueren/stream-to-observable": "^0.3.1", "any-observable": "^0.5.1", - "async-exit-hook": "^2.0.1", "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": "^3.0.0", "hosted-git-info": "^6.1.1", diff --git a/source/cli-implementation.js b/source/cli-implementation.js index 10304617..b92188bc 100755 --- a/source/cli-implementation.js +++ b/source/cli-implementation.js @@ -1,10 +1,10 @@ #!/usr/bin/env node import 'symbol-observable'; // eslint-disable-line import/no-unassigned-import -import process from 'node:process'; 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'; @@ -142,18 +142,18 @@ try { }, {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) { - process.exit(0); + gracefulExit(); } console.log(`\n ${newPkg.name} ${newPkg.version} published 🎉`); } catch (error) { console.error(`\n${logSymbols.error} ${error.message}`); - process.exit(1); + gracefulExit(1); } diff --git a/source/index.js b/source/index.js index 7fad960b..e201983e 100644 --- a/source/index.js +++ b/source/index.js @@ -13,7 +13,7 @@ import hasYarn from 'has-yarn'; import {packageDirectorySync} from 'pkg-dir'; import hostedGitInfo from 'hosted-git-info'; import onetime from 'onetime'; -import exitHook from 'async-exit-hook'; +import {asyncExitHook} from 'exit-hook'; import logSymbols from 'log-symbols'; import prerequisiteTasks from './prerequisite-tasks.js'; import gitTasks from './git-tasks.js'; @@ -86,22 +86,17 @@ const np = 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([ { From d20d682b65d3babb0189302171327bf4a52f2091 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sun, 2 Apr 2023 15:22:08 -0500 Subject: [PATCH 17/22] chore: update `rxjs`, drop other Observable libs --- package.json | 5 +---- source/index.js | 14 +++----------- source/npm/enable-2fa.js | 3 +-- source/npm/handle-npm-error.js | 5 ++--- source/npm/publish.js | 3 +-- 5 files changed, 8 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index fce7a317..fd3a9a3b 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,6 @@ "commit" ], "dependencies": { - "@samverschueren/stream-to-observable": "^0.3.1", - "any-observable": "^0.5.1", "chalk": "^5.2.0", "cosmiconfig": "^8.1.3", "del": "^7.0.0", @@ -64,9 +62,8 @@ "p-timeout": "^6.1.1", "pkg-dir": "^7.0.0", "read-pkg-up": "^9.1.0", - "rxjs": "^6.6.3", + "rxjs": "^7.8.0", "semver": "^7.3.8", - "split": "^1.0.1", "symbol-observable": "^4.0.0", "terminal-link": "^3.0.0", "update-notifier": "^6.0.2" diff --git a/source/index.js b/source/index.js index e201983e..1756f8e5 100644 --- a/source/index.js +++ b/source/index.js @@ -1,13 +1,9 @@ -import 'any-observable/register/rxjs-all.js'; import fs from 'node:fs'; import path from 'node:path'; import {execa} from 'execa'; import {deleteAsync} from 'del'; import Listr from 'listr'; -import split from 'split'; -import {merge, throwError} from 'rxjs'; -import {catchError, filter, finalize} from 'rxjs/operators/index.js'; -import streamToObservable from '@samverschueren/stream-to-observable'; +import {merge, throwError, catchError, filter, finalize} from 'rxjs'; import {readPackageUp} from 'read-pkg-up'; import hasYarn from 'has-yarn'; import {packageDirectorySync} from 'pkg-dir'; @@ -28,11 +24,7 @@ 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 complexity @@ -166,7 +158,7 @@ const np = async (input = 'patch', options) => { return []; } - return throwError(error); + return throwError(() => error); }) ) } diff --git a/source/npm/enable-2fa.js b/source/npm/enable-2fa.js index de13d5c4..9256686f 100644 --- a/source/npm/enable-2fa.js +++ b/source/npm/enable-2fa.js @@ -1,6 +1,5 @@ import {execa} from 'execa'; -import {from} from 'rxjs'; -import {catchError} from 'rxjs/operators/index.js'; +import {from, catchError} from 'rxjs'; import handleNpmError from './handle-npm-error.js'; export const getEnable2faArgs = (packageName, options) => { diff --git a/source/npm/handle-npm-error.js b/source/npm/handle-npm-error.js index ba5b584e..480d68a8 100644 --- a/source/npm/handle-npm-error.js +++ b/source/npm/handle-npm-error.js @@ -1,7 +1,6 @@ import listrInput from 'listr-input'; import chalk from 'chalk'; -import {throwError} from 'rxjs'; -import {catchError} from 'rxjs/operators/index.js'; +import {throwError, catchError} from 'rxjs'; const handleNpmError = (error, task, message, executor) => { if (typeof message === 'function') { @@ -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); }; export default handleNpmError; diff --git a/source/npm/publish.js b/source/npm/publish.js index 607cd9b4..a0d97ada 100644 --- a/source/npm/publish.js +++ b/source/npm/publish.js @@ -1,6 +1,5 @@ import {execa} from 'execa'; -import {from} from 'rxjs'; -import {catchError} from 'rxjs/operators/index.js'; +import {from, catchError} from 'rxjs'; import handleNpmError from './handle-npm-error.js'; export const getPackagePublishArguments = options => { From 6ebde872a274df23d3083a491daeb11120c0f490 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sun, 2 Apr 2023 15:25:58 -0500 Subject: [PATCH 18/22] chore: bump `minimatch` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fd3a9a3b..c8d99da5 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "listr-input": "^0.2.1", "log-symbols": "^5.1.0", "meow": "^11.0.0", - "minimatch": "^7.4.3", + "minimatch": "^8.0.2", "new-github-release-url": "^2.0.0", "npm-name": "^7.1.0", "onetime": "^6.0.0", From 3f32977301507b022e148c03ea6c8ad920500890 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sun, 2 Apr 2023 15:27:02 -0500 Subject: [PATCH 19/22] fix(`symbol-observable`): re-add comment --- source/cli-implementation.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/cli-implementation.js b/source/cli-implementation.js index b92188bc..bd9c0577 100755 --- a/source/cli-implementation.js +++ b/source/cli-implementation.js @@ -1,5 +1,6 @@ #!/usr/bin/env node -import 'symbol-observable'; // eslint-disable-line import/no-unassigned-import +// eslint-disable-next-line import/no-unassigned-import +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'; From a0527684f246352d3854d720cd2a7796633a2e76 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sun, 2 Apr 2023 17:32:10 -0500 Subject: [PATCH 20/22] tests(`integration`): don't use git submodule --- .github/workflows/main.yml | 2 - .gitmodules | 3 -- integration-test | 1 - package.json | 8 ++- test/integration.js | 103 +++++++++++++++++++++++++++++++++++-- 5 files changed, 101 insertions(+), 16 deletions(-) delete mode 100644 .gitmodules delete mode 160000 integration-test diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 12f53e50..2d58c737 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,8 +15,6 @@ jobs: - 16 steps: - uses: actions/checkout@v3 - with: - submodules: true - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} 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/package.json b/package.json index c8d99da5..fe6a5a47 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "yarn": ">=1.7.0" }, "scripts": { - "test": "xo && FORCE_HYPERLINK=1 ava" + "test": "xo && FORCE_HYPERLINK=1 ava && ava test/integration.js --no-worker-threads" }, "files": [ "source" @@ -72,21 +72,19 @@ "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": { "files": [ - "!integration-test" + "!test/integration.js" ], "nodeArguments": [ "--loader=esmock" ] }, "xo": { - "ignores": [ - "integration-test" - ], "rules": { "comma-dangle": "off", "object-shorthand": [ diff --git a/test/integration.js b/test/integration.js index 0b0ac569..54016649 100644 --- a/test/integration.js +++ b/test/integration.js @@ -1,11 +1,104 @@ +/* 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 {execa} from 'execa'; +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.failing('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: []} + ); }); From aab49504ba43a699dff967d6d9ee6308d0651e82 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sun, 2 Apr 2023 17:40:04 -0500 Subject: [PATCH 21/22] chore(`AVA`): set environment variable via config --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index fe6a5a47..5b1473d5 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "yarn": ">=1.7.0" }, "scripts": { - "test": "xo && FORCE_HYPERLINK=1 ava && ava test/integration.js --no-worker-threads" + "test": "xo && ava && ava test/integration.js --no-worker-threads" }, "files": [ "source" @@ -77,6 +77,9 @@ "xo": "^0.53.1" }, "ava": { + "environmentVariables": { + "FORCE_HYPERLINK": "1" + }, "files": [ "!test/integration.js" ], From 0e647ede29afcd7529328f157442d15f8164039e Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sun, 2 Apr 2023 21:37:00 -0500 Subject: [PATCH 22/22] chore(`xo`): lint with new defaults --- package.json | 9 ---- source/cli-implementation.js | 42 ++++++++--------- source/config.js | 4 +- source/git-tasks.js | 8 ++-- source/git-util.js | 6 +-- source/index.js | 68 ++++++++++++++-------------- source/npm/enable-2fa.js | 2 +- source/npm/handle-npm-error.js | 4 +- source/npm/publish.js | 2 +- source/npm/util.js | 16 +++---- source/prerequisite-tasks.js | 28 ++++++------ source/release-task-helper.js | 2 +- source/ui.js | 54 +++++++++++----------- source/util.js | 2 +- source/version.js | 2 +- test/_utils.js | 6 +-- test/config.js | 26 +++++------ test/git-tasks.js | 48 ++++++++++---------- test/index.js | 28 ++++++------ test/integration.js | 8 ++-- test/npmignore.js | 36 +++++++-------- test/prefix.js | 2 +- test/prerequisite-tasks.js | 82 +++++++++++++++++----------------- test/version.js | 6 +-- 24 files changed, 241 insertions(+), 250 deletions(-) diff --git a/package.json b/package.json index 5b1473d5..219d79d7 100644 --- a/package.json +++ b/package.json @@ -86,14 +86,5 @@ "nodeArguments": [ "--loader=esmock" ] - }, - "xo": { - "rules": { - "comma-dangle": "off", - "object-shorthand": [ - "error", - "properties" - ] - } } } diff --git a/source/cli-implementation.js b/source/cli-implementation.js index bd9c0577..3b394f92 100755 --- a/source/cli-implementation.js +++ b/source/cli-implementation.js @@ -49,51 +49,51 @@ const cli = meow(` 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(); @@ -107,7 +107,7 @@ try { publish: true, releaseDraft: true, yarn: hasYarn(), - '2fa': true + '2fa': true, }; const localConfig = await config(); @@ -115,7 +115,7 @@ try { const flags = { ...defaultFlags, ...localConfig, - ...cli.flags + ...cli.flags, }; // Workaround for unintended auto-casing behavior from `meow`. @@ -127,7 +127,7 @@ try { const availability = flags.publish ? await isPackageNameAvailable(pkg) : { isAvailable: false, - isUnknown: false + isUnknown: false, }; // Use current (latest) version when 'releaseDraftOnly', otherwise use the first argument. @@ -139,7 +139,7 @@ try { availability, version, runPublish, - branch + branch, }, {pkg, pkgPath}); if (!options.confirm) { diff --git a/source/config.js b/source/config.js index a7b7cd03..cd78f7b8 100644 --- a/source/config.js +++ b/source/config.js @@ -21,8 +21,8 @@ const getConfig = async () => { stopDir: searchDir, loaders: { '.js': loadESM, - '.mjs': loadESM - } + '.mjs': loadESM, + }, }); const {config} = (await explorer.search(searchDir)) || {}; diff --git a/source/git-tasks.js b/source/git-tasks.js index d6d2c244..8106685a 100644 --- a/source/git-tasks.js +++ b/source/git-tasks.js @@ -5,16 +5,16 @@ 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) { diff --git a/source/git-util.js b/source/git-util.js index 95bb9e65..917655ea 100644 --- a/source/git-util.js +++ b/source/git-util.js @@ -28,7 +28,7 @@ export const newFilesSinceLastRelease = async () => { // Get all files under version control return ignoreWalker({ path: packageDirectorySync(), - ignoreFiles: ['.gitignore'] + ignoreFiles: ['.gitignore'], }); } }; @@ -191,7 +191,7 @@ async function hasLocalBranch(branch) { 'show-ref', '--verify', '--quiet', - `refs/heads/${branch}` + `refs/heads/${branch}`, ]); return true; } catch { @@ -208,7 +208,7 @@ export const 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.', ); }; diff --git a/source/index.js b/source/index.js index 1756f8e5..a3663b01 100644 --- a/source/index.js +++ b/source/index.js @@ -94,15 +94,15 @@ const np = async (input = 'patch', options) => { { 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, - renderer: options.renderer ?? 'default' + renderer: options.renderer ?? 'default', }); if (runCleanup) { @@ -110,7 +110,7 @@ const np = async (input = 'patch', options) => { { title: 'Cleanup', enabled: () => !hasLockFile, - task: () => deleteAsync('node_modules') + task: () => deleteAsync('node_modules'), }, { title: 'Installing dependencies using Yarn', @@ -127,18 +127,18 @@ const np = 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']); - } - } + }, + }, ]); } @@ -147,7 +147,7 @@ const np = 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', @@ -159,9 +159,9 @@ const np = async (input = 'patch', options) => { } return throwError(() => error); - }) - ) - } + }), + ), + }, ]); } @@ -169,7 +169,7 @@ const np = 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}`; @@ -180,7 +180,7 @@ const np = async (input = 'patch', options) => { return `${previewText}.`; } }, - task: () => { + task() { const args = ['version', '--new-version', input]; if (options.message) { @@ -188,12 +188,12 @@ const np = 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}`; @@ -204,7 +204,7 @@ const np = async (input = 'patch', options) => { return `${previewText}.`; } }, - task: () => { + task() { const args = ['version', input]; if (options.message) { @@ -212,21 +212,21 @@ const np = 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) @@ -238,10 +238,10 @@ const np = async (input = 'patch', options) => { }), finalize(() => { publishStatus = hasError ? 'FAILED' : 'SUCCESS'; - }) + }), ); - } - } + }, + }, ]); const isExternalRegistry = npm.isExternalRegistry(pkg); @@ -249,14 +249,14 @@ const np = 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 { @@ -265,7 +265,7 @@ const np = async (input = 'patch', options) => { tasks.add({ title: 'Pushing tags', - skip: async () => { + async skip() { if (!(await git.hasUpstream())) { return 'Upstream branch not found; not pushing.'; } @@ -278,21 +278,21 @@ const np = 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), }); } diff --git a/source/npm/enable-2fa.js b/source/npm/enable-2fa.js index 9256686f..03c29ff5 100644 --- a/source/npm/enable-2fa.js +++ b/source/npm/enable-2fa.js @@ -16,7 +16,7 @@ const enable2fa = (packageName, options) => execa('npm', getEnable2faArgs(packag 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}))), ); }; diff --git a/source/npm/handle-npm-error.js b/source/npm/handle-npm-error.js index 480d68a8..7ec39c88 100644 --- a/source/npm/handle-npm-error.js +++ b/source/npm/handle-npm-error.js @@ -18,9 +18,9 @@ const handleNpmError = (error, task, message, executor) => { 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)), ); } diff --git a/source/npm/publish.js b/source/npm/publish.js index a0d97ada..85a73ddb 100644 --- a/source/npm/publish.js +++ b/source/npm/publish.js @@ -32,7 +32,7 @@ const publish = (context, pkgManager, task, options) => { context.otp = otp; return pkgPublish(pkgManager, {...options, otp}); - })) + })), ); }; diff --git a/source/npm/util.js b/source/npm/util.js index 69464bb3..0d6582a5 100644 --- a/source/npm/util.js +++ b/source/npm/util.js @@ -31,7 +31,7 @@ const filesIgnoredByDefault = [ 'npm-debug.log', 'package-lock.json', '.git/**/*', - '.git' + '.git', ]; export const checkConnection = () => pTimeout( @@ -44,8 +44,8 @@ export const checkConnection = () => pTimeout( } })(), { milliseconds: 15_000, - message: 'Connection to npm registry timed out' - } + message: 'Connection to npm registry timed out', + }, ); export const username = async ({externalRegistry}) => { @@ -124,12 +124,12 @@ export const isPackageNameAvailable = async pkg => { const args = [pkg.name]; const availability = { isAvailable: false, - isUnknown: false + isUnknown: false, }; if (isExternalRegistry(pkg)) { args.push({ - registryUrl: pkg.publishConfig.registry + registryUrl: pkg.publishConfig.registry, }); } @@ -174,7 +174,7 @@ function excludeGitAndNodeModulesPaths(singlePath) { async function getFilesIgnoredByDotnpmignore(pkg, fileList) { let allowList = await ignoreWalker({ path: packageDirectorySync(), - ignoreFiles: ['.npmignore'] + ignoreFiles: ['.npmignore'], }); allowList = allowList.filter(singlePath => excludeGitAndNodeModulesPaths(singlePath)); return fileList.filter(minimatch.filter(getIgnoredFilesGlob(allowList, pkg.directories), {matchBase: true, dot: true})); @@ -192,7 +192,7 @@ function filterFileList(globArray, fileList) { async function getFilesIncludedByDotnpmignore(pkg, fileList) { const allowList = await ignoreWalker({ path: packageDirectorySync(), - ignoreFiles: ['.npmignore'] + ignoreFiles: ['.npmignore'], }); return filterFileList(allowList, fileList); } @@ -237,7 +237,7 @@ function getDefaultIncludedFilesGlob(mainFile) { 'HISTORY*', 'LICENSE*', 'LICENCE*', - 'NOTICE*' + 'NOTICE*', ]; if (mainFile) { filesAlwaysIncluded.push(mainFile); diff --git a/source/prerequisite-tasks.js b/source/prerequisite-tasks.js index 9e74bd11..50006c8d 100644 --- a/source/prerequisite-tasks.js +++ b/source/prerequisite-tasks.js @@ -14,26 +14,26 @@ const prerequisiteTasks = (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); - } + }, }, { 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,21 +46,21 @@ const prerequisiteTasks = (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: () => { + task() { newVersion = Version.getAndValidateNewVersionFrom(input, pkg.version); - } + }, }, { title: 'Check for pre-release version', @@ -68,7 +68,7 @@ const prerequisiteTasks = (input, pkg, options) => { 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', @@ -78,8 +78,8 @@ const prerequisiteTasks = (input, pkg, options) => { const tagPrefix = await getTagVersionPrefix(options); await git.verifyTagDoesNotExistOnRemote(`${tagPrefix}${newVersion}`); - } - } + }, + }, ]; return new Listr(tasks); diff --git a/source/release-task-helper.js b/source/release-task-helper.js index 22a2af95..2e71aca9 100644 --- a/source/release-task-helper.js +++ b/source/release-task-helper.js @@ -15,7 +15,7 @@ const releaseTaskHelper = async (options, pkg) => { repoUrl: options.repoUrl, tag, body: options.releaseNotes(tag), - isPrerelease: isPreRelease + isPrerelease: isPreRelease, }); await open(url); diff --git a/source/ui.js b/source/ui.js index bf77b8b9..3cbfb213 100644 --- a/source/ui.js +++ b/source/ui.js @@ -22,7 +22,7 @@ const printCommitLog = async (repoUrl, registryUrl, fromLatestTag, releaseBranch return { hasCommits: false, hasUnreleasedCommits: false, - releaseNotes: () => {} + releaseNotes() {}, }; } @@ -34,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), }; }); @@ -65,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); @@ -74,7 +74,7 @@ const printCommitLog = async (repoUrl, registryUrl, fromLatestTag, releaseBranch return { hasCommits: true, hasUnreleasedCommits, - releaseNotes + releaseNotes, }; }; @@ -114,7 +114,7 @@ const checkNewFilesAndDependencies = async (pkg, pkgPath) => { type: 'confirm', name: 'confirm', message: `${messages.join('\n')}\nContinue?`, - default: false + default: false, }]); return answers.confirm; @@ -136,7 +136,7 @@ const ui = async (options, {pkg, pkgPath}) => { if (!answerIgnoredFiles) { return { ...options, - confirm: answerIgnoredFiles + confirm: answerIgnoredFiles, }; } } @@ -158,14 +158,14 @@ const ui = async (options, {pkg, pkgPath}) => { confirm: { type: 'confirm', message: 'Unreleased commits found. They won\'t be included in the release draft. Continue?', - default: false - } + default: false, + }, }); if (!answers.confirm) { return { ...options, - ...answers + ...answers, }; } } @@ -175,7 +175,7 @@ const ui = async (options, {pkg, pkgPath}) => { ...options, confirm: true, repoUrl, - releaseNotes + releaseNotes, }; } @@ -184,14 +184,14 @@ const ui = async (options, {pkg, pkgPath}) => { confirm: { type: 'confirm', message: 'No commits found since previous release, continue?', - default: false - } + default: false, + }, }); if (!answers.confirm) { return { ...options, - ...answers + ...answers, }; } } @@ -202,14 +202,14 @@ const ui = async (options, {pkg, pkgPath}) => { 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 - } + default: false, + }, }); if (!answers.confirm) { return { ...options, - ...answers + ...answers, }; } } @@ -222,14 +222,14 @@ const ui = async (options, {pkg, pkgPath}) => { choices: [...Version.SEMVER_INCREMENTS .map(inc => ({ name: `${inc} ${prettyVersionDiff(oldVersion, inc)}`, - value: inc + value: inc, })), new inquirer.Separator(), { name: 'Other (specify)', - value: null + value: null, }], - filter: input => Version.isValidInput(input) ? new Version(oldVersion).getNewVersionFrom(input) : input + filter: input => Version.isValidInput(input) ? new Version(oldVersion).getNewVersionFrom(input) : input, }, customVersion: { type: 'input', @@ -246,7 +246,7 @@ const ui = async (options, {pkg, pkgPath}) => { } return true; - } + }, }, tag: { type: 'list', @@ -260,10 +260,10 @@ const ui = async (options, {pkg, pkgPath}) => { new inquirer.Separator(), { name: 'Other (specify)', - value: null - } + value: null, + }, ]; - } + }, }, customTag: { type: 'input', @@ -279,14 +279,14 @@ const ui = async (options, {pkg, pkgPath}) => { } return true; - } + }, }, publishScoped: { type: 'confirm', 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 - } + default: false, + }, }); return { @@ -296,7 +296,7 @@ const ui = async (options, {pkg, pkgPath}) => { publishScoped: answers.publishScoped, confirm: true, repoUrl, - releaseNotes + releaseNotes, }; }; diff --git a/source/util.js b/source/util.js index fd7cea3a..b76a4080 100644 --- a/source/util.js +++ b/source/util.js @@ -16,7 +16,7 @@ export const readPkg = async packagePath => { } const {packageJson, path} = await readPackageUp({ - cwd: packagePath + cwd: packagePath, }); return {pkg: packageJson, pkgPath: path}; diff --git a/source/version.js b/source/version.js index 5dd05ed5..56710fac 100644 --- a/source/version.js +++ b/source/version.js @@ -15,7 +15,7 @@ export default class Version { satisfies(range) { Version.validate(this.version); return semver.satisfies(this.version, range, { - includePrerelease: true + includePrerelease: true, }); } diff --git a/test/_utils.js b/test/_utils.js index 1a7b5def..1f9a47f3 100644 --- a/test/_utils.js +++ b/test/_utils.js @@ -4,7 +4,7 @@ import {SilentRenderer} from './fixtures/listr-renderer.js'; export const _stubExeca = source => async (t, commands) => esmock(source, {}, { execa: { - execa: async (...args) => { + async execa(...args) { const results = await Promise.all(commands.map(async result => { const argsMatch = await t.try(tt => { const [command, ...commandArgs] = result.command.split(' '); @@ -26,8 +26,8 @@ export const _stubExeca = source => async (t, commands) => esmock(source, {}, { const result = results.filter(Boolean).at(0); return result ?? execa(...args); - } - } + }, + }, }); export const run = async listr => { diff --git a/test/config.js b/test/config.js index f9e4a703..0800c14d 100644 --- a/test/config.js +++ b/test/config.js @@ -15,7 +15,7 @@ const getConfigsWhenGlobalBinaryIsUsed = async homedirStub => { const getConfig = await esmock(testedModulePath, { 'is-installed-globally': true, 'pkg-dir': {packageDirectory: async () => pathPkgDir}, - 'node:os': {homedir: homedirStub} + 'node:os': {homedir: homedirStub}, }); return getConfig(); }); @@ -30,7 +30,7 @@ const getConfigsWhenLocalBinaryIsUsed = async pathPkgDir => { const getConfig = await esmock(testedModulePath, { 'is-installed-globally': false, 'pkg-dir': {packageDirectory: async () => pathPkgDir}, - 'node:os': {homedir: () => homedir} + 'node:os': {homedir: () => homedir}, }); return getConfig(); }); @@ -56,45 +56,45 @@ const useLocalBinary = test.macro(async (t, pkgDir, source) => { }); 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' + useGlobalBinary, 'homedir1', 'homedir/.np-config.json', ); 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' + useGlobalBinary, 'homedir2', 'homedir/.np-config.js', ); 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' + 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' + 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' + 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' + 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' + 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' + 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' + 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' + 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' + useLocalBinary, 'local5', 'packagedir/.np-config.mjs', ); diff --git a/test/git-tasks.js b/test/git-tasks.js index 6576a55c..d390d0c5 100644 --- a/test/git-tasks.js +++ b/test/git-tasks.js @@ -4,7 +4,7 @@ import { _stubExeca, run, assertTaskFailed, - assertTaskDoesntExist + assertTaskDoesntExist, } from './_utils.js'; /** @type {(...args: ReturnType<_stubExeca>) => Promise} */ @@ -18,12 +18,12 @@ test.serial('should fail when release branch is not specified, current branch is const gitTasks = await stubExeca(t, [{ command: 'git symbolic-ref --short HEAD', exitCode: 0, - stdout: 'feature' + 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.'} + {message: 'Not on `master` branch. Use --any-branch to publish anyway, or set a different release branch using --branch.'}, ); assertTaskFailed(t, 'Check current branch'); @@ -33,12 +33,12 @@ test.serial('should fail when current branch is not the specified release branch const gitTasks = await stubExeca(t, [{ command: 'git symbolic-ref --short HEAD', exitCode: 0, - stdout: 'feature' + 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.'} + {message: 'Not on `release` branch. Use --any-branch to publish anyway, or set a different release branch using --branch.'}, ); assertTaskFailed(t, 'Check current branch'); @@ -49,22 +49,22 @@ test.serial('should not fail when current branch not master and publishing from { 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 t.notThrowsAsync( - run(gitTasks({anyBranch: true})) + run(gitTasks({anyBranch: true})), ); assertTaskDoesntExist(t, 'Check current branch'); @@ -75,18 +75,18 @@ test.serial('should fail when local working tree modified', async 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(gitTasks({branch: 'master'})), - {message: 'Unclean working tree. Commit or stash changes first.'} + {message: 'Unclean working tree. Commit or stash changes first.'}, ); assertTaskFailed(t, 'Check local working tree'); @@ -97,23 +97,23 @@ test.serial('should fail when remote history differs', async 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(gitTasks({branch: 'master'})), - {message: 'Remote history differs. Please pull changes.'} + {message: 'Remote history differs. Please pull changes.'}, ); assertTaskFailed(t, 'Check remote history'); @@ -124,21 +124,21 @@ test.serial('checks should pass when publishing from master, working tree is cle { 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(gitTasks({branch: 'master'})) + run(gitTasks({branch: 'master'})), ); }); diff --git a/test/index.js b/test/index.js index 27027c75..dbf61745 100644 --- a/test/index.js +++ b/test/index.js @@ -10,31 +10,31 @@ const defaultOptions = { runPublish: true, availability: { isAvailable: false, - isUnknown: false + isUnknown: false, }, - renderer: 'silent' + renderer: 'silent', }; const npFails = test.macro(async (t, inputs, message) => { await t.throwsAsync( Promise.all(inputs.map(input => np(input, defaultOptions))), - {message} + {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.' + 'Version should be either patch, minor, major, prepatch, preminor, premajor, prerelease, or a valid semver version.', ); 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' + '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+`/ + /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 => { @@ -47,18 +47,18 @@ test('skip enabling 2FA if the package exists', async t => { '../source/git-tasks.js': sinon.stub(), '../source/git-util.js': { hasUpstream: sinon.stub().returns(true), - pushGraceful: sinon.stub() + pushGraceful: sinon.stub(), }, '../source/npm/enable-2fa.js': enable2faStub, - '../source/npm/publish.js': sinon.stub().returns({pipe: sinon.stub()}) + '../source/npm/publish.js': sinon.stub().returns({pipe: sinon.stub()}), }, {}); await t.notThrowsAsync(npMock('1.0.0', { ...defaultOptions, availability: { isAvailable: false, - isUnknown: false - } + isUnknown: false, + }, })); t.true(enable2faStub.notCalled); @@ -74,19 +74,19 @@ test('skip enabling 2FA if the `2fa` option is false', async t => { '../source/git-tasks.js': sinon.stub(), '../source/git-util.js': { hasUpstream: sinon.stub().returns(true), - pushGraceful: sinon.stub() + pushGraceful: sinon.stub(), }, '../source/npm/enable-2fa.js': enable2faStub, - '../source/npm/publish.js': sinon.stub().returns({pipe: sinon.stub()}) + '../source/npm/publish.js': sinon.stub().returns({pipe: sinon.stub()}), }); 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 54016649..83d43d6e 100644 --- a/test/integration.js +++ b/test/integration.js @@ -48,7 +48,7 @@ test.serial('files to package with tags added', async t => { t.deepEqual( await util.getNewFiles({files: ['*.js']}), - {unpublished: ['new'], firstTime: ['index.js']} + {unpublished: ['new'], firstTime: ['index.js']}, ); }); @@ -62,7 +62,7 @@ test.serial.failing('file `new` to package without tags added', async t => { t.deepEqual( await util.getNewFiles({files: ['index.js']}), - {unpublished: ['new'], firstTime: ['index.js']} + {unpublished: ['new'], firstTime: ['index.js']}, ); }); @@ -86,7 +86,7 @@ test.serial('files with long pathnames added', async t => { t.deepEqual( await util.getNewFiles({files: ['*.js']}), - {unpublished: [filePath1, filePath2], firstTime: []} + {unpublished: [filePath1, filePath2], firstTime: []}, ); }); @@ -99,6 +99,6 @@ test.serial('no new files added', async t => { t.deepEqual( await util.getNewFiles({files: ['*.js']}), - {unpublished: [], firstTime: []} + {unpublished: [], firstTime: []}, ); }); diff --git a/test/npmignore.js b/test/npmignore.js index 70dbd905..4f02492a 100644 --- a/test/npmignore.js +++ b/test/npmignore.js @@ -8,12 +8,12 @@ const newFiles = [ '.hg', 'test/file.txt', 'readme.md', - 'README.txt' + 'README.txt', ]; const mockPkgDir = test.macro(async (t, paths, impl) => { const testedModule = await esmock('../source/npm/util.js', { - 'pkg-dir': {packageDirectorySync: () => path.resolve(...paths)} + 'pkg-dir': {packageDirectorySync: () => path.resolve(...paths)}, }); await impl(t, testedModule); @@ -22,97 +22,97 @@ const mockPkgDir = test.macro(async (t, paths, impl) => { 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 e6c24b24..7eb5ef8c 100644 --- a/test/prefix.js +++ b/test/prefix.js @@ -18,7 +18,7 @@ test('no options passed', async t => { test.serial('defaults to "v" when command fails', async t => { const testedModule = await esmock('../source/util.js', { - execa: {default: Promise.reject} + execa: {default: Promise.reject}, }); t.is(await testedModule.getTagVersionPrefix({yarn: true}), 'v'); diff --git a/test/prerequisite-tasks.js b/test/prerequisite-tasks.js index d0a72dca..e693e6f8 100644 --- a/test/prerequisite-tasks.js +++ b/test/prerequisite-tasks.js @@ -8,7 +8,7 @@ import { _stubExeca, run, assertTaskFailed, - assertTaskDisabled + assertTaskDisabled, } from './_utils.js'; /** @type {(...args: ReturnType<_stubExeca>) => Promise} */ @@ -25,12 +25,12 @@ test.serial('public-package published on npm registry: should fail when npm regi exitCode: 1, exitCodeName: 'EPERM', stdout: '', - stderr: 'failed' + stderr: 'failed', }]); await t.throwsAsync( run(prerequisiteTasks('1.0.0', {name: 'test'}, {})), - {message: 'Connection to npm registry failed'} + {message: 'Connection to npm registry failed'}, ); assertTaskFailed(t, 'Ping npm registry'); @@ -40,11 +40,11 @@ test.serial('private package: should disable task pinging npm registry', async t const prerequisiteTasks = await stubExeca(t, [{ command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', exitCode: 0, - stdout: '' + stdout: '', }]); await t.notThrowsAsync( - run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0', private: true}, {yarn: false})) + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0', private: true}, {yarn: false})), ); assertTaskDisabled(t, 'Ping npm registry'); @@ -54,11 +54,11 @@ test.serial('external registry: should disable task pinging npm registry', async const prerequisiteTasks = await stubExeca(t, [{ command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', exitCode: 0, - stdout: '' + stdout: '', }]); await t.notThrowsAsync( - run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0', publishConfig: {registry: 'http://my.io'}}, {yarn: false})) + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0', publishConfig: {registry: 'http://my.io'}}, {yarn: false})), ); assertTaskDisabled(t, 'Ping npm registry'); @@ -69,20 +69,20 @@ test.serial('should fail when npm version does not match range in `package.json` { 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 = 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}`} + {message: `Please upgrade to npm${depRange}`}, ); assertTaskFailed(t, 'Check npm version'); @@ -93,20 +93,20 @@ test.serial('should fail when yarn version does not match range in `package.json { 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 = 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}`} + {message: `Please upgrade to yarn${depRange}`}, ); assertTaskFailed(t, 'Check yarn version'); @@ -117,20 +117,20 @@ test.serial('should fail when user is not authenticated at npm registry', async { 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(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), - {message: 'You do not have write permissions required to publish this package.'} + {message: 'You do not have write permissions required to publish this package.'}, ); process.env.NODE_ENV = 'test'; @@ -143,25 +143,25 @@ test.serial('should fail when user is not authenticated at external registry', a { 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"}' - } + stdout: '{"sindresorhus": "read"}', + }, ]); process.env.NODE_ENV = 'P'; 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.'} + {message: 'You do not have write permissions required to publish this package.'}, ); process.env.NODE_ENV = 'test'; @@ -173,13 +173,13 @@ test.serial('private package: should disable task `verify user is authenticated` const prerequisiteTasks = await stubExeca(t, [{ command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', exitCode: 0, - stdout: '' + stdout: '', }]); process.env.NODE_ENV = 'P'; await t.notThrowsAsync( - run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0', private: true}, {yarn: false})) + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0', private: true}, {yarn: false})), ); process.env.NODE_ENV = 'test'; @@ -191,14 +191,14 @@ test.serial('should fail when git version does not match range in `package.json` const prerequisiteTasks = await stubExeca(t, [{ command: 'git version', exitCode: 0, - stdout: 'git version 1.0.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}`} + {message: `Please upgrade to git${depRange}`}, ); assertTaskFailed(t, 'Check git version'); @@ -209,12 +209,12 @@ test.serial('should fail when git remote does not exist', async t => { command: 'git ls-remote origin HEAD', exitCode: 1, exitCodeName: 'EPERM', - stderr: 'not found' + stderr: 'not found', }]); await t.throwsAsync( run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), - {message: 'not found'} + {message: 'not found'}, ); assertTaskFailed(t, 'Check git remote'); @@ -223,7 +223,7 @@ test.serial('should fail when git remote does not exist', async t => { test.serial('should fail when version is invalid', async t => { 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.`} + {message: `Version should be either ${Version.SEMVER_INCREMENTS.join(', ')}, or a valid semver version.`}, ); assertTaskFailed(t, 'Validate version'); @@ -232,7 +232,7 @@ test.serial('should fail when version is invalid', async t => { test.serial('should fail when version is lower as latest version', async t => { 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`'} + {message: 'New version `0.1.0` should be higher than current version `1.0.0`'}, ); assertTaskFailed(t, 'Validate version'); @@ -241,7 +241,7 @@ test.serial('should fail when version is lower as latest version', async t => { test.serial('should fail when prerelease version of public package without dist tag given', async t => { 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'} + {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'); @@ -250,34 +250,34 @@ test.serial('should fail when prerelease version of public package without dist test.serial('should not fail when prerelease version of public package with dist tag given', async t => { const prerequisiteTasks = await stubExeca(t, [{ command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', - stdout: '' + stdout: '', }]); await t.notThrowsAsync( - run(prerequisiteTasks('2.0.0-1', {name: 'test', version: '1.0.0'}, {yarn: false, tag: 'pre'})) + 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 => { const prerequisiteTasks = await stubExeca(t, [{ command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', - stdout: '' + stdout: '', }]); await t.notThrowsAsync( - run(prerequisiteTasks('2.0.0-1', {name: 'test', version: '1.0.0', private: true}, {yarn: false})) + 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 => { const prerequisiteTasks = await stubExeca(t, [{ command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', - stdout: 'vvb' + 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.'} + {message: 'Git tag `v2.0.0` already exists.'}, ); assertTaskFailed(t, 'Check git tag existence'); @@ -286,10 +286,10 @@ test.serial('should fail when git tag already exists', async t => { test.serial('checks should pass', async t => { const prerequisiteTasks = await stubExeca(t, [{ command: 'git rev-parse --quiet --verify refs/tags/v2.0.0', - stdout: '' + stdout: '', }]); await t.notThrowsAsync( - run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})) + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), ); }); diff --git a/test/version.js b/test/version.js index 6a545799..203fb4e2 100644 --- a/test/version.js +++ b/test/version.js @@ -124,16 +124,16 @@ test('version.getAndValidateNewVersionFrom', t => { t.throws( () => Version.getAndValidateNewVersionFrom('patch', '1'), - {message: 'Version should be a valid semver version.'} + {message: 'Version should be a valid semver version.'}, ); t.throws( () => Version.getAndValidateNewVersionFrom('lol', '1.0.0'), - {message: `Version should be either ${Version.SEMVER_INCREMENTS.join(', ')}, or a valid semver version.`} + {message: `Version should be either ${Version.SEMVER_INCREMENTS.join(', ')}, or a valid semver version.`}, ); t.throws( () => Version.getAndValidateNewVersionFrom('1.0.0', '2.0.0'), - {message: 'New version `1.0.0` should be higher than current version `2.0.0`'} + {message: 'New version `1.0.0` should be higher than current version `2.0.0`'}, ); });