@@ -276,6 +276,10 @@ export default class UpCommand extends BaseCommand {
276276 Descriptor ,
277277 ] > = [ ] ;
278278
279+ // Catalog entries that need to be updated in .yarnrc.yml, keyed by
280+ // `${catalogName ?? ''}\0${entryName}` to deduplicate across workspaces.
281+ const catalogUpdates = new Map < string , { catalogName : string | null , entryName : string , newRange : string } > ( ) ;
282+
279283 for ( const [ workspace , target , /*existing*/ , { suggestions} ] of allSuggestions ) {
280284 let selected : Descriptor ;
281285
@@ -319,17 +323,25 @@ export default class UpCommand extends BaseCommand {
319323 throw new Error ( `Assertion failed: This descriptor should have a matching entry` ) ;
320324
321325 if ( current . descriptorHash !== selected . descriptorHash ) {
322- workspace . manifest [ target ] . set (
323- selected . identHash ,
324- selected ,
325- ) ;
326-
327- afterWorkspaceDependencyReplacementList . push ( [
328- workspace ,
329- target ,
330- current ,
331- selected ,
332- ] ) ;
326+ if ( current . range . startsWith ( `catalog:` ) ) {
327+ // When the dependency uses the catalog: protocol, update the catalog entry
328+ // in .yarnrc.yml rather than rewriting package.json with the resolved version.
329+ const catalogName = current . range . slice ( `catalog:` . length ) || null ;
330+ const entryName = structUtils . stringifyIdent ( current ) ;
331+ catalogUpdates . set ( `${ catalogName ?? `` } \0${ entryName } ` , { catalogName, entryName, newRange : selected . range } ) ;
332+ } else {
333+ workspace . manifest [ target ] . set (
334+ selected . identHash ,
335+ selected ,
336+ ) ;
337+
338+ afterWorkspaceDependencyReplacementList . push ( [
339+ workspace ,
340+ target ,
341+ current ,
342+ selected ,
343+ ] ) ;
344+ }
333345 } else {
334346 const resolver = configuration . makeResolver ( ) ;
335347 const resolveOptions : MinimalResolveOptions = { project, resolver} ;
@@ -341,6 +353,64 @@ export default class UpCommand extends BaseCommand {
341353 }
342354 }
343355
356+ // If there are any catalog entries to update, do them all at once in a single rc update to avoid
357+ // multiple filesystem writes, and to ensure that the in-memory configuration is updated only once
358+ if ( catalogUpdates . size > 0 ) {
359+ type RcContent = {
360+ [ key : string ] : unknown ,
361+ catalog ?: Record < string , string > ,
362+ catalogs ?: Record < string , Record < string , string > > ,
363+ }
364+ // `Configuration.updateConfiguration()` round-trips `.yarnrc.yml` through Yarn's own
365+ // `parseSyml` / `stringifySyml` serializer, which has two trade-offs:
366+ // - Comments are stripped: any `#` comments in `.yarnrc.yml` are lost on the first
367+ // `yarn up` that touches a catalog entry.
368+ // - Keys are reordered: `stringifySyml` sorts keys according to a fixed priority
369+ // list, so the order of entries in `catalog:` and `catalogs:` may change.
370+ await Configuration . updateConfiguration ( project . cwd , ( rcContent : RcContent ) => {
371+ return Array . from ( catalogUpdates . values ( ) ) . reduce ( ( updated , { catalogName, entryName, newRange} ) => {
372+ // If catalogName is null, it means that the catalog entry is under the
373+ // top-level default catalog entry, so we should update the `catalog` field
374+ if ( catalogName === null ) {
375+ const existingCatalog = updated . catalog ?? { } ;
376+ return {
377+ ...updated ,
378+ catalog : {
379+ ...existingCatalog ,
380+ [ entryName ] : newRange
381+ }
382+ } ;
383+ }
384+
385+ // Otherwise, the catalog entry is under a named catalog, so we should update
386+ // that specific entry under the `catalogs` object
387+ const existingCatalogs = updated . catalogs ?? { } ;
388+ return {
389+ ...updated ,
390+ catalogs : {
391+ ...existingCatalogs ,
392+ [ catalogName ] : {
393+ ...( existingCatalogs [ catalogName ] ?? { } ) ,
394+ [ entryName ] : newRange ,
395+ } ,
396+ } ,
397+ } ;
398+ } , rcContent ) ;
399+ } ) ;
400+
401+
402+ // Update in-memory configuration so the subsequent install resolves the new ranges
403+ for ( const { catalogName, entryName, newRange} of catalogUpdates . values ( ) ) {
404+ if ( catalogName === null ) {
405+ const catalog = configuration . values . get ( `catalog` ) ;
406+ catalog ?. set ( entryName , newRange ) ;
407+ } else {
408+ const catalogs = configuration . values . get ( `catalogs` ) ;
409+ catalogs ?. get ( catalogName ) ?. set ( entryName , newRange ) ;
410+ }
411+ }
412+ }
413+
344414 await configuration . triggerMultipleHooks (
345415 ( hooks : Hooks ) => hooks . afterWorkspaceDependencyReplacement ,
346416 afterWorkspaceDependencyReplacementList ,
0 commit comments