Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
398 changes: 398 additions & 0 deletions .pnp.cjs

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions .yarn/versions/ae3cd5fb.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
releases:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be updated after other discussions are resolved.

"@yarnpkg/cli": minor
"@yarnpkg/plugin-catalog": major
"@yarnpkg/plugin-pack": patch

declined:
- "@yarnpkg/plugin-compat"
- "@yarnpkg/plugin-constraints"
- "@yarnpkg/plugin-dlx"
- "@yarnpkg/plugin-essentials"
- "@yarnpkg/plugin-init"
- "@yarnpkg/plugin-interactive-tools"
- "@yarnpkg/plugin-nm"
- "@yarnpkg/plugin-npm"
- "@yarnpkg/plugin-npm-cli"
- "@yarnpkg/plugin-patch"
- "@yarnpkg/plugin-pnp"
- "@yarnpkg/plugin-pnpm"
- "@yarnpkg/plugin-stage"
- "@yarnpkg/plugin-typescript"
- "@yarnpkg/plugin-version"
- "@yarnpkg/plugin-workspace-tools"
- "@yarnpkg/builder"
- "@yarnpkg/core"
- "@yarnpkg/doctor"
89 changes: 89 additions & 0 deletions packages/docusaurus/docs/features/catalogs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
category: features
slug: /features/catalogs
title: "Catalogs"
description: Centralize dependency version management across your workspace with reusable version catalogs.
---

## Overview

Catalogs provide centralized dependency version management for workspaces. Inspired by [pnpm's catalog functionality](https://pnpm.io/catalogs), they allow you to define version ranges in your `.yarnrc.yml` and reference them across multiple `package.json` files using the `catalog:` protocol.

This eliminates version duplication, ensures consistency across packages, and makes dependency upgrades much simpler — update one place instead of many files.

:::info
The catalog plugin is included by default starting from Yarn 4.10.0.
:::

### Basic usage

Define a catalog in your `.yarnrc.yml`:

```yaml
catalog:
react: ^18.3.1
lodash: ^4.17.21
```

Reference entries in your `package.json`:

```json
{
"dependencies": {
"react": "catalog:",
"lodash": "catalog:"
}
}
```

## Named catalogs

You can define multiple named catalogs using the `catalogs` field for different purposes:

```yaml
catalog:
lodash: ^4.17.21

catalogs:
react18:
react: ^18.3.1
react-dom: ^18.3.1
react17:
react: ^17.0.2
react-dom: ^17.0.2
```

Reference named catalogs by specifying the name:

```json
{
"dependencies": {
"lodash": "catalog:",
"react": "catalog:react18"
}
}
```

## Publishing

When publishing packages, the `catalog:` protocol is automatically replaced with actual version ranges, ensuring compatibility with other package managers:

```json
// Source package.json
{
"dependencies": {
"react": "catalog:react18"
}
}

// Published package.json
{
"dependencies": {
"react": "^18.3.1"
}
}
```

## Supported fields

The `catalog:` protocol works in `dependencies`, `devDependencies`, `peerDependencies`.
73 changes: 73 additions & 0 deletions packages/plugin-catalog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# `@yarnpkg/plugin-catalog`

This plugin adds support for centralized dependency version management through catalogs, similar to pnpm's catalog feature.

It hooks into:
- `reduceDependency` and replaces catalog ranges with the ones defined in a catalog.
- `beforeWorkspacePacking` replacing catalogs with actual ranges before packing

## Install

This plugin is included by default starting from Yarn 4.10.0.

## Usage

### Default Catalog

Define a catalog in your `.yarnrc.yml`:

```yaml
catalog:
react: ^18.0.0
lodash: ^4.17.21
```

Then reference catalog entries in your `package.json`:

```json
{
"dependencies": {
"react": "catalog:",
"lodash": "catalog:"
}
}
```

### Named Catalogs

You can define multiple named catalogs for different purposes:

```yaml
# Default catalog
catalog:
lodash: ^4.17.21
typescript: ~4.9.0

# Named catalogs
catalogs:
react18:
react: ^18.3.1
react-dom: ^18.3.1

react17:
react: ^17.0.2
react-dom: ^17.0.2

vue3:
vue: ^3.4.0
vuex: ^4.1.0
```

Then reference them in your `package.json`:

```json
{
"dependencies": {
"lodash": "catalog:",
"react": "catalog:react18",
"vue": "catalog:vue3"
}
}
```

The comprehensive feature documentation can be found in `packages/docusaurus/docs/features/catalog.mdx`.
45 changes: 45 additions & 0 deletions packages/plugin-catalog/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@yarnpkg/plugin-catalog",
"version": "0.0.1",
"license": "BSD-2-Clause",
"main": "./sources/index.ts",
"exports": {
".": "./sources/index.ts",
"./package.json": "./package.json"
},
"dependencies": {
"@yarnpkg/fslib": "workspace:^",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@yarnpkg/core": "workspace:^",
"@yarnpkg/plugin-pack": "workspace:^"
},
"devDependencies": {
"@yarnpkg/core": "workspace:^",
"@yarnpkg/plugin-pack": "workspace:^"
},
"repository": {
"type": "git",
"url": "git+https://github.com/yarnpkg/berry.git",
"directory": "packages/plugin-catalog"
},
"scripts": {
"postpack": "rm -rf lib",
"prepack": "run build:compile \"$(pwd)\""
},
"publishConfig": {
"main": "./lib/index.js",
"exports": {
".": "./lib/index.js",
"./package.json": "./package.json"
}
},
"files": [
"/lib/**/*"
],
"engines": {
"node": ">=18.12.0"
},
"stableVersion": "0.0.1"
}
1 change: 1 addition & 0 deletions packages/plugin-catalog/sources/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const CATALOG_DESCRIPTOR_PREFIX = `catalog:`;
107 changes: 107 additions & 0 deletions packages/plugin-catalog/sources/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {type Descriptor, type Locator, type Plugin, type Project, type Resolver, type ResolveOptions, type Workspace, SettingsType, structUtils} from '@yarnpkg/core';
import {Hooks as CoreHooks} from '@yarnpkg/core';
import {DEPENDENCY_TYPES, Hooks as PackHooks} from '@yarnpkg/plugin-pack';

import {isCatalogReference, resolveDescriptorFromCatalog} from './utils';

declare module '@yarnpkg/core' {
interface ConfigurationValueMap {
catalog: Map<string, string>;
catalogs: Map<string, Map<string, string>>;
}
}


const plugin: Plugin<CoreHooks & PackHooks> = {
configuration: {
/**
* Example:
* ```yaml
* catalog:
* react: ^18.3.1
* lodash: ^4.17.21
* ```
*/
catalog: {
description: `The default catalog of packages`,
type: SettingsType.MAP,
valueDefinition: {
description: `The catalog of packages`,
type: SettingsType.STRING,
},
},
/**
* Example:
* ```yaml
* catalogs:
* react18:
* react: ^18.3.1
* react-dom: ^18.3.1
* react17:
* react: ^17.0.2
* react-dom: ^17.0.2
* ```
*/
catalogs: {
description: `Named catalogs of packages`,
type: SettingsType.MAP,
valueDefinition: {
description: `A named catalog`,
type: SettingsType.MAP,
valueDefinition: {
description: `Package version in the catalog`,
type: SettingsType.STRING,
},
},
},
},
hooks: {
/**
* To allow publishing packages with catalog references, we need to replace the
* catalog references with the actual version ranges during the packing phase.
*/
beforeWorkspacePacking: (workspace: Workspace, rawManifest: any) => {
const project = workspace.project;

for (const dependencyType of DEPENDENCY_TYPES) {
const dependencies = rawManifest[dependencyType];
if (!dependencies) continue;

for (const [identStr, range] of Object.entries(dependencies)) {
if (typeof range !== `string` || !isCatalogReference(range)) continue;

try {
// Create a descriptor to resolve from catalog
const ident = structUtils.parseIdent(identStr);
const descriptor = structUtils.makeDescriptor(ident, range);

// Resolve the catalog reference to get the actual version range
const resolvedDescriptor = resolveDescriptorFromCatalog(project, descriptor);

// Replace the catalog reference with the resolved range
dependencies[identStr] = resolvedDescriptor.range;
} catch {
// If resolution fails, leave the catalog reference as-is
// This will allow the error to be caught during normal resolution
continue;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error will be "unsupported protocol catalog:" which might be surprising, right? Could we make resolveDescriptorFromCatalog return a nullable value, and throw a UsageError when it returns that (I prefer to avoid blanket try statements as they often hide unrelated exceptions)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, my mistake here. I am now letting the error bubble up, but it isn't being properly catched by the StreamReport.

image

...

After some digging, it seems this process.nextTick() throws the error outside of the StreamReport scope. Removing it doesn't break any of the tests, but makes things slightly slower. Do you have recollections on why it was introduced originally?

image

https://github.com/yarnpkg/berry/pull/125/files#diff-0f0e8cfb83ca10fffa5acf788887422f5f26377c114ea0862219357c4806fdc0L16

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately no (I suspect it has something to do with Node.js streams and how they drain), but we can remove it and just add a simple await new Promise(process.nextTick) call right after starting the pack.

}
}
}
},

/**
* On this hook, we will check if the dependency is a catalog reference, and if so,
* we will replace the range with the actual range defined in the catalog.
*/
reduceDependency: async (dependency: Descriptor, project: Project, locator: Locator, initialDependency: Descriptor, {resolver, resolveOptions}: {resolver: Resolver, resolveOptions: ResolveOptions}) => {
if (isCatalogReference(dependency.range)) {
const resolvedDescriptor = resolveDescriptorFromCatalog(project, dependency);
return resolvedDescriptor;
}
return dependency;
},
},
};

// eslint-disable-next-line arca/no-default-export
export default plugin;
Loading
Loading