Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Yarn berry support #723

Merged
merged 21 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
mifi marked this conversation as resolved.
Show resolved Hide resolved
|| 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);
});