@@ -72,16 +72,19 @@ async function getWorkspaceGlobs(rootDir: string, pm: PackageManager): Promise<s
7272 return [ ] ;
7373}
7474
75- /** Load catalog definitions from pnpm-workspace.yaml or root package.json */
76- async function loadCatalogs ( rootDir : string , pm : PackageManager ) : Promise < CatalogMap > {
75+ /**
76+ * Files that may contain catalog definitions, in the order they're applied.
77+ * Later entries override earlier ones (matching loadCatalogs behavior).
78+ */
79+ export const CATALOG_FILES = [ 'pnpm-workspace.yaml' , 'package.json' ] as const ;
80+
81+ /** Parse catalog definitions from the raw contents of pnpm-workspace.yaml and root package.json */
82+ export function parseCatalogs ( pnpmWorkspaceYaml : string | null , rootPackageJson : string | null ) : CatalogMap {
7783 const catalogs : CatalogMap = new Map ( ) ;
7884
79- if ( pm === 'pnpm' ) {
80- // pnpm: catalogs live in pnpm-workspace.yaml
81- const wsFile = resolve ( rootDir , 'pnpm-workspace.yaml' ) ;
82- if ( await exists ( wsFile ) ) {
83- const content = await readText ( wsFile ) ;
84- const parsed = yaml . load ( content ) as {
85+ if ( pnpmWorkspaceYaml ) {
86+ try {
87+ const parsed = yaml . load ( pnpmWorkspaceYaml ) as {
8588 catalog ?: Record < string , string > ;
8689 catalogs ?: Record < string , Record < string , string > > ;
8790 } | null ;
@@ -94,43 +97,66 @@ async function loadCatalogs(rootDir: string, pm: PackageManager): Promise<Catalo
9497 catalogs . set ( name , deps ) ;
9598 }
9699 }
100+ } catch {
101+ // ignore malformed yaml
97102 }
98103 }
99104
100- // bun/npm/yarn + pnpm fallback: catalogs in root package.json
101- try {
102- const pkg = await readJson < Record < string , unknown > > ( resolve ( rootDir , 'package.json' ) ) ;
103-
104- // Check top-level catalog/catalogs
105- if ( pkg . catalog && typeof pkg . catalog === 'object' ) {
106- catalogs . set ( '' , pkg . catalog as Record < string , string > ) ;
107- }
108- if ( pkg . catalogs && typeof pkg . catalogs === 'object' ) {
109- for ( const [ name , deps ] of Object . entries ( pkg . catalogs as Record < string , Record < string , string > > ) ) {
110- catalogs . set ( name , deps ) ;
111- }
112- }
105+ if ( rootPackageJson ) {
106+ try {
107+ const pkg = JSON . parse ( rootPackageJson ) as Record < string , unknown > ;
113108
114- // Also check inside workspaces object (bun style)
115- const workspaces = pkg . workspaces ;
116- if ( workspaces && typeof workspaces === 'object' && ! Array . isArray ( workspaces ) ) {
117- const ws = workspaces as Record < string , unknown > ;
118- if ( ws . catalog && typeof ws . catalog === 'object' ) {
119- catalogs . set ( '' , ws . catalog as Record < string , string > ) ;
109+ // Top-level catalog/catalogs (used by bun, yarn, and proposed npm)
110+ if ( pkg . catalog && typeof pkg . catalog === 'object' ) {
111+ catalogs . set ( '' , pkg . catalog as Record < string , string > ) ;
120112 }
121- if ( ws . catalogs && typeof ws . catalogs === 'object' ) {
122- for ( const [ name , deps ] of Object . entries ( ws . catalogs as Record < string , Record < string , string > > ) ) {
113+ if ( pkg . catalogs && typeof pkg . catalogs === 'object' ) {
114+ for ( const [ name , deps ] of Object . entries ( pkg . catalogs as Record < string , Record < string , string > > ) ) {
123115 catalogs . set ( name , deps ) ;
124116 }
125117 }
118+
119+ // Inside workspaces object (bun style)
120+ const workspaces = pkg . workspaces ;
121+ if ( workspaces && typeof workspaces === 'object' && ! Array . isArray ( workspaces ) ) {
122+ const ws = workspaces as Record < string , unknown > ;
123+ if ( ws . catalog && typeof ws . catalog === 'object' ) {
124+ catalogs . set ( '' , ws . catalog as Record < string , string > ) ;
125+ }
126+ if ( ws . catalogs && typeof ws . catalogs === 'object' ) {
127+ for ( const [ name , deps ] of Object . entries ( ws . catalogs as Record < string , Record < string , string > > ) ) {
128+ catalogs . set ( name , deps ) ;
129+ }
130+ }
131+ }
132+ } catch {
133+ // ignore malformed json
126134 }
127- } catch {
128- // ignore
129135 }
130136
131137 return catalogs ;
132138}
133139
140+ /** Load catalog definitions from pnpm-workspace.yaml or root package.json */
141+ async function loadCatalogs ( rootDir : string , pm : PackageManager ) : Promise < CatalogMap > {
142+ // pnpm-workspace.yaml is only read for pnpm — other PMs don't recognize it
143+ let pnpmYaml : string | null = null ;
144+ if ( pm === 'pnpm' ) {
145+ const wsFile = resolve ( rootDir , 'pnpm-workspace.yaml' ) ;
146+ if ( await exists ( wsFile ) ) {
147+ pnpmYaml = await readText ( wsFile ) ;
148+ }
149+ }
150+
151+ let pkgJsonText : string | null = null ;
152+ const pkgJsonPath = resolve ( rootDir , 'package.json' ) ;
153+ if ( await exists ( pkgJsonPath ) ) {
154+ pkgJsonText = await readText ( pkgJsonPath ) ;
155+ }
156+
157+ return parseCatalogs ( pnpmYaml , pkgJsonText ) ;
158+ }
159+
134160/** Resolve a specific dependency's catalog: reference */
135161export function resolveCatalogDep ( depName : string , range : string , catalogs : CatalogMap ) : string | null {
136162 if ( ! range . startsWith ( 'catalog:' ) ) return null ;
@@ -139,3 +165,44 @@ export function resolveCatalogDep(depName: string, range: string, catalogs: Cata
139165 if ( ! catalog ) return null ;
140166 return catalog [ depName ] ?? null ;
141167}
168+
169+ /**
170+ * Diff two catalog states and return the set of (catalogName → changed depNames).
171+ * Includes added, removed, and version-changed entries.
172+ */
173+ export function diffCatalogMaps ( before : CatalogMap , after : CatalogMap ) : Map < string , Set < string > > {
174+ const changes = new Map < string , Set < string > > ( ) ;
175+ const catalogNames = new Set ( [ ...before . keys ( ) , ...after . keys ( ) ] ) ;
176+
177+ for ( const catalogName of catalogNames ) {
178+ const beforeDeps = before . get ( catalogName ) ?? { } ;
179+ const afterDeps = after . get ( catalogName ) ?? { } ;
180+ const depNames = new Set ( [ ...Object . keys ( beforeDeps ) , ...Object . keys ( afterDeps ) ] ) ;
181+ const changedDeps = new Set < string > ( ) ;
182+ for ( const depName of depNames ) {
183+ if ( beforeDeps [ depName ] !== afterDeps [ depName ] ) {
184+ changedDeps . add ( depName ) ;
185+ }
186+ }
187+ if ( changedDeps . size > 0 ) {
188+ changes . set ( catalogName , changedDeps ) ;
189+ }
190+ }
191+
192+ return changes ;
193+ }
194+
195+ /**
196+ * Given a set of catalog entries that have changed, return the set of catalog
197+ * references (e.g. "catalog:" or "catalog:testing") that affect those entries.
198+ * Used to match package.json dep ranges against changed catalog entries.
199+ */
200+ export function isCatalogRefAffected (
201+ range : string ,
202+ depName : string ,
203+ catalogChanges : Map < string , Set < string > > ,
204+ ) : boolean {
205+ if ( ! range . startsWith ( 'catalog:' ) ) return false ;
206+ const catalogName = range . slice ( 'catalog:' . length ) . trim ( ) || '' ;
207+ return catalogChanges . get ( catalogName ) ?. has ( depName ) ?? false ;
208+ }
0 commit comments