Skip to content

Commit

Permalink
Add Yarn Berry support (#723)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
mifi and sindresorhus authored Dec 5, 2023
1 parent 8888307 commit 0d9522b
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 30 deletions.
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
7 changes: 5 additions & 2 deletions source/cli-implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
Expand Down Expand Up @@ -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();
Expand Down
56 changes: 38 additions & 18 deletions source/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand All @@ -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';
Expand Down Expand Up @@ -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',
Expand All @@ -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;
Expand All @@ -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',
Expand All @@ -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 [];
Expand All @@ -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}`;
Expand All @@ -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}`;
Expand All @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion source/npm/handle-npm-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -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?');
}

Expand Down
12 changes: 6 additions & 6 deletions source/npm/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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});
})),
);

Expand Down
5 changes: 5 additions & 0 deletions source/npm/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
20 changes: 18 additions & 2 deletions source/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
16 changes: 16 additions & 0 deletions source/yarn.js
Original file line number Diff line number Diff line change
@@ -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);
}
10 changes: 10 additions & 0 deletions test/npm/util/get-registry-url.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
15 changes: 15 additions & 0 deletions test/util/yarn.js
Original file line number Diff line number Diff line change
@@ -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: '[email protected]',
}), false);
t.is(checkIfYarnBerry({
packageManager: '[email protected]',
}), true);
});

0 comments on commit 0d9522b

Please sign in to comment.