Skip to content

Commit 0d9522b

Browse files
mifisindresorhus
andauthoredDec 5, 2023
Add Yarn Berry support (#723)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent 8888307 commit 0d9522b

10 files changed

+119
-30
lines changed
 

‎readme.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
### Why not
5050

5151
- Monorepos are not supported.
52-
- Yarn >= 2 and pnpm are not supported.
52+
- pnpm is not supported.
5353
- Custom registries are not supported ([but could be with your help](https://github.com/sindresorhus/np/issues/420)).
5454
- 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.
5555

‎source/cli-implementation.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as git from './git-util.js';
1212
import * as npm from './npm/util.js';
1313
import {SEMVER_INCREMENTS} from './version.js';
1414
import ui from './ui.js';
15+
import {checkIfYarnBerry} from './yarn.js';
1516
import np from './index.js';
1617

1718
const cli = meow(`
@@ -131,20 +132,22 @@ try {
131132

132133
const branch = flags.branch ?? await git.defaultBranch();
133134

135+
const isYarnBerry = flags.yarn && checkIfYarnBerry(pkg);
136+
134137
const options = await ui({
135138
...flags,
136139
runPublish,
137140
availability,
138141
version,
139142
branch,
140-
}, {pkg, rootDir});
143+
}, {pkg, rootDir, isYarnBerry});
141144

142145
if (!options.confirm) {
143146
gracefulExit();
144147
}
145148

146149
console.log(); // Prints a newline for readability
147-
const newPkg = await np(options.version, options, {pkg, rootDir});
150+
const newPkg = await np(options.version, options, {pkg, rootDir, isYarnBerry});
148151

149152
if (options.preview || options.releaseDraftOnly) {
150153
gracefulExit();

‎source/index.js

+38-18
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ import * as util from './util.js';
1818
import * as git from './git-util.js';
1919
import * as npm from './npm/util.js';
2020

21-
const exec = (cmd, args) => {
21+
const exec = (cmd, args, options) => {
2222
// Use `Observable` support if merged https://github.com/sindresorhus/execa/pull/26
23-
const cp = execa(cmd, args);
23+
const cp = execa(cmd, args, options);
2424

2525
return merge(cp.stdout, cp.stderr, cp).pipe(filter(Boolean));
2626
};
2727

2828
// eslint-disable-next-line complexity
29-
const np = async (input = 'patch', options, {pkg, rootDir}) => {
29+
const np = async (input = 'patch', options, {pkg, rootDir, isYarnBerry}) => {
3030
if (!hasYarn() && options.yarn) {
3131
throw new Error('Could not use Yarn without yarn.lock file');
3232
}
@@ -36,10 +36,22 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => {
3636
options.cleanup = false;
3737
}
3838

39+
function getPackageManagerName() {
40+
if (options.yarn === true) {
41+
if (isYarnBerry) {
42+
return 'Yarn Berry';
43+
}
44+
45+
return 'Yarn';
46+
}
47+
48+
return 'npm';
49+
}
50+
3951
const runTests = options.tests && !options.yolo;
4052
const runCleanup = options.cleanup && !options.yolo;
4153
const pkgManager = options.yarn === true ? 'yarn' : 'npm';
42-
const pkgManagerName = options.yarn === true ? 'Yarn' : 'npm';
54+
const pkgManagerName = getPackageManagerName();
4355
const hasLockFile = fs.existsSync(path.resolve(rootDir, options.yarn ? 'yarn.lock' : 'package-lock.json')) || fs.existsSync(path.resolve(rootDir, 'npm-shrinkwrap.json'));
4456
const isOnGitHub = options.repoUrl && hostedGitInfo.fromUrl(options.repoUrl)?.type === 'github';
4557
const testScript = options.testScript || 'test';
@@ -88,6 +100,13 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => {
88100

89101
const shouldEnable2FA = options['2fa'] && options.availability.isAvailable && !options.availability.isUnknown && !pkg.private && !npm.isExternalRegistry(pkg);
90102

103+
// Yarn berry doesn't support git commiting/tagging, so use npm
104+
const shouldUseYarnForVersioning = options.yarn === true && !isYarnBerry;
105+
const shouldUseNpmForVersioning = options.yarn === false || isYarnBerry;
106+
107+
// To prevent the process from hanging due to watch mode (e.g. when running `vitest`)
108+
const ciEnvOptions = {env: {CI: 'true'}};
109+
91110
const tasks = new Listr([
92111
{
93112
title: 'Prerequisite check',
@@ -105,10 +124,11 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => {
105124
task: () => deleteAsync('node_modules'),
106125
},
107126
{
108-
title: 'Installing dependencies using Yarn',
127+
title: `Installing dependencies using ${pkgManagerName}`,
109128
enabled: () => options.yarn === true,
110-
task: () => (
111-
exec('yarn', ['install', '--frozen-lockfile', '--production=false']).pipe(
129+
task() {
130+
const args = isYarnBerry ? ['install', '--immutable'] : ['install', '--frozen-lockfile', '--production=false'];
131+
return exec('yarn', args).pipe(
112132
catchError(async error => {
113133
if ((!error.stderr.startsWith('error Your lockfile needs to be updated'))) {
114134
return;
@@ -120,8 +140,8 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => {
120140

121141
throw new Error('yarn.lock file is outdated. Run yarn, commit the updated lockfile and try again.');
122142
}),
123-
)
124-
),
143+
);
144+
},
125145
},
126146
{
127147
title: 'Installing dependencies using npm',
@@ -134,14 +154,14 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => {
134154
] : [],
135155
...runTests ? [
136156
{
137-
title: 'Running tests using npm',
157+
title: `Running tests using ${pkgManagerName}`,
138158
enabled: () => options.yarn === false,
139-
task: () => exec('npm', testCommand),
159+
task: () => exec('npm', testCommand, ciEnvOptions),
140160
},
141161
{
142-
title: 'Running tests using Yarn',
162+
title: `Running tests using ${pkgManagerName}`,
143163
enabled: () => options.yarn === true,
144-
task: () => exec('yarn', testCommand).pipe(
164+
task: () => exec('yarn', testCommand, ciEnvOptions).pipe(
145165
catchError(error => {
146166
if (error.message.includes(`Command "${testScript}" not found`)) {
147167
return [];
@@ -153,8 +173,8 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => {
153173
},
154174
] : [],
155175
{
156-
title: 'Bumping version using Yarn',
157-
enabled: () => options.yarn === true,
176+
title: `Bumping version using ${pkgManagerName}`,
177+
enabled: () => shouldUseYarnForVersioning,
158178
skip() {
159179
if (options.preview) {
160180
let previewText = `[Preview] Command not executed: yarn version --new-version ${input}`;
@@ -178,7 +198,7 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => {
178198
},
179199
{
180200
title: 'Bumping version using npm',
181-
enabled: () => options.yarn === false,
201+
enabled: () => shouldUseNpmForVersioning,
182202
skip() {
183203
if (options.preview) {
184204
let previewText = `[Preview] Command not executed: npm version ${input}`;
@@ -205,14 +225,14 @@ const np = async (input = 'patch', options, {pkg, rootDir}) => {
205225
title: `Publishing package using ${pkgManagerName}`,
206226
skip() {
207227
if (options.preview) {
208-
const args = getPackagePublishArguments(options);
228+
const args = getPackagePublishArguments(options, isYarnBerry);
209229
return `[Preview] Command not executed: ${pkgManager} ${args.join(' ')}.`;
210230
}
211231
},
212232
task(context, task) {
213233
let hasError = false;
214234

215-
return publish(context, pkgManager, task, options)
235+
return publish(context, pkgManager, isYarnBerry, task, options)
216236
.pipe(
217237
catchError(async error => {
218238
hasError = true;

‎source/npm/handle-npm-error.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ const handleNpmError = (error, task, message, executor) => {
2626

2727
// Attempting to privately publish a scoped package without the correct npm plan
2828
// https://stackoverflow.com/a/44862841/10292952
29-
if (error.code === 402 || error.stderr.includes('npm ERR! 402 Payment Required')) {
29+
if (
30+
error.code === 402
31+
|| error.stderr.includes('npm ERR! 402 Payment Required') // Npm
32+
|| error.stdout.includes('Response Code: 402 (Payment Required)') // Yarn Berry
33+
) {
3034
throw new Error('You cannot publish a scoped package privately without a paid plan. Did you mean to publish publicly?');
3135
}
3236

‎source/npm/publish.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import {execa} from 'execa';
22
import {from, catchError} from 'rxjs';
33
import handleNpmError from './handle-npm-error.js';
44

5-
export const getPackagePublishArguments = options => {
6-
const args = ['publish'];
5+
export const getPackagePublishArguments = (options, isYarnBerry) => {
6+
const args = isYarnBerry ? ['npm', 'publish'] : ['publish'];
77

88
if (options.contents) {
99
args.push(options.contents);
@@ -24,14 +24,14 @@ export const getPackagePublishArguments = options => {
2424
return args;
2525
};
2626

27-
const pkgPublish = (pkgManager, options) => execa(pkgManager, getPackagePublishArguments(options));
27+
const pkgPublish = (pkgManager, isYarnBerry, options) => execa(pkgManager, getPackagePublishArguments(options, isYarnBerry));
2828

29-
const publish = (context, pkgManager, task, options) =>
30-
from(pkgPublish(pkgManager, options)).pipe(
29+
const publish = (context, pkgManager, isYarnBerry, task, options) =>
30+
from(pkgPublish(pkgManager, isYarnBerry, options)).pipe(
3131
catchError(error => handleNpmError(error, task, otp => {
3232
context.otp = otp;
3333

34-
return pkgPublish(pkgManager, {...options, otp});
34+
return pkgPublish(pkgManager, isYarnBerry, {...options, otp});
3535
})),
3636
);
3737

‎source/npm/util.js

+5
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ export const getFilesToBePacked = async rootDir => {
146146
};
147147

148148
export const getRegistryUrl = async (pkgManager, pkg) => {
149+
if (pkgManager === 'yarn-berry') {
150+
const {stdout} = await execa('yarn', ['config', 'get', 'npmRegistryServer']);
151+
return stdout;
152+
}
153+
149154
const args = ['config', 'get', 'registry'];
150155
if (isExternalRegistry(pkg)) {
151156
args.push('--registry', pkg.publishConfig.registry);

‎source/ui.js

+18-2
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,27 @@ const checkNewFilesAndDependencies = async (pkg, rootDir) => {
120120
};
121121

122122
// eslint-disable-next-line complexity
123-
const ui = async (options, {pkg, rootDir}) => {
123+
const ui = async (options, {pkg, rootDir, isYarnBerry}) => {
124124
const oldVersion = pkg.version;
125125
const extraBaseUrls = ['gitlab.com'];
126126
const repoUrl = pkg.repository && githubUrlFromGit(pkg.repository.url, {extraBaseUrls});
127-
const pkgManager = options.yarn ? 'yarn' : 'npm';
127+
128+
const pkgManager = (() => {
129+
if (!options.yarn) {
130+
return 'npm';
131+
}
132+
133+
if (isYarnBerry) {
134+
return 'yarn-berry';
135+
}
136+
137+
return 'yarn';
138+
})();
139+
140+
if (isYarnBerry && npm.isExternalRegistry(pkg)) {
141+
throw new Error('External registry is not yet supported with Yarn Berry');
142+
}
143+
128144
const registryUrl = await npm.getRegistryUrl(pkgManager, pkg);
129145
const releaseBranch = options.branch;
130146

‎source/yarn.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import semver from 'semver';
2+
3+
export function checkIfYarnBerry(pkg) {
4+
if (typeof pkg.packageManager !== 'string') {
5+
return false;
6+
}
7+
8+
const match = pkg.packageManager.match(/^yarn@(.+)$/);
9+
if (!match) {
10+
return false;
11+
}
12+
13+
const [, yarnVersion] = match;
14+
const versionParsed = semver.parse(yarnVersion);
15+
return (versionParsed.major >= 2);
16+
}

‎test/npm/util/get-registry-url.js

+10
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ test('yarn', createFixture, [{
2424
);
2525
});
2626

27+
test('yarn-berry', createFixture, [{
28+
command: 'yarn config get npmRegistryServer',
29+
stdout: 'https://registry.yarnpkg.com',
30+
}], async ({t, testedModule: npm}) => {
31+
t.is(
32+
await npm.getRegistryUrl('yarn-berry', {}),
33+
'https://registry.yarnpkg.com',
34+
);
35+
});
36+
2737
test('external', createFixture, [{
2838
command: 'npm config get registry --registry http://my-internal-registry.local',
2939
stdout: 'http://my-internal-registry.local',

‎test/util/yarn.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import test from 'ava';
2+
import {checkIfYarnBerry} from '../../source/yarn.js';
3+
4+
test('checkIfYarnBerry', t => {
5+
t.is(checkIfYarnBerry({}), false);
6+
t.is(checkIfYarnBerry({
7+
packageManager: 'npm',
8+
}), false);
9+
t.is(checkIfYarnBerry({
10+
packageManager: 'yarn@1.0.0',
11+
}), false);
12+
t.is(checkIfYarnBerry({
13+
packageManager: 'yarn@2.0.0',
14+
}), true);
15+
});

0 commit comments

Comments
 (0)
Please sign in to comment.