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..23247595 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 user 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 [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/cli-implementation.js b/source/cli-implementation.js index 9f7ffe45..7f5204d9 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,10 +11,9 @@ 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'; -const cli = meow(` +export const cli = meow(` Usage $ np @@ -31,13 +29,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 +78,8 @@ const cli = meow(` tag: { type: 'string', }, - yarn: { - type: 'boolean', - default: hasYarn(), + packageManager: { + type: 'string', }, contents: { type: 'string', @@ -105,7 +102,7 @@ const cli = meow(` updateNotifier({pkg: cli.pkg}).notify(); -try { +export async function getOptions() { const {pkg, rootDir} = await util.readPkg(cli.flags.contents); const localConfig = await config(rootDir); @@ -119,6 +116,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 +133,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/index.js b/source/index.js index 338dbaf3..03e99052 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 '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 releaseTaskHelper from './release-task-helper.js'; import * as util from './util.js'; import * as git from './git-util.js'; import * as npm from './npm/util.js'; +import {findLockFile, getPackageManagerConfig, printCommand} from './package-manager/index.js'; +import handleNpmError from './npm/handle-npm-error.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,37 +25,26 @@ 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 {any | typeof import('./cli-implementation.js').cli['unnormalizedFlags']} options + * @param {{pkg: import('read-pkg').NormalizedPackageJson; rootDir: string}} context + */ + +const np = async (input = 'patch', options, {pkg, rootDir}) => { + const pkgManager = getPackageManagerConfig(rootDir, pkg); + const publishCli = options.publishCli || pkgManager.cli; // 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); @@ -105,10 +94,6 @@ 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'}}; @@ -122,122 +107,78 @@ const np = async (input = 'patch', options, {pkg, rootDir, isYarnBerry}) => { 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: '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']); - }, - }, - ] : [], - ...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}.`; - } - }, + title: 'Cleanup', + enabled: () => runCleanup && !lockfile, + task: () => deleteAsync('node_modules'), + }, + { + title: `Installing dependencies using ${pkgManager.nickname}`, + enabled: () => runCleanup, task() { - const args = ['version', '--new-version', input]; + return exec(...pkgManager.installCommand).pipe( + catchError(async error => { + const isLockfileComplaint = error.stderr.startsWith('error Your lockfile needs to be updated'); - if (options.message) { - args.push('--message', options.message); - } + if (isLockfileComplaint && await git.checkIfFileGitIgnored(lockfile.filename)) { + return; + } - return exec('yarn', args); + throw new Error(`Lockfile is outdated. Run ${printCommand(pkgManager.installCommand)}, commit the updated lockfile and try again.`, { + cause: error, + }); + }), + ); }, }, { - title: 'Bumping version using npm', - enabled: () => shouldUseNpmForVersioning, + title: 'Running tests', + enabled: () => runTests, + task: () => exec(pkgManager.cli, ['run', testScript], ciEnvOptions), + }, + { + 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 args = getPackagePublishArguments(options); + return `[Preview] Command not executed: ${publishCli} ${args.join(' ')}.`; } }, task(context, task) { let hasError = false; - return publish(context, pkgManager, isYarnBerry, task, options) + return exec(publishCli, getPackagePublishArguments(options)) + .pipe( + catchError(error => handleNpmError(error, task, otp => { + context.otp = otp; + + return exec(publishCli, getPackagePublishArguments({...options, otp})); + })), + ) .pipe( catchError(async error => { hasError = true; 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..73f29f88 --- /dev/null +++ b/source/package-manager/configs.js @@ -0,0 +1,42 @@ +/** @type {import('./types.d.ts').PackageManagerConfig} */ + +export const npmConfig = { + cli: 'npm', + nickname: 'npm', + installCommand: ['npm', ['install', '--engine-strict']], + versionCommand: version => ['npm', ['version', version]], + getRegistryCommand: ['npm', ['config', 'get', 'registry']], + lockfiles: ['package-lock.json', 'npm-shrinkwrap.json'], +}; +/** @type {import('./types.d.ts').PackageManagerConfig} */ + +export const pnpmConfig = { + cli: 'pnpm', + nickname: 'pnpm', + installCommand: ['pnpm', ['install']], + versionCommand: version => ['pnpm', ['version', version]], + getRegistryCommand: ['pnpm', ['config', 'get', 'registry']], + lockfiles: ['pnpm-lock.yaml'], +}; +/** @type {import('./types.d.ts').PackageManagerConfig} */ + +export const yarnConfig = { + cli: 'yarn', + nickname: 'yarn', + installCommand: ['yarn', ['install', '--frozen-lockfile', '--production=false']], + getRegistryCommand: ['yarn', ['config', 'get', 'registry']], + versionCommand: version => ['yarn', ['version', '--new-version', version]], + lockfiles: ['yarn.lock'], +}; +/** @type {import('./types.d.ts').PackageManagerConfig} */ + +export const yarnBerryConfig = { + cli: 'yarn', + nickname: 'yarn-berry', + installCommand: ['yarn', ['install', '--immutable']], + versionCommand: version => ['yarn', ['version', '--new-version', version]], + publishCli: 'npm', // Yarn berry doesn't support git committing/tagging, so use npm + 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..108b87d8 --- /dev/null +++ b/source/package-manager/index.js @@ -0,0 +1,69 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import semver from 'semver'; +import {npmConfig, yarnBerryConfig, pnpmConfig, yarnConfig} from './configs.js'; + +/** + * @param {string} rootDir + * @param {import('./types.d.ts').PackageManagerConfig} config + */ +export function findLockFile(rootDir, config) { + return config.lockfiles + .map(filename => ({filename, filepath: path.resolve(rootDir, filename)})) + .find(({filepath}) => fs.existsSync(filepath)); +} + +/** + * @param {string} rootDir + * @param {import('read-pkg').NormalizedPackageJson} pkg + */ +export function getPackageManagerConfig(rootDir, pkg) { + let config = configFromPackageManagerField(pkg); + + if (config === npmConfig && findLockFile(rootDir, config)) { + // If npm and lockfile exists, use npm ci to install packages + config = { + ...config, + installCommand: ['npm', ['ci', '--engine-strict']], + }; + } + + return config || configFromLockfile(rootDir) || npmConfig; +} + +/** @param {import('read-pkg').NormalizedPackageJson} pkg */ +function configFromPackageManagerField(pkg) { + if (typeof pkg.packageManager !== 'string') { + return npmConfig; + } + + const [packageManager, version] = pkg.packageManager.split('@'); + + if (packageManager === 'yarn' && version && semver.gte(version, '2.0.0')) { + return yarnBerryConfig; + } + + if (packageManager === 'npm') { + return npmConfig; + } + + if (packageManager === 'pnpm') { + return pnpmConfig; + } + + if (packageManager === 'yarn') { + return yarnConfig; + } + + throw new Error(`Invalid package manager: ${pkg.packageManager}`); +} + +/** @param {string} rootDir */ +function configFromLockfile(rootDir, options = [npmConfig, pnpmConfig, 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..3b147d6b --- /dev/null +++ b/source/package-manager/types.d.ts @@ -0,0 +1,14 @@ +export type PackageManager = 'npm' | 'yarn' | 'pnpm'; + +export type Command = [cli: string, args: string[]]; + +export type PackageManagerConfig = { + cli: PackageManager; + nickname: string; + installCommand: Command; + versionCommand: (version: string) => [cli: string, args: string[]]; + publishCli?: string; + getRegistryCommand: Command; + throwOnExternalRegistry?: boolean; + lockfiles: string[]; +}; diff --git a/source/ui.js b/source/ui.js index 3fb1a607..67c04de6 100644 --- a/source/ui.js +++ b/source/ui.js @@ -4,10 +4,12 @@ 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 Version, {SEMVER_INCREMENTS} from './version.js'; import * as util from './util.js'; import * as git from './git-util.js'; import * as npm from './npm/util.js'; +import {getPackageManagerConfig} from './package-manager/index.js'; const printCommitLog = async (repoUrl, registryUrl, fromLatestTag, releaseBranch) => { const revision = fromLatestTag ? await git.latestTagOrFirstCommit() : await git.previousTagOrFirstCommit(); @@ -119,29 +121,19 @@ const checkNewFilesAndDependencies = async (pkg, rootDir) => { return answers.confirm; }; -// eslint-disable-next-line complexity -const ui = async (options, {pkg, rootDir, isYarnBerry = false}) => { +/** @type {(options: Options, context: {rootDir: string; pkg: import('read-pkg').NormalizedPackageJson}) => Promise} */ +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 packageManagerConfig = getPackageManagerConfig(rootDir, pkg); - if (isYarnBerry && npm.isExternalRegistry(pkg)) { - throw new Error('External registry is not yet supported with Yarn Berry'); + if (packageManagerConfig.throwOnExternalRegistry && npm.isExternalRegistry(pkg)) { + throw new Error(`External registry is not yet supported with ${packageManagerConfig.nickname}.`); } - const registryUrl = await npm.getRegistryUrl(pkgManager, pkg); + const {stdout: registryUrl} = await execa(...packageManagerConfig.getRegistryCommand); const releaseBranch = options.branch; if (options.runPublish) { @@ -241,7 +233,7 @@ const ui = async (options, {pkg, rootDir, isYarnBerry = false}) => { && !options.tag ); - const alreadyPublicScoped = isYarnBerry && options.runPublish && await util.getNpmPackageAccess(pkg.name) === 'public'; + const alreadyPublicScoped = packageManagerConfig.nickname === '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/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/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/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..62ccf4ef 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), }, 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/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); -});