-
Notifications
You must be signed in to change notification settings - Fork 119
feat: support pnpm catalogs #1397
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
Changes from all commits
85cb47c
059bc38
e7013e5
f902841
adcefef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,152 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| import type { PackageJson } from 'pkg-types' | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| import { existsSync, readFileSync, writeFileSync } from 'node:fs' | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| import { dirname, join, resolve } from 'pathe' | ||||||||||||||||||||||||||||||||||||||||||||
| import { parsePnpmWorkspaceYaml } from 'pnpm-workspace-yaml' | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const CATALOG_SPECIFIER_RE = /^catalog:(.*)$/ | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const DEFAULT_CATALOG = 'default' | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export interface CatalogEntry { | ||||||||||||||||||||||||||||||||||||||||||||
| /** The catalog the specifier points at. `default` for a bare `catalog:`. */ | ||||||||||||||||||||||||||||||||||||||||||||
| catalog: string | ||||||||||||||||||||||||||||||||||||||||||||
| /** The specifier the catalog resolves to, e.g. `^4.2.0`. Absent when the catalog has no such entry. */ | ||||||||||||||||||||||||||||||||||||||||||||
| specifier?: string | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export interface CatalogConfig { | ||||||||||||||||||||||||||||||||||||||||||||
| /** Absolute path of the `pnpm-workspace.yaml` the catalogs are declared in. */ | ||||||||||||||||||||||||||||||||||||||||||||
| filePath: string | ||||||||||||||||||||||||||||||||||||||||||||
| catalogs: Record<string, Record<string, string>> | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||
| * The catalog a `catalog:` / `catalog:name` specifier refers to, or `undefined` | ||||||||||||||||||||||||||||||||||||||||||||
| * for any other specifier. | ||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||
| export function parseCatalogSpecifier(specifier: string | undefined): string | undefined { | ||||||||||||||||||||||||||||||||||||||||||||
| const match = specifier?.match(CATALOG_SPECIFIER_RE) | ||||||||||||||||||||||||||||||||||||||||||||
| if (!match) { | ||||||||||||||||||||||||||||||||||||||||||||
| return undefined | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| return match[1] || DEFAULT_CATALOG | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** Nearest `pnpm-workspace.yaml` at or above `cwd`. */ | ||||||||||||||||||||||||||||||||||||||||||||
| export function findPnpmWorkspaceYaml(cwd: string): string | undefined { | ||||||||||||||||||||||||||||||||||||||||||||
| for (let dir = resolve(cwd); ; dir = dirname(dir)) { | ||||||||||||||||||||||||||||||||||||||||||||
| const filePath = join(dir, 'pnpm-workspace.yaml') | ||||||||||||||||||||||||||||||||||||||||||||
| if (existsSync(filePath)) { | ||||||||||||||||||||||||||||||||||||||||||||
| return filePath | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| if (dir === dirname(dir)) { | ||||||||||||||||||||||||||||||||||||||||||||
| return undefined | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const configCache = new Map<string, CatalogConfig | undefined>() | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** Discard memoised catalog configuration, so a later read sees changes on disk. */ | ||||||||||||||||||||||||||||||||||||||||||||
| export function clearCatalogCache(): void { | ||||||||||||||||||||||||||||||||||||||||||||
| configCache.clear() | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||
| * Catalogs declared in the nearest `pnpm-workspace.yaml`, keyed by catalog name. | ||||||||||||||||||||||||||||||||||||||||||||
| * The top-level `catalog` key is exposed as {@link DEFAULT_CATALOG}. | ||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||
| * Results are memoised per workspace file, as commands such as `nuxi info` query | ||||||||||||||||||||||||||||||||||||||||||||
| * dozens of dependencies in a row. | ||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||
| export function readCatalogConfig(cwd: string): CatalogConfig | undefined { | ||||||||||||||||||||||||||||||||||||||||||||
| const filePath = findPnpmWorkspaceYaml(cwd) | ||||||||||||||||||||||||||||||||||||||||||||
| if (!filePath) { | ||||||||||||||||||||||||||||||||||||||||||||
| return undefined | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if (configCache.has(filePath)) { | ||||||||||||||||||||||||||||||||||||||||||||
| return configCache.get(filePath) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const config = parseCatalogConfig(filePath) | ||||||||||||||||||||||||||||||||||||||||||||
| configCache.set(filePath, config) | ||||||||||||||||||||||||||||||||||||||||||||
| return config | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| function parseCatalogConfig(filePath: string): CatalogConfig | undefined { | ||||||||||||||||||||||||||||||||||||||||||||
| let workspace: ReturnType<typeof parsePnpmWorkspaceYaml> | ||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||
| workspace = parsePnpmWorkspaceYaml(readFileSync(filePath, 'utf-8')) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| catch { | ||||||||||||||||||||||||||||||||||||||||||||
| return undefined | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const json = workspace.toJSON() | ||||||||||||||||||||||||||||||||||||||||||||
| const catalogs: CatalogConfig['catalogs'] = { ...json.catalogs } | ||||||||||||||||||||||||||||||||||||||||||||
| if (json.catalog) { | ||||||||||||||||||||||||||||||||||||||||||||
| catalogs[DEFAULT_CATALOG] = json.catalog | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if (Object.keys(catalogs).length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||
| return undefined | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| return { filePath, catalogs } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||
| * Resolve the specifier `pkg` is declared with in `pkgJson` through the project's | ||||||||||||||||||||||||||||||||||||||||||||
| * catalogs. Returns `undefined` when the dependency is absent or not | ||||||||||||||||||||||||||||||||||||||||||||
| * catalog-managed, so a caller can fall back to the declared specifier. | ||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||
| export function resolveCatalogEntry(cwd: string, pkgJson: PackageJson | null | undefined, pkg: string): CatalogEntry | undefined { | ||||||||||||||||||||||||||||||||||||||||||||
| const specifier = pkgJson?.dependencies?.[pkg] || pkgJson?.devDependencies?.[pkg] | ||||||||||||||||||||||||||||||||||||||||||||
| const catalog = parseCatalogSpecifier(specifier) | ||||||||||||||||||||||||||||||||||||||||||||
| if (!catalog) { | ||||||||||||||||||||||||||||||||||||||||||||
| return undefined | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const config = readCatalogConfig(cwd) | ||||||||||||||||||||||||||||||||||||||||||||
| return { catalog, specifier: config?.catalogs[catalog]?.[pkg] } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+106
to
+114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -n -C3 --glob '*.ts' \
'resolveCatalogEntry\(|optionalDependencies|peerDependencies' \
packages/nuxt-cli/src packages/nuxt-cli/testRepository: nuxt/cli Length of output: 14300 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '--- packages/nuxt-cli/src/utils/catalog.ts outline ---\n'
ast-grep outline packages/nuxt-cli/src/utils/catalog.ts --view expanded || true
printf '\n--- packages/nuxt-cli/src/utils/catalog.ts relevant lines ---\n'
sed -n '1,150p' packages/nuxt-cli/src/utils/catalog.ts | cat -n
printf '\n--- packages/nuxt-cli/src/utils/versions.ts ---\n'
sed -n '1,60p' packages/nuxt-cli/src/utils/versions.ts | cat -n
printf '\n--- pnpm catalog references in package manifests ---\n'
rg -n '"catalog:("' -g 'package.json' .Repository: nuxt/cli Length of output: 237 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- packages/nuxt-cli/src/utils/catalog.ts outline ---'
ast-grep outline packages/nuxt-cli/src/utils/catalog.ts --view expanded || true
printf '%s\n' ''
printf '%s\n' '--- packages/nuxt-cli/src/utils/catalog.ts relevant lines ---'
sed -n '1,150p' packages/nuxt-cli/src/utils/catalog.ts | cat -n
printf '%s\n' ''
printf '%s\n' '--- packages/nuxt-cli/src/utils/versions.ts ---'
sed -n '1,60p' packages/nuxt-cli/src/utils/versions.ts | cat -n
printf '%s\n' ''
printf '%s\n' '--- pnpm catalog references in package manifests ---'
rg -n '"catalog:("' -g 'package.json' . || trueRepository: nuxt/cli Length of output: 10135 🌐 Web query:
💡 Result: In pnpm, catalogs are a workspace feature that allows you to define dependency version ranges centrally in your pnpm-workspace.yaml file and reference them across your workspace packages [1][2]. You can use the catalog: protocol to reference these versions in the following dependency sections within any package.json file in your workspace: - dependencies [3] - devDependencies [3] - peerDependencies [3][4] - optionalDependencies [3] Additionally, the catalog: protocol can be used within the overrides field inside your pnpm-workspace.yaml file [5][3]. How it works: 1. Define your catalogs in pnpm-workspace.yaml using either the singular catalog field (for the default catalog) or the plural catalogs field (for named catalogs) [1][2]. 2. Reference the catalog version in a package.json file using the catalog: protocol [1][3]. - For the default catalog, you can use the shorthand catalog: [1][6]. - For named catalogs, use the syntax catalog:<catalog_name> (e.g., catalog:react18) [1][6]. Example: In pnpm-workspace.yaml: catalog: react: ^18.3.1 catalogs: testing: vitest: ^2.0.0 In a package.json: { "dependencies": { "react": "catalog:" }, "devDependencies": { "vitest": "catalog:testing" } } You can also add dependencies to catalogs directly via the CLI using pnpm add --save-catalog or pnpm add --save-catalog-name <catalog_name> [7]. Citations:
Resolve catalog references from all dependency sections.
Proposed fix- const specifier = pkgJson?.dependencies?.[pkg] || pkgJson?.devDependencies?.[pkg]
+ const specifier = pkgJson?.dependencies?.[pkg]
+ ?? pkgJson?.devDependencies?.[pkg]
+ ?? pkgJson?.optionalDependencies?.[pkg]
+ ?? pkgJson?.peerDependencies?.[pkg]📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** The outcome of a {@link updateCatalogEntries} call. */ | ||||||||||||||||||||||||||||||||||||||||||||
| export type UpdateCatalogEntriesResult = 'updated' | 'unchanged' | 'failed' | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export interface CatalogEntryUpdate { | ||||||||||||||||||||||||||||||||||||||||||||
| catalog: string | ||||||||||||||||||||||||||||||||||||||||||||
| pkg: string | ||||||||||||||||||||||||||||||||||||||||||||
| specifier: string | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||
| * Point catalog entries at new specifiers in a single read/write of | ||||||||||||||||||||||||||||||||||||||||||||
| * `pnpm-workspace.yaml`, preserving its comments, anchors and aliases. | ||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||
| export function updateCatalogEntries(cwd: string, updates: CatalogEntryUpdate[]): UpdateCatalogEntriesResult { | ||||||||||||||||||||||||||||||||||||||||||||
| const filePath = findPnpmWorkspaceYaml(cwd) | ||||||||||||||||||||||||||||||||||||||||||||
| if (!filePath) { | ||||||||||||||||||||||||||||||||||||||||||||
| return 'failed' | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||
| const workspace = parsePnpmWorkspaceYaml(readFileSync(filePath, 'utf-8')) | ||||||||||||||||||||||||||||||||||||||||||||
| for (const { catalog, pkg, specifier } of updates) { | ||||||||||||||||||||||||||||||||||||||||||||
| workspace.setPackage(catalog, pkg, specifier) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| if (!workspace.hasChanged()) { | ||||||||||||||||||||||||||||||||||||||||||||
| return 'unchanged' | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| writeFileSync(filePath, workspace.toString(), 'utf-8') | ||||||||||||||||||||||||||||||||||||||||||||
| configCache.delete(filePath) | ||||||||||||||||||||||||||||||||||||||||||||
| return 'updated' | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| catch { | ||||||||||||||||||||||||||||||||||||||||||||
| return 'failed' | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.