import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { type Tree } from 'nx/src/devkit-exports'; import { formatCatalogError, type CatalogManager } from './manager'; import { readCatalogConfigFromFs, readCatalogConfigFromTree, updateCatalogVersionsInFile, } from './manager-utils'; import type { CatalogDefinitions, CatalogEntry, CatalogReference, } from './types'; const YARNRC_FILENAME = '.yarnrc.yml'; /** * Yarn Berry (v4+) catalog manager implementation */ export class YarnCatalogManager implements CatalogManager { readonly name = 'yarn'; readonly catalogProtocol = 'catalog:'; isCatalogReference(version: string): boolean { return version.startsWith(this.catalogProtocol); } parseCatalogReference(version: string): CatalogReference | null { if (!this.isCatalogReference(version)) { return null; } const catalogName = version.substring(this.catalogProtocol.length); // Normalize both "catalog:" and "catalog:default" to the same representation const isDefault = !catalogName || catalogName === 'default'; return { catalogName: isDefault ? undefined : catalogName, isDefaultCatalog: isDefault, }; } getCatalogDefinitionFilePaths(): string[] { return [YARNRC_FILENAME]; } getCatalogDefinitions(treeOrRoot: Tree | string): CatalogDefinitions | null { if (typeof treeOrRoot === 'string') { const configPath = join(treeOrRoot, YARNRC_FILENAME); if (!existsSync(configPath)) { return null; } return readCatalogConfigFromFs(YARNRC_FILENAME, configPath); } else { if (!treeOrRoot.exists(YARNRC_FILENAME)) { return null; } return readCatalogConfigFromTree(YARNRC_FILENAME, treeOrRoot); } } resolveCatalogReference( treeOrRoot: Tree | string, packageName: string, version: string ): string | null { const catalogRef = this.parseCatalogReference(version); if (!catalogRef) { return null; } const catalogDefs = this.getCatalogDefinitions(treeOrRoot); if (!catalogDefs) { return null; } let catalogToUse: CatalogEntry | undefined; if (catalogRef.isDefaultCatalog) { // Check both locations for default catalog catalogToUse = catalogDefs.catalog ?? catalogDefs.catalogs?.default; } else if (catalogRef.catalogName) { catalogToUse = catalogDefs.catalogs?.[catalogRef.catalogName]; } return catalogToUse?.[packageName] || null; } validateCatalogReference( treeOrRoot: Tree | string, packageName: string, version: string ): void { const catalogRef = this.parseCatalogReference(version); if (!catalogRef) { throw new Error( `Invalid catalog reference syntax: "${version}". Expected format: "catalog:" or "catalog:name"` ); } const catalogDefs = this.getCatalogDefinitions(treeOrRoot); if (!catalogDefs) { throw new Error( formatCatalogError( `Cannot get Yarn catalog definitions. No ${YARNRC_FILENAME} found in workspace root.`, [`Create a ${YARNRC_FILENAME} file in your workspace root`] ) ); } let catalogToUse: CatalogEntry | undefined; if (catalogRef.isDefaultCatalog) { const hasCatalog = !!catalogDefs.catalog; const hasCatalogsDefault = !!catalogDefs.catalogs?.default; // Error if both defined if (hasCatalog && hasCatalogsDefault) { throw new Error( "The 'default' catalog was defined multiple times. Use the 'catalog' field or 'catalogs.default', but not both." ); } catalogToUse = catalogDefs.catalog ?? catalogDefs.catalogs?.default; if (!catalogToUse) { const availableCatalogs = Object.keys(catalogDefs.catalogs || {}); const suggestions = [ `Define a default catalog in ${YARNRC_FILENAME} under the "catalog" key`, ]; if (availableCatalogs.length > 0) { suggestions.push( `Or select from the available named catalogs: ${availableCatalogs .map((c) => `"catalog:${c}"`) .join(', ')}` ); } throw new Error( formatCatalogError( `No default catalog defined in ${YARNRC_FILENAME}`, suggestions ) ); } } else if (catalogRef.catalogName) { catalogToUse = catalogDefs.catalogs?.[catalogRef.catalogName]; if (!catalogToUse) { const availableCatalogs = Object.keys( catalogDefs.catalogs || {} ).filter((c) => c !== 'default'); const defaultCatalog = !!catalogDefs.catalog ? 'catalog' : !catalogDefs.catalogs?.default ? 'catalogs.default' : null; const suggestions = [ `Define the catalog in ${YARNRC_FILENAME} under the "catalogs" key`, ]; if (availableCatalogs.length > 0) { suggestions.push( `Or select from the available named catalogs: ${availableCatalogs .map((c) => `"catalog:${c}"`) .join(', ')}` ); } if (defaultCatalog) { suggestions.push(`Or use the default catalog ("${defaultCatalog}")`); } throw new Error( formatCatalogError( `Catalog "${catalogRef.catalogName}" not found in ${YARNRC_FILENAME}`, suggestions ) ); } } if (!catalogToUse![packageName]) { let catalogName: string; if (catalogRef.isDefaultCatalog) { // Context-aware messaging based on which location exists const hasCatalog = !!catalogDefs.catalog; catalogName = hasCatalog ? 'default catalog ("catalog")' : 'default catalog ("catalogs.default")'; } else { catalogName = `catalog '${catalogRef.catalogName}'`; } const availablePackages = Object.keys(catalogToUse!); const suggestions = [ `Add "${packageName}" to ${catalogName} in ${YARNRC_FILENAME}`, ]; if (availablePackages.length > 0) { suggestions.push( `Or select from the available packages in ${catalogName}: ${availablePackages .map((p) => `"${p}"`) .join(', ')}` ); } throw new Error( formatCatalogError( `Package "${packageName}" not found in ${catalogName}`, suggestions ) ); } } updateCatalogVersions( treeOrRoot: Tree | string, updates: Array<{ packageName: string; version: string; catalogName?: string; }> ): void { updateCatalogVersionsInFile(YARNRC_FILENAME, treeOrRoot, updates); } }