Files
nrwl--nx/packages/devkit/src/utils/catalog/yarn-manager.ts
T
2026-07-13 12:38:36 +08:00

225 lines
6.6 KiB
TypeScript

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);
}
}