/** * Catalog Resolver * * Provides utilities for loading and resolving Bun workspace catalog references. * Handles both default catalog (catalog:) and named catalogs (catalog:testing). * * @example * ```typescript * const resolver = await CatalogResolver.create(); * const version = resolver.resolve('react', 'catalog:react'); * // Returns: '19.0.0' * ``` */ import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { CONFIG } from "./config.js"; import { discoverPackageDirs } from "./discover-packages.js"; export type ExportsField = | string | { [key: string]: ExportsField } | ExportsField[]; export interface PackageJson { name?: string; version?: string; private?: boolean; main?: string; module?: string; types?: string; files?: string[]; exports?: ExportsField; scripts?: Record; dependencies?: Record; devDependencies?: Record; peerDependencies?: Record; workspaces?: { catalog?: Record; catalogs?: Record>; }; [extra: string]: unknown; } export interface CatalogEntry { catalogName: string; version: string; } /** * Resolves catalog references to actual versions */ export class CatalogResolver { private catalogs: Map> = new Map(); private workspaceVersions: Map = new Map(); private constructor() {} /** * Create a CatalogResolver by loading catalogs from root package.json * and discovering workspace package versions. * * @returns Initialized CatalogResolver instance * @throws Error if root package.json cannot be read */ static async create(): Promise { const resolver = new CatalogResolver(); await resolver.loadCatalogs(); await resolver.loadWorkspaceVersions(); return resolver; } /** * Discover all @suaveplan/* workspace packages and their current versions. * Walks `packages///` via `discoverPackageDirs`. */ private async loadWorkspaceVersions(): Promise { const pkgDirs = await discoverPackageDirs(); for (const pkgDir of pkgDirs) { const pkgPath = join(pkgDir, "package.json"); try { const content = await readFile(pkgPath, "utf-8"); const pkg: PackageJson = JSON.parse(content); if (pkg.name && pkg.version) { this.workspaceVersions.set(pkg.name, pkg.version); } } catch { // Skip entries without a readable package.json } } } /** * Load catalogs from root package.json * * Loads both the default catalog (workspaces.catalog) and named catalogs * (workspaces.catalogs) into memory for fast lookups. */ private async loadCatalogs(): Promise { const rootPkgPath = join(CONFIG.paths.root, "package.json"); const content = await readFile(rootPkgPath, "utf-8"); const rootPkg: PackageJson = JSON.parse(content); // Load default catalog if (rootPkg.workspaces?.catalog) { this.catalogs.set("", rootPkg.workspaces.catalog); } // Load named catalogs if (rootPkg.workspaces?.catalogs) { for (const [name, versions] of Object.entries( rootPkg.workspaces.catalogs )) { this.catalogs.set(name, versions); } } } /** * Get the number of loaded catalogs */ getCatalogCount(): number { return this.catalogs.size; } /** * Get all catalog names */ getCatalogNames(): string[] { return Array.from(this.catalogs.keys()); } /** * Resolve a catalog reference to an actual version * * @param packageName - The package name to resolve (e.g., 'react') * @param catalogRef - The catalog reference (e.g., 'catalog:react' or 'catalog:') * @returns The resolved version string, or null if not found * * @example * ```typescript * resolver.resolve('react', 'catalog:react') // Returns: '19.0.0' * resolver.resolve('vitest', 'catalog:testing') // Returns: '2.1.0' * resolver.resolve('typescript', 'catalog:') // Returns: '5.9.2' * ``` */ resolve(packageName: string, catalogRef: string): string | null { const match = catalogRef.match(/^catalog:(.*)$/); if (!match) { return null; } const catalogName = match[1] || ""; const catalog = this.catalogs.get(catalogName); if (!catalog) { return null; } return catalog[packageName] || null; } /** * Find which catalog contains a package * * @param packageName - The package name to search for * @returns CatalogEntry with catalog name and version, or null if not found * * @example * ```typescript * resolver.findInCatalogs('react') * // Returns: { catalogName: 'react', version: '19.0.0' } * ``` */ findInCatalogs(packageName: string): CatalogEntry | null { for (const [catalogName, catalog] of this.catalogs.entries()) { const version = catalog[packageName]; if (version) { return { catalogName, version }; } } return null; } /** * Resolve dependencies object, converting catalog refs to actual versions * * @param deps - Dependencies object from package.json * @param currentVersion - Current package version (for workspace:* refs) * @returns Resolved dependencies with actual versions * * @example * ```typescript * const resolved = resolver.resolveDependencies({ * 'react': 'catalog:react', * '@suaveplan/utils': 'workspace:*', * 'lodash': '^4.17.21' * }, '1.0.0'); * // Returns: { * // 'react': '19.0.0', * // '@suaveplan/utils': '^1.0.0', * // 'lodash': '^4.17.21' * // } * ``` */ resolveDependencies( deps: Record | undefined, _currentVersion: string ): Record | undefined { if (!deps) return undefined; const resolved: Record = {}; for (const [name, version] of Object.entries(deps)) { let resolvedVersion = version; // First, resolve catalog references if (version.startsWith("catalog:")) { const catalogResolved = this.resolve(name, version); if (catalogResolved) { resolvedVersion = catalogResolved; } else { continue; // Skip unresolved catalog refs } } // Then, resolve workspace references (including those from catalogs) if (resolvedVersion.startsWith("workspace:")) { if (name.startsWith("@suaveplan/")) { // Use the actual workspace package version so consumers // pick up the correct minimum (prevents bun from resolving // to stale nested versions from the registry). const wsVersion = this.workspaceVersions.get(name); resolved[name] = wsVersion ? `^${wsVersion}` : "^1.0.0"; } else { resolved[name] = "*"; } } else { resolved[name] = resolvedVersion; } } return Object.keys(resolved).length > 0 ? resolved : undefined; } /** * Convert a regular version string to a catalog reference * * @param packageName - The package name * @param version - The version string * @returns Catalog reference string, or null if package not in any catalog * * @example * ```typescript * resolver.toCatalogRef('react', '19.0.0') * // Returns: 'catalog:react' * ``` */ toCatalogRef(packageName: string, version: string): string | null { const entry = this.findInCatalogs(packageName); if (!entry) { return null; } // Only convert if versions match if (entry.version === version) { return entry.catalogName ? `catalog:${entry.catalogName}` : "catalog:"; } return null; } /** * Get all packages in a specific catalog * * @param catalogName - The catalog name (empty string for default catalog) * @returns Record of package names to versions */ getCatalog(catalogName: string): Record | undefined { return this.catalogs.get(catalogName); } /** * Check if a package exists in any catalog * * @param packageName - The package name to check * @returns true if package exists in at least one catalog */ hasPackage(packageName: string): boolean { return this.findInCatalogs(packageName) !== null; } }