From 4b3b599571c9475b2be035b370fef4fe0b2e85d0 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Mon, 26 Feb 2024 07:55:38 -0500 Subject: [PATCH] Add pnpm support (#730) Co-authored-by: Misha Kaletsky Co-authored-by: Sindre Sorhus --- package.json | 8 +- readme.md | 22 ++- source/.npmignore | 1 + source/cli-implementation.js | 31 ++-- source/git-util.js | 15 -- source/index.js | 185 ++++++++------------- source/npm/handle-npm-error.js | 2 +- source/npm/publish.js | 21 +-- source/npm/util.js | 15 -- source/package-manager/configs.js | 51 ++++++ source/package-manager/index.js | 60 +++++++ source/package-manager/types.d.ts | 58 +++++++ source/prerequisite-tasks.js | 15 +- source/release-task-helper.js | 6 +- source/ui.js | 31 ++-- source/util.js | 17 +- source/yarn.js | 16 -- test/cli.js | 2 +- test/git-util/check-if-file-git-ignored.js | 20 --- test/index.js | 11 +- test/npm/util/get-registry-url.js | 49 ------ test/tasks/prerequisite-tasks.js | 35 ++-- test/ui/new-files-dependencies.js | 1 - test/ui/prompts/tags.js | 4 +- test/ui/prompts/version.js | 1 - test/util/get-pre-release-prefix.js | 14 +- test/util/get-tag-version-prefix.js | 11 +- test/util/yarn.js | 15 -- 28 files changed, 348 insertions(+), 369 deletions(-) create mode 100644 source/.npmignore create mode 100644 source/package-manager/configs.js create mode 100644 source/package-manager/index.js create mode 100644 source/package-manager/types.d.ts delete mode 100644 source/yarn.js delete mode 100644 test/git-util/check-if-file-git-ignored.js delete mode 100644 test/npm/util/get-registry-url.js delete mode 100644 test/util/yarn.js diff --git a/package.json b/package.json index bab6ae23..4d266119 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "node": ">=18", "npm": ">=9", "git": ">=2.11.0", - "yarn": ">=1.7.0" + "yarn": ">=1.7.0", + "pnpm": ">=8" }, "scripts": { "test": "xo && ava" @@ -40,7 +41,6 @@ "execa": "^8.0.1", "exit-hook": "^4.0.0", "github-url-from-git": "^1.5.0", - "has-yarn": "^3.0.0", "hosted-git-info": "^7.0.1", "ignore-walk": "^6.0.3", "import-local": "^3.1.0", @@ -52,7 +52,7 @@ "listr": "^0.14.3", "listr-input": "^0.2.1", "log-symbols": "^6.0.0", - "meow": "^12.1.1", + "meow": "^13.1.0", "new-github-release-url": "^2.0.0", "npm-name": "^7.1.1", "onetime": "^7.0.0", @@ -62,8 +62,8 @@ "p-timeout": "^6.1.2", "path-exists": "^5.0.0", "pkg-dir": "^8.0.0", - "read-pkg": "^9.0.1", "read-package-up": "^11.0.0", + "read-pkg": "^9.0.1", "rxjs": "^7.8.1", "semver": "^7.5.4", "symbol-observable": "^4.0.0", diff --git a/readme.md b/readme.md index 1f5930c8..84699c54 100644 --- a/readme.md +++ b/readme.md @@ -74,7 +74,7 @@ $ np --help $ np Version can be: - major | minor | patch | premajor | preminor | prepatch | prerelease | 1.2.3 + patch | minor | major | prepatch | preminor | premajor | prerelease | 1.2.3 Options --any-branch Allow publishing from any branch @@ -85,13 +85,13 @@ $ np --help --no-publish Skips publishing --preview Show tasks without actually executing them --tag Publish under a given dist-tag - --no-yarn Don't use Yarn --contents Subdirectory to publish --no-release-draft Skips opening a GitHub release draft --release-draft-only Only opens a GitHub release draft for the latest published version --test-script Name of npm run script to run tests before publishing (default: test) --no-2fa Don't enable 2FA on new packages (not recommended) - --message Version bump commit message. `%s` will be replaced with version. (default: '%s' with npm and 'v%s' with yarn) + --message Version bump commit message, '%s' will be replaced with version (default: '%s' with npm and 'v%s' with yarn) + --package-manager Use a specific package manager (default: 'packageManager' field in package.json) Examples $ np @@ -121,21 +121,21 @@ Currently, these are the flags you can configure: - `publish` - Publish (`true` by default). - `preview` - Show tasks without actually executing them (`false` by default). - `tag` - Publish under a given dist-tag (`latest` by default). -- `yarn` - Use yarn if possible (`true` by default). - `contents` - Subdirectory to publish (`.` by default). - `releaseDraft` - Open a GitHub release draft after releasing (`true` by default). - `testScript` - Name of npm run script to run tests before publishing (`test` by default). - `2fa` - Enable 2FA on new packages (`true` by default) (setting this to `false` is not recommended). - `message` - The commit message used for the version bump. Any `%s` in the string will be replaced with the new version. By default, npm uses `%s` and Yarn uses `v%s`. +- `packageManager` - Set the package manager to be used. Defaults to the [packageManager field in package.json](https://nodejs.org/api/packages.html#packagemanager), so only use if you can't update package.json for some reason. -For example, this configures `np` to never use Yarn and to use `dist` as the subdirectory to publish: +For example, this configures `np` to use `unit-test` as a test script, and to use `dist` as the subdirectory to publish: `package.json` ```json { "name": "superb-package", "np": { - "yarn": false, + "testScript": "unit-test", "contents": "dist" } } @@ -144,7 +144,7 @@ For example, this configures `np` to never use Yarn and to use `dist` as the sub `.np-config.json` ```json { - "yarn": false, + "testScript": "unit-test", "contents": "dist" } ``` @@ -152,7 +152,7 @@ For example, this configures `np` to never use Yarn and to use `dist` as the sub `.np-config.js` or `.np-config.cjs` ```js module.exports = { - yarn: false, + testScript: 'unit-test', contents: 'dist' }; ``` @@ -160,7 +160,7 @@ module.exports = { `.np-config.mjs` ```js export default { - yarn: false, + testScript: 'unit-test', contents: 'dist' }; ``` @@ -276,6 +276,10 @@ Set the [`registry` option](https://docs.npmjs.com/misc/config#registry) in pack } ``` +### Package managers + +If a package manager is not set in package.json, via configuration (`packageManager`), or via the CLI (`--package-manager`), `np` will attempt to infer the best package manager to use by looking for lockfiles. But it's recommended to set the [`packageManager` field](https://nodejs.org/api/packages.html#packagemanager) in your package.json to be consistent with other tools. See also the [corepack docs](https://nodejs.org/api/corepack.html). + ### Publish with a CI If you use a Continuous Integration server to publish your tagged commits, use the `--no-publish` flag to skip the publishing step of `np`. diff --git a/source/.npmignore b/source/.npmignore new file mode 100644 index 00000000..cd4efd8e --- /dev/null +++ b/source/.npmignore @@ -0,0 +1 @@ +*.d.ts diff --git a/source/cli-implementation.js b/source/cli-implementation.js index 9f7ffe45..c13fa608 100755 --- a/source/cli-implementation.js +++ b/source/cli-implementation.js @@ -4,7 +4,6 @@ import 'symbol-observable'; // Important: This needs to be first to prevent weir 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 util from './util.js'; @@ -12,9 +11,10 @@ import * as git from './git-util.js'; import * as npm from './npm/util.js'; import {SEMVER_INCREMENTS} from './version.js'; import ui from './ui.js'; -import {checkIfYarnBerry} from './yarn.js'; import np from './index.js'; +/** @typedef {typeof cli} CLI */ + const cli = meow(` Usage $ np @@ -31,13 +31,13 @@ const cli = meow(` --no-publish Skips publishing --preview Show tasks without actually executing them --tag Publish under a given dist-tag - --no-yarn Don't use Yarn --contents Subdirectory to publish --no-release-draft Skips opening a GitHub release draft --release-draft-only Only opens a GitHub release draft for the latest published version --test-script Name of npm run script to run tests before publishing (default: test) --no-2fa Don't enable 2FA on new packages (not recommended) --message Version bump commit message, '%s' will be replaced with version (default: '%s' with npm and 'v%s' with yarn) + --package-manager Use a specific package manager (default: 'packageManager' field in package.json) Examples $ np @@ -80,9 +80,8 @@ const cli = meow(` tag: { type: 'string', }, - yarn: { - type: 'boolean', - default: hasYarn(), + packageManager: { + type: 'string', }, contents: { type: 'string', @@ -105,7 +104,9 @@ const cli = meow(` updateNotifier({pkg: cli.pkg}).notify(); -try { +/** @typedef {Awaited>['options']} Options */ + +export async function getOptions() { const {pkg, rootDir} = await util.readPkg(cli.flags.contents); const localConfig = await config(rootDir); @@ -119,6 +120,10 @@ try { flags['2fa'] = flags['2Fa']; } + if (flags.packageManager) { + pkg.packageManager = flags.packageManager; + } + const runPublish = !flags.releaseDraftOnly && flags.publish && !pkg.private; // TODO: does this need to run if `runPublish` is false? @@ -132,22 +137,26 @@ try { const branch = flags.branch ?? await git.defaultBranch(); - const isYarnBerry = flags.yarn && checkIfYarnBerry(pkg); - const options = await ui({ ...flags, runPublish, availability, version, branch, - }, {pkg, rootDir, isYarnBerry}); + }, {pkg, rootDir}); + + return {options, rootDir, pkg}; +} + +try { + const {options, rootDir, pkg} = await getOptions(); if (!options.confirm) { gracefulExit(); } console.log(); // Prints a newline for readability - const newPkg = await np(options.version, options, {pkg, rootDir, isYarnBerry}); + const newPkg = await np(options.version, options, {pkg, rootDir}); if (options.preview || options.releaseDraftOnly) { gracefulExit(); diff --git a/source/git-util.js b/source/git-util.js index cfe36356..037d4a27 100644 --- a/source/git-util.js +++ b/source/git-util.js @@ -270,18 +270,3 @@ export const verifyRecentGitVersion = async () => { const installedVersion = await gitVersion(); util.validateEngineVersionSatisfies('git', installedVersion); }; - -export const checkIfFileGitIgnored = async pathToFile => { - try { - const {stdout} = await execa('git', ['check-ignore', pathToFile]); - return Boolean(stdout); - } catch (error) { - // If file is not ignored, `git check-ignore` throws an empty error and exits. - // Check that and return false so as not to throw an unwanted error. - if (error.stdout === '' && error.stderr === '') { - return false; - } - - throw error; - } -}; diff --git a/source/index.js b/source/index.js index 338dbaf3..f5484dba 100644 --- a/source/index.js +++ b/source/index.js @@ -1,23 +1,23 @@ -import fs from 'node:fs'; -import path from 'node:path'; import {execa} from 'execa'; import {deleteAsync} from 'del'; import Listr from 'listr'; -import {merge, throwError, catchError, filter, finalize} from 'rxjs'; -import hasYarn from 'has-yarn'; +import {merge, catchError, filter, finalize, from} from 'rxjs'; import hostedGitInfo from 'hosted-git-info'; import onetime from 'onetime'; import {asyncExitHook} from 'exit-hook'; import logSymbols from 'log-symbols'; import prerequisiteTasks from './prerequisite-tasks.js'; import gitTasks from './git-tasks.js'; -import publish, {getPackagePublishArguments} from './npm/publish.js'; +import {getPackagePublishArguments} from './npm/publish.js'; import enable2fa, {getEnable2faArgs} from './npm/enable-2fa.js'; +import handleNpmError from './npm/handle-npm-error.js'; import releaseTaskHelper from './release-task-helper.js'; +import {findLockfile, getPackageManagerConfig, printCommand} from './package-manager/index.js'; import * as util from './util.js'; import * as git from './git-util.js'; import * as npm from './npm/util.js'; +/** @type {(cmd: string, args: string[], options?: import('execa').Options) => any} */ const exec = (cmd, args, options) => { // Use `Observable` support if merged https://github.com/sindresorhus/execa/pull/26 const cp = execa(cmd, args, options); @@ -25,40 +25,27 @@ const exec = (cmd, args, options) => { return merge(cp.stdout, cp.stderr, cp).pipe(filter(Boolean)); }; -// eslint-disable-next-line complexity -const np = async (input = 'patch', options, {pkg, rootDir, isYarnBerry}) => { - if (!hasYarn() && options.yarn) { - throw new Error('Could not use Yarn without yarn.lock file'); - } +/** +@param {string} input +@param {import('./cli-implementation.js').Options} options +@param {{pkg: import('read-pkg').NormalizedPackageJson; rootDir: string}} context +*/ +const np = async (input = 'patch', options, {pkg, rootDir}) => { + const pkgManager = getPackageManagerConfig(rootDir, pkg); // TODO: Remove sometime far in the future if (options.skipCleanup) { options.cleanup = false; } - function getPackageManagerName() { - if (options.yarn === true) { - if (isYarnBerry) { - return 'Yarn Berry'; - } - - return 'Yarn'; - } - - return 'npm'; - } - const runTests = options.tests && !options.yolo; const runCleanup = options.cleanup && !options.yolo; - const pkgManager = options.yarn === true ? 'yarn' : 'npm'; - const pkgManagerName = getPackageManagerName(); - const hasLockFile = fs.existsSync(path.resolve(rootDir, options.yarn ? 'yarn.lock' : 'package-lock.json')) || fs.existsSync(path.resolve(rootDir, 'npm-shrinkwrap.json')); + const lockfile = findLockfile(rootDir, pkgManager); const isOnGitHub = options.repoUrl && hostedGitInfo.fromUrl(options.repoUrl)?.type === 'github'; const testScript = options.testScript || 'test'; - const testCommand = options.testScript ? ['run', testScript] : [testScript]; if (options.releaseDraftOnly) { - await releaseTaskHelper(options, pkg); + await releaseTaskHelper(options, pkg, pkgManager); return pkg; } @@ -68,7 +55,7 @@ const np = async (input = 'patch', options, {pkg, rootDir, isYarnBerry}) => { const rollback = onetime(async () => { console.log('\nPublish failed. Rolling back to the previous stateā€¦'); - const tagVersionPrefix = await util.getTagVersionPrefix(options); + const tagVersionPrefix = await util.getTagVersionPrefix(pkgManager); const latestTag = await git.latestTag(); const versionInLatestTag = latestTag.slice(tagVersionPrefix.length); @@ -105,139 +92,97 @@ const np = async (input = 'patch', options, {pkg, rootDir, isYarnBerry}) => { const shouldEnable2FA = options['2fa'] && options.availability.isAvailable && !options.availability.isUnknown && !pkg.private && !npm.isExternalRegistry(pkg); - // Yarn berry doesn't support git commiting/tagging, so use npm - const shouldUseYarnForVersioning = options.yarn === true && !isYarnBerry; - const shouldUseNpmForVersioning = options.yarn === false || isYarnBerry; - // To prevent the process from hanging due to watch mode (e.g. when running `vitest`) const ciEnvOptions = {env: {CI: 'true'}}; + /** @param {typeof options} _options */ + function getPublishCommand(_options) { + const publishCommand = pkgManager.publishCommand || (args => [pkgManager.cli, args]); + const args = getPackagePublishArguments(_options); + return publishCommand(args); + } + const tasks = new Listr([ { title: 'Prerequisite check', enabled: () => options.runPublish, - task: () => prerequisiteTasks(input, pkg, options), + task: () => prerequisiteTasks(input, pkg, options, pkgManager), }, { title: 'Git', task: () => gitTasks(options), }, - ...runCleanup ? [ - { - title: 'Cleanup', - enabled: () => !hasLockFile, - task: () => deleteAsync('node_modules'), - }, - { - title: `Installing dependencies using ${pkgManagerName}`, - enabled: () => options.yarn === true, - task() { - const args = isYarnBerry ? ['install', '--immutable'] : ['install', '--frozen-lockfile', '--production=false']; - return exec('yarn', args).pipe( - catchError(async error => { - if ((!error.stderr.startsWith('error Your lockfile needs to be updated'))) { - return; - } - - if (await git.checkIfFileGitIgnored('yarn.lock')) { - return; - } - - throw new Error('yarn.lock file is outdated. Run yarn, commit the updated lockfile and try again.'); - }), - ); + { + title: 'Cleanup', + enabled: () => runCleanup && !lockfile, + task: () => deleteAsync('node_modules'), + }, + { + title: `Installing dependencies using ${pkgManager.id}`, + enabled: () => runCleanup, + task: () => new Listr([ + { + title: 'Running install command', + task() { + const installCommand = lockfile ? pkgManager.installCommand : pkgManager.installCommandNoLockfile; + return exec(...installCommand); + }, }, - }, - { - title: 'Installing dependencies using npm', - enabled: () => options.yarn === false, - task() { - const args = hasLockFile ? ['ci'] : ['install', '--no-package-lock', '--no-production']; - return exec('npm', [...args, '--engine-strict']); + { + title: 'Checking working tree is still clean', // If lockfile was out of date and tracked by git, this will fail + task: () => git.verifyWorkingTreeIsClean(), }, - }, - ] : [], - ...runTests ? [ - { - title: `Running tests using ${pkgManagerName}`, - enabled: () => options.yarn === false, - task: () => exec('npm', testCommand, ciEnvOptions), - }, - { - title: `Running tests using ${pkgManagerName}`, - enabled: () => options.yarn === true, - task: () => exec('yarn', testCommand, ciEnvOptions).pipe( - catchError(error => { - if (error.message.includes(`Command "${testScript}" not found`)) { - return []; - } - - return throwError(() => error); - }), - ), - }, - ] : [], + ]), + }, { - title: `Bumping version using ${pkgManagerName}`, - enabled: () => shouldUseYarnForVersioning, - skip() { - if (options.preview) { - let previewText = `[Preview] Command not executed: yarn version --new-version ${input}`; - - if (options.message) { - previewText += ` --message '${options.message.replaceAll('%s', input)}'`; - } - - return `${previewText}.`; - } - }, - task() { - const args = ['version', '--new-version', input]; - - if (options.message) { - args.push('--message', options.message); - } - - return exec('yarn', args); - }, + title: 'Running tests', + enabled: () => runTests, + task: () => exec(pkgManager.cli, ['run', testScript], ciEnvOptions), }, { - title: 'Bumping version using npm', - enabled: () => shouldUseNpmForVersioning, + title: 'Bumping version', skip() { if (options.preview) { - let previewText = `[Preview] Command not executed: npm version ${input}`; + const [cli, args] = pkgManager.versionCommand(input); if (options.message) { - previewText += ` --message '${options.message.replaceAll('%s', input)}'`; + args.push('--message', options.message.replaceAll('%s', input)); } - return `${previewText}.`; + return `[Preview] Command not executed: ${printCommand([cli, args])}`; } }, task() { - const args = ['version', input]; + const [cli, args] = pkgManager.versionCommand(input); if (options.message) { args.push('--message', options.message); } - return exec('npm', args); + return exec(cli, args); }, }, ...options.runPublish ? [ { - title: `Publishing package using ${pkgManagerName}`, + title: 'Publishing package', skip() { if (options.preview) { - const args = getPackagePublishArguments(options, isYarnBerry); - return `[Preview] Command not executed: ${pkgManager} ${args.join(' ')}.`; + const command = getPublishCommand(options); + return `[Preview] Command not executed: ${printCommand(command)}.`; } }, + /** @type {(context, task) => Listr.ListrTaskResult} */ task(context, task) { let hasError = false; - return publish(context, pkgManager, isYarnBerry, task, options) + return from(execa(...getPublishCommand(options))) + .pipe( + catchError(error => handleNpmError(error, task, otp => { + context.otp = otp; + + return execa(...getPublishCommand({...options, otp})); + })), + ) .pipe( catchError(async error => { hasError = true; @@ -289,7 +234,7 @@ const np = async (input = 'patch', options, {pkg, rootDir, isYarnBerry}) => { } }, // TODO: parse version outside of index - task: () => releaseTaskHelper(options, pkg), + task: () => releaseTaskHelper(options, pkg, pkgManager), }] : [], ], { showSubtasks: false, diff --git a/source/npm/handle-npm-error.js b/source/npm/handle-npm-error.js index 9ad57ba7..9709e2cb 100644 --- a/source/npm/handle-npm-error.js +++ b/source/npm/handle-npm-error.js @@ -28,7 +28,7 @@ const handleNpmError = (error, task, message, executor) => { // https://stackoverflow.com/a/44862841/10292952 if ( error.code === 402 - || error.stderr.includes('npm ERR! 402 Payment Required') // Npm + || error.stderr.includes('npm ERR! 402 Payment Required') // Npm/pnpm || error.stdout.includes('Response Code: 402 (Payment Required)') // Yarn Berry ) { throw new Error('You cannot publish a scoped package privately without a paid plan. Did you mean to publish publicly?'); diff --git a/source/npm/publish.js b/source/npm/publish.js index a35a2d5a..8a499239 100644 --- a/source/npm/publish.js +++ b/source/npm/publish.js @@ -1,9 +1,5 @@ -import {execa} from 'execa'; -import {from, catchError} from 'rxjs'; -import handleNpmError from './handle-npm-error.js'; - -export const getPackagePublishArguments = (options, isYarnBerry) => { - const args = isYarnBerry ? ['npm', 'publish'] : ['publish']; +export const getPackagePublishArguments = options => { + const args = ['publish']; if (options.contents) { args.push(options.contents); @@ -23,16 +19,3 @@ export const getPackagePublishArguments = (options, isYarnBerry) => { return args; }; - -const pkgPublish = (pkgManager, isYarnBerry, options) => execa(pkgManager, getPackagePublishArguments(options, isYarnBerry)); - -const publish = (context, pkgManager, isYarnBerry, task, options) => - from(pkgPublish(pkgManager, isYarnBerry, options)).pipe( - catchError(error => handleNpmError(error, task, otp => { - context.otp = otp; - - return pkgPublish(pkgManager, isYarnBerry, {...options, otp}); - })), - ); - -export default publish; diff --git a/source/npm/util.js b/source/npm/util.js index aa4bccbd..a3250832 100644 --- a/source/npm/util.js +++ b/source/npm/util.js @@ -144,18 +144,3 @@ export const getFilesToBePacked = async rootDir => { const {files} = JSON.parse(stdout).at(0); return files.map(file => file.path); }; - -export const getRegistryUrl = async (pkgManager, pkg) => { - if (pkgManager === 'yarn-berry') { - const {stdout} = await execa('yarn', ['config', 'get', 'npmRegistryServer']); - return stdout; - } - - const args = ['config', 'get', 'registry']; - if (isExternalRegistry(pkg)) { - args.push('--registry', pkg.publishConfig.registry); - } - - const {stdout} = await execa(pkgManager, args); - return stdout; -}; diff --git a/source/package-manager/configs.js b/source/package-manager/configs.js new file mode 100644 index 00000000..45bfc456 --- /dev/null +++ b/source/package-manager/configs.js @@ -0,0 +1,51 @@ +/** @type {import('./types.d.ts').PackageManagerConfig} */ +export const npmConfig = { + cli: 'npm', + id: 'npm', + installCommand: ['npm', ['ci', '--engine-strict']], + installCommandNoLockfile: ['npm', ['install', '--no-package-lock', '--no-production', '--engine-strict']], + versionCommand: version => ['npm', ['version', version]], + getRegistryCommand: ['npm', ['config', 'get', 'registry']], + tagVersionPrefixCommand: ['npm', ['config', 'get', 'tag-version-prefix']], + lockfiles: ['package-lock.json', 'npm-shrinkwrap.json'], +}; + +/** @type {import('./types.d.ts').PackageManagerConfig} */ +export const pnpmConfig = { + cli: 'pnpm', + id: 'pnpm', + installCommand: ['pnpm', ['install']], + installCommandNoLockfile: ['pnpm', ['install']], + versionCommand: version => ['pnpm', ['version', version]], + tagVersionPrefixCommand: ['pnpm', ['config', 'get', 'tag-version-prefix']], + getRegistryCommand: ['pnpm', ['config', 'get', 'registry']], + lockfiles: ['pnpm-lock.yaml'], +}; + +/** @type {import('./types.d.ts').PackageManagerConfig} */ +export const yarnConfig = { + cli: 'yarn', + id: 'yarn', + installCommand: ['yarn', ['install', '--frozen-lockfile', '--production=false']], + installCommandNoLockfile: ['yarn', ['install', '--production=false']], + getRegistryCommand: ['yarn', ['config', 'get', 'registry']], + tagVersionPrefixCommand: ['yarn', ['config', 'get', 'version-tag-prefix']], + versionCommand: version => ['yarn', ['version', '--new-version', version]], + lockfiles: ['yarn.lock'], +}; + +/** @type {import('./types.d.ts').PackageManagerConfig} */ +export const yarnBerryConfig = { + cli: 'yarn', + id: 'yarn-berry', + installCommand: ['yarn', ['install', '--immutable']], + installCommandNoLockfile: ['yarn', ['install']], + // Yarn berry doesn't support git committing/tagging, so we use npm instead + versionCommand: version => ['npm', ['version', version]], + tagVersionPrefixCommand: ['yarn', ['config', 'get', 'version-tag-prefix']], + // Yarn berry offloads publishing to npm, e.g. `yarn npm publish x.y.z` + publishCommand: args => ['yarn', ['npm', ...args]], + getRegistryCommand: ['yarn', ['config', 'get', 'npmRegistryServer']], + throwOnExternalRegistry: true, + lockfiles: ['yarn.lock'], +}; diff --git a/source/package-manager/index.js b/source/package-manager/index.js new file mode 100644 index 00000000..93252f72 --- /dev/null +++ b/source/package-manager/index.js @@ -0,0 +1,60 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import semver from 'semver'; +import * as configs from './configs.js'; + +/** +@param {string} rootDir +@param {import('./types.d.ts').PackageManagerConfig} config +*/ +export function findLockfile(rootDir, config) { + return config.lockfiles + .map(filename => path.resolve(rootDir || '.', filename)) + .find(filepath => fs.existsSync(filepath)); +} + +/** +@param {string} rootDir +@param {import('read-pkg').NormalizedPackageJson} pkg +*/ +export function getPackageManagerConfig(rootDir, pkg) { + const config = configFromPackageManagerField(pkg); + return config || configFromLockfile(rootDir) || configs.npmConfig; +} + +/** @param {import('read-pkg').NormalizedPackageJson} pkg */ +function configFromPackageManagerField(pkg) { + if (typeof pkg.packageManager !== 'string') { + return undefined; + } + + const [packageManager, version] = pkg.packageManager.split('@'); + + if (packageManager === 'yarn' && version && semver.gte(version, '2.0.0')) { + return configs.yarnBerryConfig; + } + + if (packageManager === 'npm') { + return configs.npmConfig; + } + + if (packageManager === 'pnpm') { + return configs.pnpmConfig; + } + + if (packageManager === 'yarn') { + return configs.yarnConfig; + } + + throw new Error(`Invalid package manager: ${pkg.packageManager}`); +} + +/** @param {string} rootDir */ +function configFromLockfile(rootDir, options = [configs.npmConfig, configs.pnpmConfig, configs.yarnConfig]) { + return options.find(config => findLockfile(rootDir, config)); +} + +/** @param {import('./types.d.ts').Command} command */ +export function printCommand([cli, args]) { + return `${cli} ${args.join(' ')}`; +} diff --git a/source/package-manager/types.d.ts b/source/package-manager/types.d.ts new file mode 100644 index 00000000..7a3d6264 --- /dev/null +++ b/source/package-manager/types.d.ts @@ -0,0 +1,58 @@ +export type PackageManager = 'npm' | 'yarn' | 'pnpm'; + +/** +CLI and arguments, which can be passed to `execa`. +*/ +export type Command = [cli: string, args: string[]]; + +export type PackageManagerConfig = { + /** + The main CLI, e.g. the `npm` in `npm install`, `npm test`, etc. + */ + cli: PackageManager; + + /** + How the package manager should be referred to in user-facing messages (since there are two different configs for some, e.g. yarn and yarn-berry). + */ + id: string; + + /** + How to install packages when there is a lockfile, e.g. `["npm", ["install"]]`. + */ + installCommand: Command; + + /** + How to install packages when there is no lockfile, e.g. `["npm", ["install"]]`. + */ + installCommandNoLockfile: Command; + + /** + Given a version string, return a version command e.g. `version => ["npm", ["version", version]]`. + */ + versionCommand: (version: string) => [cli: string, args: string[]]; + + /** + Modify the actual publish command. Defaults to `args => [config.cli, args]`. + */ + publishCommand?: (args: string[]) => Command; + + /** + CLI command which is expected to output the npm registry to use, e.g. `['npm', ['config', 'get', 'registry']]`. + */ + getRegistryCommand: Command; + + /** + CLI command expected to output the version tag prefix (often "v"). e,g. `['npm', ['config', 'get', 'tag-version-prefix']]`. + */ + tagVersionPrefixCommand: Command; + + /** + Set to true if the package manager doesn't support external registries. `np` will throw if one is detected and this is set. + */ + throwOnExternalRegistry?: boolean; + + /** + List of lockfile names expected for this package manager, relative to CWD. e.g. `['package-lock.json', 'npm-shrinkwrap.json']`. + */ + lockfiles: string[]; +}; diff --git a/source/prerequisite-tasks.js b/source/prerequisite-tasks.js index 4f9fc597..529821a6 100644 --- a/source/prerequisite-tasks.js +++ b/source/prerequisite-tasks.js @@ -6,7 +6,7 @@ import * as util from './util.js'; import * as git from './git-util.js'; import * as npm from './npm/util.js'; -const prerequisiteTasks = (input, pkg, options) => { +const prerequisiteTasks = (input, pkg, options, pkgManager) => { const isExternalRegistry = npm.isExternalRegistry(pkg); let newVersion; @@ -17,15 +17,10 @@ const prerequisiteTasks = (input, pkg, options) => { task: async () => npm.checkConnection(), }, { - title: 'Check npm version', - task: async () => npm.verifyRecentNpmVersion(), - }, - { - title: 'Check yarn version', - enabled: () => options.yarn === true, + title: `Check ${pkgManager.cli} version`, async task() { - const {stdout: yarnVersion} = await execa('yarn', ['--version']); - util.validateEngineVersionSatisfies('yarn', yarnVersion); + const {stdout: version} = await execa(pkgManager.cli, ['--version']); + util.validateEngineVersionSatisfies(pkgManager.cli, version); }, }, { @@ -77,7 +72,7 @@ const prerequisiteTasks = (input, pkg, options) => { async task() { await git.fetch(); - const tagPrefix = await util.getTagVersionPrefix(options); + const tagPrefix = await util.getTagVersionPrefix(pkgManager); await git.verifyTagDoesNotExistOnRemote(`${tagPrefix}${newVersion}`); }, diff --git a/source/release-task-helper.js b/source/release-task-helper.js index ebaf2269..7f09c9f5 100644 --- a/source/release-task-helper.js +++ b/source/release-task-helper.js @@ -3,12 +3,12 @@ import newGithubReleaseUrl from 'new-github-release-url'; import {getTagVersionPrefix, getPreReleasePrefix} from './util.js'; import Version from './version.js'; -const releaseTaskHelper = async (options, pkg) => { +const releaseTaskHelper = async (options, pkg, pkgManager) => { const newVersion = options.releaseDraftOnly ? new Version(pkg.version) - : new Version(pkg.version).setFrom(options.version.toString(), {prereleasePrefix: await getPreReleasePrefix(options)}); + : new Version(pkg.version).setFrom(options.version.toString(), {prereleasePrefix: await getPreReleasePrefix(pkgManager)}); - const tag = await getTagVersionPrefix(options) + newVersion.toString(); + const tag = await getTagVersionPrefix(pkgManager) + newVersion.toString(); const url = newGithubReleaseUrl({ repoUrl: options.repoUrl, diff --git a/source/ui.js b/source/ui.js index 3fb1a607..115c08a8 100644 --- a/source/ui.js +++ b/source/ui.js @@ -4,6 +4,8 @@ import githubUrlFromGit from 'github-url-from-git'; import {htmlEscape} from 'escape-goat'; import isScoped from 'is-scoped'; import isInteractive from 'is-interactive'; +import {execa} from 'execa'; +import {getPackageManagerConfig} from './package-manager/index.js'; import Version, {SEMVER_INCREMENTS} from './version.js'; import * as util from './util.js'; import * as git from './git-util.js'; @@ -119,29 +121,22 @@ const checkNewFilesAndDependencies = async (pkg, rootDir) => { return answers.confirm; }; -// eslint-disable-next-line complexity -const ui = async (options, {pkg, rootDir, isYarnBerry = false}) => { +/** +@param {import('./cli-implementation.js').CLI['flags']} options +@param {{pkg: import('read-pkg').NormalizedPackageJson; rootDir: string}} context +*/ +const ui = async (options, {pkg, rootDir}) => { const oldVersion = pkg.version; const extraBaseUrls = ['gitlab.com']; const repoUrl = pkg.repository && githubUrlFromGit(pkg.repository.url, {extraBaseUrls}); - const pkgManager = (() => { - if (!options.yarn) { - return 'npm'; - } - - if (isYarnBerry) { - return 'yarn-berry'; - } - - return 'yarn'; - })(); + const pkgManager = getPackageManagerConfig(rootDir, pkg); - if (isYarnBerry && npm.isExternalRegistry(pkg)) { - throw new Error('External registry is not yet supported with Yarn Berry'); + if (pkgManager.throwOnExternalRegistry && npm.isExternalRegistry(pkg)) { + throw new Error(`External registry is not yet supported with ${pkgManager.id}.`); } - const registryUrl = await npm.getRegistryUrl(pkgManager, pkg); + const {stdout: registryUrl} = await execa(...pkgManager.getRegistryCommand); const releaseBranch = options.branch; if (options.runPublish) { @@ -160,7 +155,7 @@ const ui = async (options, {pkg, rootDir, isYarnBerry = false}) => { console.log(`\nCreate a release draft on GitHub for ${chalk.bold.magenta(pkg.name)} ${chalk.dim(`(current: ${oldVersion})`)}\n`); } else { const versionText = options.version - ? chalk.dim(`(current: ${oldVersion}, next: ${new Version(oldVersion, options.version, {prereleasePrefix: await util.getPreReleasePrefix(options)}).format()})`) + ? chalk.dim(`(current: ${oldVersion}, next: ${new Version(oldVersion, options.version, {prereleasePrefix: await util.getPreReleasePrefix(pkgManager)}).format()})`) : chalk.dim(`(current: ${oldVersion})`); console.log(`\nPublish a new version of ${chalk.bold.magenta(pkg.name)} ${versionText}\n`); @@ -241,7 +236,7 @@ const ui = async (options, {pkg, rootDir, isYarnBerry = false}) => { && !options.tag ); - const alreadyPublicScoped = isYarnBerry && options.runPublish && await util.getNpmPackageAccess(pkg.name) === 'public'; + const alreadyPublicScoped = pkgManager.id === 'yarn-berry' && options.runPublish && await util.getNpmPackageAccess(pkg.name) === 'public'; // Note that inquirer question.when is a bit confusing. Only `false` will cause the question to be skipped. // Any other value like `true` and `undefined` means ask the question. diff --git a/source/util.js b/source/util.js index 379aba31..e0ea3e66 100644 --- a/source/util.js +++ b/source/util.js @@ -60,13 +60,12 @@ export const linkifyCommitRange = (url, commitRange) => { return terminalLink(commitRange, `${url}/compare/${commitRange}`); }; -export const getTagVersionPrefix = pMemoize(async options => { - ow(options, ow.object.hasKeys('yarn')); +/** @type {(config: import('./package-manager/types.js').PackageManagerConfig) => Promise} */ +export const getTagVersionPrefix = pMemoize(async config => { + ow(config, ow.object.hasKeys('tagVersionPrefixCommand')); try { - const {stdout} = options.yarn - ? await execa('yarn', ['config', 'get', 'version-tag-prefix']) - : await execa('npm', ['config', 'get', 'tag-version-prefix']); + const {stdout} = await execa(...config.tagVersionPrefixCommand); return stdout; } catch { @@ -131,12 +130,12 @@ export const getNewDependencies = async (newPkg, rootDir) => { return newDependencies; }; -export const getPreReleasePrefix = pMemoize(async options => { - ow(options, ow.object.hasKeys('yarn')); +/** @type {(config: import('./package-manager/types.js').PackageManagerConfig) => Promise} */ +export const getPreReleasePrefix = pMemoize(async config => { + ow(config, ow.object.hasKeys('cli')); try { - const packageManager = options.yarn ? 'yarn' : 'npm'; - const {stdout} = await execa(packageManager, ['config', 'get', 'preid']); + const {stdout} = await execa(config.cli, ['config', 'get', 'preid']); return stdout === 'undefined' ? '' : stdout; } catch { diff --git a/source/yarn.js b/source/yarn.js deleted file mode 100644 index d4da4275..00000000 --- a/source/yarn.js +++ /dev/null @@ -1,16 +0,0 @@ -import semver from 'semver'; - -export function checkIfYarnBerry(pkg) { - if (typeof pkg.packageManager !== 'string') { - return false; - } - - const match = pkg.packageManager.match(/^yarn@(.+)$/); - if (!match) { - return false; - } - - const [, yarnVersion] = match; - const versionParsed = semver.parse(yarnVersion); - return (versionParsed.major >= 2); -} diff --git a/test/cli.js b/test/cli.js index c898fd67..2406bd82 100644 --- a/test/cli.js +++ b/test/cli.js @@ -24,13 +24,13 @@ test('flags: --help', cliPasses, cli, '--help', [ '--no-publish Skips publishing', '--preview Show tasks without actually executing them', '--tag Publish under a given dist-tag', - '--no-yarn Don\'t use Yarn', '--contents Subdirectory to publish', '--no-release-draft Skips opening a GitHub release draft', '--release-draft-only Only opens a GitHub release draft for the latest published version', '--test-script Name of npm run script to run tests before publishing (default: test)', '--no-2fa Don\'t enable 2FA on new packages (not recommended)', '--message Version bump commit message, \'%s\' will be replaced with version (default: \'%s\' with npm and \'v%s\' with yarn)', + '--package-manager Use a specific package manager (default: \'packageManager\' field in package.json)', '', 'Examples', '$ np', diff --git a/test/git-util/check-if-file-git-ignored.js b/test/git-util/check-if-file-git-ignored.js deleted file mode 100644 index 1c5ba52b..00000000 --- a/test/git-util/check-if-file-git-ignored.js +++ /dev/null @@ -1,20 +0,0 @@ -import test from 'ava'; -import {temporaryDirectory} from 'tempy'; -import {checkIfFileGitIgnored} from '../../source/git-util.js'; - -test('returns true for ignored files', async t => { - t.true(await checkIfFileGitIgnored('yarn.lock')); -}); - -test('returns false for non-ignored files', async t => { - t.false(await checkIfFileGitIgnored('package.json')); -}); - -test('errors if path is outside of repo', async t => { - const temporary = temporaryDirectory(); - - await t.throwsAsync( - checkIfFileGitIgnored(`${temporary}/file.js`), - {message: /fatal:/}, - ); -}); diff --git a/test/index.js b/test/index.js index bf3ac759..7e3b3da2 100644 --- a/test/index.js +++ b/test/index.js @@ -40,18 +40,24 @@ test('errors on too low version', npFails, /New version 1\.0\.0(?:-beta)? should be higher than current version \d+\.\d+\.\d+/, ); +const fakeExecaReturn = () => Object.assign( + Promise.resolve({pipe: sinon.stub()}), + {stdout: '', stderr: ''}, +); + test('skip enabling 2FA if the package exists', async t => { const enable2faStub = sinon.stub(); /** @type {typeof np} */ const npMock = await esmock('../source/index.js', { del: {deleteAsync: sinon.stub()}, - execa: {execa: sinon.stub().returns({pipe: sinon.stub()})}, + execa: {execa: sinon.stub().returns(fakeExecaReturn())}, '../source/prerequisite-tasks.js': sinon.stub(), '../source/git-tasks.js': sinon.stub(), '../source/git-util.js': { hasUpstream: sinon.stub().returns(true), pushGraceful: sinon.stub(), + verifyWorkingTreeIsClean: sinon.stub(), }, '../source/npm/enable-2fa.js': enable2faStub, '../source/npm/publish.js': sinon.stub().returns({pipe: sinon.stub()}), @@ -74,12 +80,13 @@ test('skip enabling 2FA if the `2fa` option is false', async t => { /** @type {typeof np} */ const npMock = await esmock('../source/index.js', { del: {deleteAsync: sinon.stub()}, - execa: {execa: sinon.stub().returns({pipe: sinon.stub()})}, + execa: {execa: sinon.stub().returns(fakeExecaReturn())}, '../source/prerequisite-tasks.js': sinon.stub(), '../source/git-tasks.js': sinon.stub(), '../source/git-util.js': { hasUpstream: sinon.stub().returns(true), pushGraceful: sinon.stub(), + verifyWorkingTreeIsClean: sinon.stub(), }, '../source/npm/enable-2fa.js': enable2faStub, '../source/npm/publish.js': sinon.stub().returns({pipe: sinon.stub()}), diff --git a/test/npm/util/get-registry-url.js b/test/npm/util/get-registry-url.js deleted file mode 100644 index 318804da..00000000 --- a/test/npm/util/get-registry-url.js +++ /dev/null @@ -1,49 +0,0 @@ -import test from 'ava'; -import {_createFixture} from '../../_helpers/stub-execa.js'; - -/** @type {ReturnType>} */ -const createFixture = _createFixture('../../../source/npm/util.js', import.meta.url); - -test('npm', createFixture, [{ - command: 'npm config get registry', - stdout: 'https://registry.npmjs.org/', -}], async ({t, testedModule: npm}) => { - t.is( - await npm.getRegistryUrl('npm', {}), - 'https://registry.npmjs.org/', - ); -}); - -test('yarn', createFixture, [{ - command: 'yarn config get registry', - stdout: 'https://registry.yarnpkg.com', -}], async ({t, testedModule: npm}) => { - t.is( - await npm.getRegistryUrl('yarn', {}), - 'https://registry.yarnpkg.com', - ); -}); - -test('yarn-berry', createFixture, [{ - command: 'yarn config get npmRegistryServer', - stdout: 'https://registry.yarnpkg.com', -}], async ({t, testedModule: npm}) => { - t.is( - await npm.getRegistryUrl('yarn-berry', {}), - 'https://registry.yarnpkg.com', - ); -}); - -test('external', createFixture, [{ - command: 'npm config get registry --registry http://my-internal-registry.local', - stdout: 'http://my-internal-registry.local', -}], async ({t, testedModule: npm}) => { - t.is( - await npm.getRegistryUrl('npm', { - publishConfig: { - registry: 'http://my-internal-registry.local', - }, - }), - 'http://my-internal-registry.local', - ); -}); diff --git a/test/tasks/prerequisite-tasks.js b/test/tasks/prerequisite-tasks.js index 2363ff0a..b7480912 100644 --- a/test/tasks/prerequisite-tasks.js +++ b/test/tasks/prerequisite-tasks.js @@ -1,5 +1,6 @@ import process from 'node:process'; import test from 'ava'; +import {npmConfig, yarnConfig} from '../../source/package-manager/configs.js'; import actualPrerequisiteTasks from '../../source/prerequisite-tasks.js'; import {npPkg} from '../../source/util.js'; import {SilentRenderer} from '../_helpers/listr-renderer.js'; @@ -21,7 +22,7 @@ test.serial('public-package published on npm registry: should fail when npm regi stderr: 'failed', }], async ({t, testedModule: prerequisiteTasks}) => { await t.throwsAsync( - run(prerequisiteTasks('1.0.0', {name: 'test'}, {})), + run(prerequisiteTasks('1.0.0', {name: 'test'}, {}, npmConfig)), {message: 'Connection to npm registry failed'}, ); @@ -33,7 +34,7 @@ test.serial('private package: should disable task pinging npm registry', createF stdout: '', }], async ({t, testedModule: prerequisiteTasks}) => { 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}, {}, npmConfig)), ); assertTaskDisabled(t, 'Ping npm registry'); @@ -44,7 +45,7 @@ test.serial('external registry: should disable task pinging npm registry', creat stdout: '', }], async ({t, testedModule: prerequisiteTasks}) => { 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'}}, {}, npmConfig)), ); assertTaskDisabled(t, 'Ping npm registry'); @@ -63,7 +64,7 @@ test.serial('should fail when npm version does not match range in `package.json` const depRange = npPkg.engines.npm; await t.throwsAsync( - run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {}, npmConfig)), {message: `\`np\` requires npm ${depRange}`}, ); @@ -83,7 +84,7 @@ test.serial('should fail when yarn version does not match range in `package.json const depRange = npPkg.engines.yarn; await t.throwsAsync( - run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: true})), + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {}, yarnConfig)), {message: `\`np\` requires yarn ${depRange}`}, ); @@ -103,7 +104,7 @@ test.serial('should fail when user is not authenticated at npm registry', create process.env.NODE_ENV = 'P'; await t.throwsAsync( - run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {}, npmConfig)), {message: 'You do not have write permissions required to publish this package.'}, ); @@ -125,7 +126,7 @@ test.serial('should fail when user is not authenticated at external registry', c 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})), + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0', publishConfig: {registry: 'http://my.io'}}, {}, npmConfig)), {message: 'You do not have write permissions required to publish this package.'}, ); @@ -143,7 +144,7 @@ test.serial('private package: should disable task `verify user is authenticated` 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}, {}, npmConfig)), ); process.env.NODE_ENV = 'test'; @@ -158,7 +159,7 @@ test.serial('should fail when git version does not match range in `package.json` const depRange = npPkg.engines.git; await t.throwsAsync( - run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {}, npmConfig)), {message: `\`np\` requires git ${depRange}`}, ); @@ -172,7 +173,7 @@ test.serial('should fail when git remote does not exist', createFixture, [{ stderr: 'not found', }], async ({t, testedModule: prerequisiteTasks}) => { await t.throwsAsync( - run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {}, npmConfig)), {message: 'not found'}, ); @@ -181,7 +182,7 @@ test.serial('should fail when git remote does not exist', createFixture, [{ test.serial('should fail when version is invalid', async t => { await t.throwsAsync( - run(actualPrerequisiteTasks('DDD', {name: 'test', version: '1.0.0'}, {yarn: false})), + run(actualPrerequisiteTasks('DDD', {name: 'test', version: '1.0.0'}, {}, npmConfig)), {message: 'New version DDD should either be one of patch, minor, major, prepatch, preminor, premajor, prerelease, or a valid SemVer version.'}, ); @@ -190,7 +191,7 @@ test.serial('should fail when version is invalid', async t => { test.serial('should fail when version is lower than latest version', async t => { await t.throwsAsync( - run(actualPrerequisiteTasks('0.1.0', {name: 'test', version: '1.0.0'}, {yarn: false})), + run(actualPrerequisiteTasks('0.1.0', {name: 'test', version: '1.0.0'}, {}, npmConfig)), {message: 'New version 0.1.0 should be higher than current version 1.0.0.'}, ); @@ -199,7 +200,7 @@ test.serial('should fail when version is lower than 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})), + run(actualPrerequisiteTasks('2.0.0-1', {name: 'test', version: '1.0.0'}, {}, npmConfig)), {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'}, ); @@ -211,7 +212,7 @@ test.serial('should not fail when prerelease version of public package with dist stdout: '', }], async ({t, testedModule: prerequisiteTasks}) => { 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'}, {tag: 'pre'}, npmConfig)), ); }); @@ -220,7 +221,7 @@ test.serial('should not fail when prerelease version of private package without stdout: '', }], async ({t, testedModule: prerequisiteTasks}) => { 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}, {}, npmConfig)), ); }); @@ -229,7 +230,7 @@ test.serial('should fail when git tag already exists', createFixture, [{ stdout: 'vvb', }], async ({t, testedModule: prerequisiteTasks}) => { await t.throwsAsync( - run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {yarn: false})), + run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {}, npmConfig)), {message: 'Git tag `v2.0.0` already exists.'}, ); @@ -241,6 +242,6 @@ test.serial('checks should pass', createFixture, [{ stdout: '', }], async ({t, testedModule: prerequisiteTasks}) => { 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'}, {}, npmConfig)), ); }); diff --git a/test/ui/new-files-dependencies.js b/test/ui/new-files-dependencies.js index 7c548cd7..fc5eb792 100644 --- a/test/ui/new-files-dependencies.js +++ b/test/ui/new-files-dependencies.js @@ -43,7 +43,6 @@ const createFixture = test.macro(async (t, pkg, commands, expected) => { const {ui, logs: logsArray} = await mockInquirer({t, answers: {confirm: {confirm: false}}, mocks: { './npm/util.js': { - getRegistryUrl: sinon.stub().resolves(''), checkIgnoreStrategy: sinon.stub().resolves(), }, 'node:process': {cwd: () => temporaryDir}, diff --git a/test/ui/prompts/tags.js b/test/ui/prompts/tags.js index 52423b40..1cf6acdc 100644 --- a/test/ui/prompts/tags.js +++ b/test/ui/prompts/tags.js @@ -6,7 +6,6 @@ import {mockInquirer} from '../../_helpers/mock-inquirer.js'; const testUi = test.macro(async (t, {version, tags, answers}, assertions) => { const {ui, logs} = await mockInquirer({t, answers: {confirm: true, ...answers}, mocks: { './npm/util.js': { - getRegistryUrl: sinon.stub().resolves(''), checkIgnoreStrategy: sinon.stub().resolves(), prereleaseTags: sinon.stub().resolves(tags), }, @@ -18,6 +17,9 @@ const testUi = test.macro(async (t, {version, tags, answers}, assertions) => { latestTagOrFirstCommit: sinon.stub().resolves(`v${npPkg.version}`), commitLogFromRevision: sinon.stub().resolves(''), }, + './package-manager/index.js': { + findLockfile: sinon.stub().resolves(undefined), + }, }}); const results = await ui({ diff --git a/test/ui/prompts/version.js b/test/ui/prompts/version.js index eb1021fb..8cb125e9 100644 --- a/test/ui/prompts/version.js +++ b/test/ui/prompts/version.js @@ -5,7 +5,6 @@ import {mockInquirer} from '../../_helpers/mock-inquirer.js'; const testUi = test.macro(async (t, {version, answers}, assertions) => { const {ui, logs} = await mockInquirer({t, answers: {confirm: true, ...answers}, mocks: { './npm/util.js': { - getRegistryUrl: sinon.stub().resolves(''), checkIgnoreStrategy: sinon.stub().resolves(), }, './util.js': { diff --git a/test/util/get-pre-release-prefix.js b/test/util/get-pre-release-prefix.js index 113294c7..f99b2afc 100644 --- a/test/util/get-pre-release-prefix.js +++ b/test/util/get-pre-release-prefix.js @@ -12,7 +12,7 @@ test('returns preid postfix if set - npm', createFixture, [{ stdout: 'pre', }], async ({t, testedModule: {getPreReleasePrefix}}) => { t.is( - await getPreReleasePrefix({yarn: false}), + await getPreReleasePrefix({cli: 'npm'}), 'pre', ); }); @@ -22,7 +22,7 @@ test('returns preid postfix if set - yarn', createFixture, [{ stdout: 'pre', }], async ({t, testedModule: {getPreReleasePrefix}}) => { t.is( - await getPreReleasePrefix({yarn: true}), + await getPreReleasePrefix({cli: 'yarn'}), 'pre', ); }); @@ -32,7 +32,7 @@ test('returns empty string if not set - npm', createFixture, [{ stdout: 'undefined', }], async ({t, testedModule: {getPreReleasePrefix}}) => { t.is( - await getPreReleasePrefix({yarn: false}), + await getPreReleasePrefix({cli: 'npm'}), '', ); }); @@ -42,7 +42,7 @@ test('returns empty string if not set - yarn', createFixture, [{ stdout: 'undefined', }], async ({t, testedModule: {getPreReleasePrefix}}) => { t.is( - await getPreReleasePrefix({yarn: true}), + await getPreReleasePrefix({cli: 'yarn'}), '', ); }); @@ -52,13 +52,13 @@ test('no options passed', async t => { originalGetPreReleasePrefix(), {message: stripIndent` Expected argument to be of type \`object\` but received type \`undefined\` - Expected object to have keys \`["yarn"]\` + Expected object to have keys \`["cli"]\` `}, ); await t.throwsAsync( originalGetPreReleasePrefix({}), - {message: 'Expected object to have keys `["yarn"]`'}, + {message: 'Expected object to have keys `["cli"]`'}, ); }); @@ -66,7 +66,7 @@ test.serial('returns actual value', async t => { const originalPreid = process.env.NPM_CONFIG_PREID; process.env.NPM_CONFIG_PREID = 'beta'; - t.is(await originalGetPreReleasePrefix({yarn: false}), 'beta'); + t.is(await originalGetPreReleasePrefix({cli: 'npm'}), 'beta'); process.env.NPM_CONFIG_PREID = originalPreid; }); diff --git a/test/util/get-tag-version-prefix.js b/test/util/get-tag-version-prefix.js index aa387110..db714847 100644 --- a/test/util/get-tag-version-prefix.js +++ b/test/util/get-tag-version-prefix.js @@ -2,6 +2,7 @@ import test from 'ava'; import {stripIndent} from 'common-tags'; import {_createFixture} from '../_helpers/stub-execa.js'; import {getTagVersionPrefix as originalGetTagVersionPrefix} from '../../source/util.js'; +import {npmConfig, yarnConfig} from '../../source/package-manager/configs.js'; /** @type {ReturnType>} */ const createFixture = _createFixture('../../source/util.js', import.meta.url); @@ -11,7 +12,7 @@ test('returns tag prefix - npm', createFixture, [{ stdout: 'ver', }], async ({t, testedModule: {getTagVersionPrefix}}) => { t.is( - await getTagVersionPrefix({yarn: false}), + await getTagVersionPrefix(npmConfig), 'ver', ); }); @@ -21,7 +22,7 @@ test('returns preId postfix - yarn', createFixture, [{ stdout: 'ver', }], async ({t, testedModule: {getTagVersionPrefix}}) => { t.is( - await getTagVersionPrefix({yarn: true}), + await getTagVersionPrefix(yarnConfig), 'ver', ); }); @@ -31,7 +32,7 @@ test('defaults to "v" when command fails', createFixture, [{ exitCode: 1, }], async ({t, testedModule: {getTagVersionPrefix}}) => { t.is( - await getTagVersionPrefix({yarn: false}), + await getTagVersionPrefix(npmConfig), 'v', ); }); @@ -41,12 +42,12 @@ test('no options passed', async t => { originalGetTagVersionPrefix(), {message: stripIndent` Expected argument to be of type \`object\` but received type \`undefined\` - Expected object to have keys \`["yarn"]\` + Expected object to have keys \`["tagVersionPrefixCommand"]\` `}, ); await t.throwsAsync( originalGetTagVersionPrefix({}), - {message: 'Expected object to have keys `["yarn"]`'}, + {message: 'Expected object to have keys `["tagVersionPrefixCommand"]`'}, ); }); diff --git a/test/util/yarn.js b/test/util/yarn.js deleted file mode 100644 index bae4fd36..00000000 --- a/test/util/yarn.js +++ /dev/null @@ -1,15 +0,0 @@ -import test from 'ava'; -import {checkIfYarnBerry} from '../../source/yarn.js'; - -test('checkIfYarnBerry', t => { - t.is(checkIfYarnBerry({}), false); - t.is(checkIfYarnBerry({ - packageManager: 'npm', - }), false); - t.is(checkIfYarnBerry({ - packageManager: 'yarn@1.0.0', - }), false); - t.is(checkIfYarnBerry({ - packageManager: 'yarn@2.0.0', - }), true); -});