diff --git a/readme.md b/readme.md index ed0c02a7..1f5930c8 100644 --- a/readme.md +++ b/readme.md @@ -49,7 +49,7 @@ ### Why not - Monorepos are not supported. -- Yarn >= 2 and pnpm are not supported. +- pnpm is not supported. - Custom registries are not supported ([but could be with your help](https://github.com/sindresorhus/np/issues/420)). - CI is [not an ideal environment](https://github.com/sindresorhus/np/issues/619#issuecomment-994493179) for `np`. It's meant to be used locally as an interactive tool. diff --git a/source/cli-implementation.js b/source/cli-implementation.js index e706f2ad..9f7ffe45 100755 --- a/source/cli-implementation.js +++ b/source/cli-implementation.js @@ -12,6 +12,7 @@ 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(` @@ -131,20 +132,22 @@ 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}); + }, {pkg, rootDir, isYarnBerry}); if (!options.confirm) { gracefulExit(); } console.log(); // Prints a newline for readability - const newPkg = await np(options.version, options, {pkg, rootDir}); + const newPkg = await np(options.version, options, {pkg, rootDir, isYarnBerry}); if (options.preview || options.releaseDraftOnly) { gracefulExit(); diff --git a/source/index.js b/source/index.js index 93a9407d..94ed1550 100644 --- a/source/index.js +++ b/source/index.js @@ -18,15 +18,15 @@ import * as util from './util.js'; import * as git from './git-util.js'; import * as npm from './npm/util.js'; -const exec = (cmd, args) => { +const exec = (cmd, args, options) => { // Use `Observable` support if merged https://github.com/sindresorhus/execa/pull/26 - const cp = execa(cmd, args); + const cp = execa(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}) => { +const np = async (input = 'patch', options, {pkg, rootDir, isYarnBerry}) => { if (!hasYarn() && options.yarn) { throw new Error('Could not use Yarn without yarn.lock file'); } @@ -36,10 +36,22 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => { 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 = 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 isOnGitHub = options.repoUrl && hostedGitInfo.fromUrl(options.repoUrl)?.type === 'github'; const testScript = options.testScript || 'test'; @@ -88,6 +100,13 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => { 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'}}; + const tasks = new Listr([ { title: 'Prerequisite check', @@ -105,10 +124,11 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => { task: () => deleteAsync('node_modules'), }, { - title: 'Installing dependencies using Yarn', + title: `Installing dependencies using ${pkgManagerName}`, enabled: () => options.yarn === true, - task: () => ( - exec('yarn', ['install', '--frozen-lockfile', '--production=false']).pipe( + 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; @@ -120,8 +140,8 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => { throw new Error('yarn.lock file is outdated. Run yarn, commit the updated lockfile and try again.'); }), - ) - ), + ); + }, }, { title: 'Installing dependencies using npm', @@ -134,14 +154,14 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => { ] : [], ...runTests ? [ { - title: 'Running tests using npm', + title: `Running tests using ${pkgManagerName}`, enabled: () => options.yarn === false, - task: () => exec('npm', testCommand), + task: () => exec('npm', testCommand, ciEnvOptions), }, { - title: 'Running tests using Yarn', + title: `Running tests using ${pkgManagerName}`, enabled: () => options.yarn === true, - task: () => exec('yarn', testCommand).pipe( + task: () => exec('yarn', testCommand, ciEnvOptions).pipe( catchError(error => { if (error.message.includes(`Command "${testScript}" not found`)) { return []; @@ -153,8 +173,8 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => { }, ] : [], { - title: 'Bumping version using Yarn', - enabled: () => options.yarn === true, + title: `Bumping version using ${pkgManagerName}`, + enabled: () => shouldUseYarnForVersioning, skip() { if (options.preview) { let previewText = `[Preview] Command not executed: yarn version --new-version ${input}`; @@ -178,7 +198,7 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => { }, { title: 'Bumping version using npm', - enabled: () => options.yarn === false, + enabled: () => shouldUseNpmForVersioning, skip() { if (options.preview) { let previewText = `[Preview] Command not executed: npm version ${input}`; @@ -205,14 +225,14 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => { title: `Publishing package using ${pkgManagerName}`, skip() { if (options.preview) { - const args = getPackagePublishArguments(options); + const args = getPackagePublishArguments(options, isYarnBerry); return `[Preview] Command not executed: ${pkgManager} ${args.join(' ')}.`; } }, task(context, task) { let hasError = false; - return publish(context, pkgManager, task, options) + return publish(context, pkgManager, isYarnBerry, task, options) .pipe( catchError(async error => { hasError = true; diff --git a/source/npm/handle-npm-error.js b/source/npm/handle-npm-error.js index 4188ea54..9ad57ba7 100644 --- a/source/npm/handle-npm-error.js +++ b/source/npm/handle-npm-error.js @@ -26,7 +26,11 @@ const handleNpmError = (error, task, message, executor) => { // Attempting to privately publish a scoped package without the correct npm plan // https://stackoverflow.com/a/44862841/10292952 - if (error.code === 402 || error.stderr.includes('npm ERR! 402 Payment Required')) { + if ( + error.code === 402 + || error.stderr.includes('npm ERR! 402 Payment Required') // Npm + || 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 76d09290..a35a2d5a 100644 --- a/source/npm/publish.js +++ b/source/npm/publish.js @@ -2,8 +2,8 @@ import {execa} from 'execa'; import {from, catchError} from 'rxjs'; import handleNpmError from './handle-npm-error.js'; -export const getPackagePublishArguments = options => { - const args = ['publish']; +export const getPackagePublishArguments = (options, isYarnBerry) => { + const args = isYarnBerry ? ['npm', 'publish'] : ['publish']; if (options.contents) { args.push(options.contents); @@ -24,14 +24,14 @@ export const getPackagePublishArguments = options => { return args; }; -const pkgPublish = (pkgManager, options) => execa(pkgManager, getPackagePublishArguments(options)); +const pkgPublish = (pkgManager, isYarnBerry, options) => execa(pkgManager, getPackagePublishArguments(options, isYarnBerry)); -const publish = (context, pkgManager, task, options) => - from(pkgPublish(pkgManager, options)).pipe( +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, {...options, otp}); + return pkgPublish(pkgManager, isYarnBerry, {...options, otp}); })), ); diff --git a/source/npm/util.js b/source/npm/util.js index 4954cbf4..aa4bccbd 100644 --- a/source/npm/util.js +++ b/source/npm/util.js @@ -146,6 +146,11 @@ export const getFilesToBePacked = async rootDir => { }; 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); diff --git a/source/ui.js b/source/ui.js index 66e7984a..f03f4f2b 100644 --- a/source/ui.js +++ b/source/ui.js @@ -120,11 +120,27 @@ const checkNewFilesAndDependencies = async (pkg, rootDir) => { }; // eslint-disable-next-line complexity -const ui = async (options, {pkg, rootDir}) => { +const ui = async (options, {pkg, rootDir, isYarnBerry}) => { const oldVersion = pkg.version; const extraBaseUrls = ['gitlab.com']; const repoUrl = pkg.repository && githubUrlFromGit(pkg.repository.url, {extraBaseUrls}); - const pkgManager = options.yarn ? 'yarn' : 'npm'; + + const pkgManager = (() => { + if (!options.yarn) { + return 'npm'; + } + + if (isYarnBerry) { + return 'yarn-berry'; + } + + return 'yarn'; + })(); + + if (isYarnBerry && npm.isExternalRegistry(pkg)) { + throw new Error('External registry is not yet supported with Yarn Berry'); + } + const registryUrl = await npm.getRegistryUrl(pkgManager, pkg); const releaseBranch = options.branch; diff --git a/source/yarn.js b/source/yarn.js new file mode 100644 index 00000000..d4da4275 --- /dev/null +++ b/source/yarn.js @@ -0,0 +1,16 @@ +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 index fdfd47ea..318804da 100644 --- a/test/npm/util/get-registry-url.js +++ b/test/npm/util/get-registry-url.js @@ -24,6 +24,16 @@ test('yarn', createFixture, [{ ); }); +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', diff --git a/test/util/yarn.js b/test/util/yarn.js new file mode 100644 index 00000000..bae4fd36 --- /dev/null +++ b/test/util/yarn.js @@ -0,0 +1,15 @@ +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); +});