@@ -21,7 +21,7 @@ const NODE_MODULES = `node_modules` as Filename;
2121const DOT_BIN = `.bin` as Filename ;
2222const INSTALL_STATE_FILE = `.yarn-state.yml` as Filename ;
2323
24- type InstallState = { locatorMap : NodeModulesLocatorMap , locationTree : LocationTree , binSymlinks : BinSymlinkMap , nmMode : NodeModulesMode } ;
24+ type InstallState = { locatorMap : NodeModulesLocatorMap , locationTree : LocationTree , binSymlinks : BinSymlinkMap , nmMode : NodeModulesMode , mtimeMs : number } ;
2525type BinSymlinkMap = Map < PortablePath , Map < Filename , PortablePath > > ;
2626type LoadManifest = ( locator : LocatorKey , installLocation : PortablePath ) => Promise < Pick < Manifest , 'bin' > > ;
2727
@@ -232,7 +232,7 @@ class NodeModulesInstaller implements Installer {
232232 if ( preinstallState === null || nmModeSetting !== preinstallState . nmMode ) {
233233 this . opts . project . storedBuildState . clear ( ) ;
234234
235- preinstallState = { locatorMap : new Map ( ) , binSymlinks : new Map ( ) , locationTree : new Map ( ) , nmMode : nmModeSetting } ;
235+ preinstallState = { locatorMap : new Map ( ) , binSymlinks : new Map ( ) , locationTree : new Map ( ) , nmMode : nmModeSetting , mtimeMs : 0 } ;
236236 }
237237
238238 const hoistingLimitsByCwd = new Map ( this . opts . project . workspaces . map ( workspace => {
@@ -392,7 +392,7 @@ async function extractCustomPackageData(pkg: Package, fetchResult: FetchResult)
392392 } ;
393393}
394394
395- async function writeInstallState ( project : Project , locatorMap : NodeModulesLocatorMap , binSymlinks : BinSymlinkMap , nmMode : { value : NodeModulesMode } ) {
395+ async function writeInstallState ( project : Project , locatorMap : NodeModulesLocatorMap , binSymlinks : BinSymlinkMap , nmMode : { value : NodeModulesMode } , { installChangedByUser } : { installChangedByUser : boolean } ) {
396396 let locatorState = `` ;
397397
398398 locatorState += `# Warning: This file is automatically generated. Removing it is fine, but will\n` ;
@@ -445,6 +445,10 @@ async function writeInstallState(project: Project, locatorMap: NodeModulesLocato
445445 const rootPath = project . cwd ;
446446 const installStatePath = ppath . join ( rootPath , NODE_MODULES , INSTALL_STATE_FILE ) ;
447447
448+ // Force install state file rewrite, so that it has mtime bigger than all node_modules subfolders
449+ if ( installChangedByUser )
450+ await xfs . removePromise ( installStatePath ) ;
451+
448452 await xfs . changeFilePromise ( installStatePath , locatorState , {
449453 automaticNewlines : true ,
450454 } ) ;
@@ -454,7 +458,13 @@ async function findInstallState(project: Project, {unrollAliases = false}: {unro
454458 const rootPath = project . cwd ;
455459 const installStatePath = ppath . join ( rootPath , NODE_MODULES , INSTALL_STATE_FILE ) ;
456460
457- if ( ! xfs . existsSync ( installStatePath ) )
461+ let stats ;
462+ try {
463+ stats = await xfs . statPromise ( installStatePath ) ;
464+ } catch ( e ) {
465+ }
466+
467+ if ( ! stats )
458468 return null ;
459469
460470 const locatorState = parseSyml ( await xfs . readFilePromise ( installStatePath , `utf8` ) ) ;
@@ -481,7 +491,7 @@ async function findInstallState(project: Project, {unrollAliases = false}: {unro
481491 const location = ppath . join ( rootPath , npath . toPortablePath ( relativeLocation ) ) ;
482492 const symlinks = miscUtils . getMapWithDefault ( binSymlinks , location ) ;
483493 for ( const [ name , target ] of Object . entries ( locationSymlinks as any ) ) {
484- symlinks . set ( toFilename ( name ) , npath . toPortablePath ( [ location , NODE_MODULES , target ] . join ( ppath . delimiter ) ) ) ;
494+ symlinks . set ( toFilename ( name ) , npath . toPortablePath ( [ location , NODE_MODULES , target ] . join ( ppath . sep ) ) ) ;
485495 }
486496 }
487497 }
@@ -510,7 +520,7 @@ async function findInstallState(project: Project, {unrollAliases = false}: {unro
510520 }
511521 }
512522
513- return { locatorMap, binSymlinks, locationTree : buildLocationTree ( locatorMap , { skipPrefix : project . cwd } ) , nmMode} ;
523+ return { locatorMap, binSymlinks, locationTree : buildLocationTree ( locatorMap , { skipPrefix : project . cwd } ) , nmMode, mtimeMs : stats . mtimeMs } ;
514524}
515525
516526const removeDir = async ( dir : PortablePath , options : { contentsOnly : boolean , innerLoop ?: boolean , allowSymlink ?: boolean } ) : Promise < any > => {
@@ -818,40 +828,109 @@ const copyPromise = async (dstDir: PortablePath, srcDir: PortablePath, {baseFs,
818828} ;
819829
820830/**
821- * This function removes node_modules roots that do not exist on the filesystem from the location tree.
822- *
823- * This is needed to transparently support workflows on CI systems. When
824- * user caches only top-level node_modules and forgets to cache node_modules
825- * from deeper workspaces. By removing non-existent node_modules roots
826- * we make our location tree to represent the real tree on the file system.
827- *
828- * Please note, that this function doesn't help with any other inconsistency
829- * on a deeper level inside node_modules tree, it helps only when some node_modules roots
830- * do not exist at all
831+ * Synchronizes previous install state with the actual directories available on disk
831832 *
832833 * @param locationTree location tree
834+ * @param binSymlinks bin symlinks map
835+ * @param stateMtimeMs state file timestamp (this file is written after all node_modules files and directories)
833836 *
834- * @returns location tree with non-existent node_modules roots stripped
837+ * @returns location tree and bin symlinks with modules, unavailable on disk, removed
835838 */
836- function refineNodeModulesRoots ( locationTree : LocationTree , binSymlinks : BinSymlinkMap ) : { locationTree : LocationTree , binSymlinks : BinSymlinkMap } {
837- const refinedLocationTree : LocationTree = new Map ( [ ...locationTree ] ) ;
838- const refinedBinSymlinks : BinSymlinkMap = new Map ( [ ...binSymlinks ] ) ;
839+ function syncPreinstallStateWithDisk ( locationTree : LocationTree , binSymlinks : BinSymlinkMap , stateMtimeMs : number , project : Project ) : { locationTree : LocationTree , binSymlinks : BinSymlinkMap , locatorLocations : Map < LocatorKey , Set < PortablePath > > , installChangedByUser : boolean } {
840+ const refinedLocationTree : LocationTree = new Map ( ) ;
841+ const refinedBinSymlinks = new Map ( ) ;
842+ const locatorLocations = new Map ( ) ;
843+ let installChangedByUser = false ;
844+
845+ const syncNodeWithDisk = ( parentPath : PortablePath , entry : Filename , parentNode : LocationNode , refinedNode : LocationNode , nodeModulesDiskEntries : Set < Filename > ) => {
846+ let doesExistOnDisk = true ;
847+ const entryPath = ppath . join ( parentPath , entry ) ;
848+ let childNodeModulesDiskEntries = new Set < Filename > ( ) ;
849+
850+ if ( entry === NODE_MODULES ) {
851+ let stats ;
852+ try {
853+ stats = xfs . statSync ( entryPath ) ;
854+ } catch ( e ) {
855+ }
839856
840- for ( const [ workspaceRoot , node ] of locationTree ) {
841- const nodeModulesRoot = ppath . join ( workspaceRoot , NODE_MODULES ) ;
842- if ( ! xfs . existsSync ( nodeModulesRoot ) ) {
843- node . children . delete ( NODE_MODULES ) ;
844-
845- // O(m^2) complexity algorithm, but on a very few values, so not worth the trouble to optimize it
846- for ( const location of refinedBinSymlinks . keys ( ) ) {
847- if ( ppath . contains ( nodeModulesRoot , location ) !== null ) {
848- refinedBinSymlinks . delete ( location ) ;
857+ doesExistOnDisk = ! ! stats ;
858+
859+ if ( ! stats ) {
860+ installChangedByUser = true ;
861+ } else if ( stats . mtimeMs > stateMtimeMs ) {
862+ installChangedByUser = true ;
863+ childNodeModulesDiskEntries = new Set ( xfs . readdirSync ( entryPath ) ) ;
864+ } else {
865+ childNodeModulesDiskEntries = new Set ( parentNode . children . get ( NODE_MODULES ) ! . children . keys ( ) ) ;
866+ }
867+
868+ const binarySymlinks = binSymlinks . get ( parentPath ) ;
869+ if ( binarySymlinks ) {
870+ const binPath = ppath . join ( parentPath , NODE_MODULES , DOT_BIN ) ;
871+ let binStats ;
872+ try {
873+ binStats = xfs . statSync ( binPath ) ;
874+ } catch ( e ) {
849875 }
876+
877+ if ( ! binStats ) {
878+ installChangedByUser = true ;
879+ } else if ( binStats . mtimeMs > stateMtimeMs ) {
880+ installChangedByUser = true ;
881+
882+ const diskEntries = new Set ( xfs . readdirSync ( binPath ) ) ;
883+ const refinedBinarySymlinks = new Map ( ) ;
884+ refinedBinSymlinks . set ( parentPath , refinedBinarySymlinks ) ;
885+
886+ for ( const [ entry , target ] of binarySymlinks ) {
887+ if ( diskEntries . has ( entry ) ) {
888+ refinedBinarySymlinks . set ( entry , target ) ;
889+ }
890+ }
891+ } else {
892+ refinedBinSymlinks . set ( parentPath , binarySymlinks ) ;
893+ }
894+ }
895+ } else {
896+ doesExistOnDisk = nodeModulesDiskEntries . has ( entry ) ;
897+ }
898+
899+ const node = parentNode . children . get ( entry ) ! ;
900+ if ( doesExistOnDisk ) {
901+ const { linkType, locator} = node ;
902+ const childRefinedNode = { children : new Map ( ) , linkType, locator} ;
903+ refinedNode . children . set ( entry , childRefinedNode ) ;
904+ if ( locator ) {
905+ const locations = miscUtils . getSetWithDefault ( locatorLocations , locator ) ;
906+ locations . add ( entryPath ) ;
907+ locatorLocations . set ( locator , locations ) ;
908+ }
909+
910+ for ( const childEntry of node . children . keys ( ) ) {
911+ syncNodeWithDisk ( entryPath , childEntry , node , childRefinedNode , childNodeModulesDiskEntries ) ;
850912 }
913+ } else if ( node . locator ) {
914+ project . storedBuildState . delete ( structUtils . parseLocator ( node . locator ) . locatorHash ) ;
915+ }
916+ } ;
917+
918+ for ( const [ workspaceRoot , node ] of locationTree ) {
919+ const { linkType, locator} = node ;
920+ const refinedNode = { children : new Map ( ) , linkType, locator} ;
921+ refinedLocationTree . set ( workspaceRoot , refinedNode ) ;
922+ if ( locator ) {
923+ const locations = miscUtils . getSetWithDefault ( locatorLocations , node . locator ) ;
924+ locations . add ( workspaceRoot ) ;
925+ locatorLocations . set ( node . locator , locations ) ;
926+ }
927+
928+ if ( node . children . has ( NODE_MODULES ) ) {
929+ syncNodeWithDisk ( workspaceRoot , NODE_MODULES , node , refinedNode , new Set ( ) ) ;
851930 }
852931 }
853932
854- return { locationTree : refinedLocationTree , binSymlinks : refinedBinSymlinks } ;
933+ return { locationTree : refinedLocationTree , binSymlinks : refinedBinSymlinks , locatorLocations , installChangedByUser } ;
855934}
856935
857936function isLinkLocator ( locatorKey : LocatorKey ) : boolean {
@@ -942,7 +1021,12 @@ export function getGlobalHardlinksStore(configuration: Configuration): PortableP
9421021async function persistNodeModules ( preinstallState : InstallState , installState : NodeModulesLocatorMap , { baseFs, project, report, loadManifest, realLocatorChecksums} : { project : Project , baseFs : FakeFS < PortablePath > , report : Report , loadManifest : LoadManifest , realLocatorChecksums : Map < LocatorHash , string | null > } ) {
9431022 const rootNmDirPath = ppath . join ( project . cwd , NODE_MODULES ) ;
9441023
945- const { locationTree : prevLocationTree , binSymlinks : prevBinSymlinks } = refineNodeModulesRoots ( preinstallState . locationTree , preinstallState . binSymlinks ) ;
1024+ const {
1025+ locationTree : prevLocationTree ,
1026+ binSymlinks : prevBinSymlinks ,
1027+ locatorLocations : prevLocatorLocations ,
1028+ installChangedByUser,
1029+ } = syncPreinstallStateWithDisk ( preinstallState . locationTree , preinstallState . binSymlinks , preinstallState . mtimeMs , project ) ;
9461030
9471031 const locationTree = buildLocationTree ( installState , { skipPrefix : project . cwd } ) ;
9481032
@@ -1082,7 +1166,7 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
10821166
10831167 // Update changed locations
10841168 const addList : Array < { srcDir : PortablePath , dstDir : PortablePath , linkType : LinkType , realLocatorHash : LocatorHash } > = [ ] ;
1085- for ( const [ prevLocator , { locations} ] of preinstallState . locatorMap . entries ( ) ) {
1169+ for ( const [ prevLocator , locations ] of prevLocatorLocations ) {
10861170 for ( const location of locations ) {
10871171 const { locationRoot, segments} = parseLocation ( location , {
10881172 skipPrefix : project . cwd ,
@@ -1205,7 +1289,7 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
12051289 const binSymlinks = await createBinSymlinkMap ( installState , locationTree , project . cwd , { loadManifest} ) ;
12061290 await persistBinSymlinks ( prevBinSymlinks , binSymlinks , project . cwd ) ;
12071291
1208- await writeInstallState ( project , installState , binSymlinks , nmMode ) ;
1292+ await writeInstallState ( project , installState , binSymlinks , nmMode , { installChangedByUser } ) ;
12091293
12101294 if ( nmModeSetting == NodeModulesMode . HARDLINKS_GLOBAL && nmMode . value == NodeModulesMode . HARDLINKS_LOCAL ) {
12111295 report . reportWarningOnce ( MessageName . NM_HARDLINKS_MODE_DOWNGRADED , `'nmMode' has been downgraded to 'hardlinks-local' due to global cache and install folder being on different devices` ) ;
0 commit comments