diff --git a/common/changes/@microsoft/rush/resolver-cache-cache_2025-08-25-20-30.json b/common/changes/@microsoft/rush/resolver-cache-cache_2025-08-25-20-30.json new file mode 100644 index 00000000000..d0c66e2eb49 --- /dev/null +++ b/common/changes/@microsoft/rush/resolver-cache-cache_2025-08-25-20-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "[resolver-cache-plugin] Optimize search for nested package.json files with persistent cache file keyed by integrity hash.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts b/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts index 5917bb1a5d5..118159e5601 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts @@ -42,6 +42,10 @@ function getPlatformInfo(): IPlatformInfo { const END_TOKEN: string = '/package.json":'; +interface INestedPackageJsonCache { + subPackagesByIntegrity: [string, string[] | boolean][]; +} + /** * Plugin entry point for after install. * @param rushSession - The Rush Session @@ -80,9 +84,21 @@ export async function afterInstallAsync( rushConfiguration.getProjectLookupForRoot(workspaceRoot); const cacheFilePath: string = `${workspaceRoot}/resolver-cache.json`; + const subPackageCacheFilePath: string = `${workspaceRoot}/subpackage-entry-cache.json`; terminal.writeLine(`Resolver cache will be written at ${cacheFilePath}`); + let oldSubPackagesByIntegrity: Map | undefined; + const subPackagesByIntegrity: Map = new Map(); + try { + const cacheContent: string = await FileSystem.readFileAsync(subPackageCacheFilePath); + const cacheJson: INestedPackageJsonCache = JSON.parse(cacheContent); + oldSubPackagesByIntegrity = new Map(cacheJson.subPackagesByIntegrity); + terminal.writeLine(`Loaded subpackage cache from ${subPackageCacheFilePath}`); + } catch (err) { + // Ignore + } + async function afterExternalPackagesAsync( contexts: Map, missingOptionalDependencies: Set @@ -91,9 +107,11 @@ export async function afterInstallAsync( * Loads the index file from the pnpm store to discover nested package.json files in an external package * For internal packages, assumes there are no nested package.json files. * @param context - The context to find nested package.json files for - * @returns A promise that resolves when the nested package.json files are found, if applicable + * @returns A promise that resolves to the nested package.json paths, false if the package fails to load, or true if the package has no nested package.json files. */ - async function findNestedPackageJsonsForContextAsync(context: IResolverContext): Promise { + async function tryFindNestedPackageJsonsForContextAsync( + context: IResolverContext + ): Promise { const { descriptionFileRoot, descriptionFileHash } = context; if (descriptionFileHash === undefined) { @@ -101,7 +119,7 @@ export async function afterInstallAsync( terminal.writeDebugLine( `Package at ${descriptionFileRoot} does not have a file list. Assuming no nested "package.json" files.` ); - return; + return true; } // Convert an integrity hash like @@ -121,8 +139,6 @@ export async function afterInstallAsync( let endIndex: number = indexContent.lastIndexOf(END_TOKEN); if (endIndex > 0) { const nestedPackageDirs: string[] = []; - // eslint-disable-next-line require-atomic-updates - context.nestedPackageDirs = nestedPackageDirs; do { const startIndex: number = indexContent.lastIndexOf('"', endIndex); if (startIndex < 0) { @@ -134,17 +150,54 @@ export async function afterInstallAsync( nestedPackageDirs.push(nestedPath); endIndex = indexContent.lastIndexOf(END_TOKEN, startIndex - 1); } while (endIndex > 0); + return nestedPackageDirs; } + return true; } catch (error) { if (!context.optional) { throw new Error( `Error reading index file for: "${context.descriptionFileRoot}" (${descriptionFileHash}): ${error.toString()}` ); - } else { - terminal.writeLine(`Trimming missing optional dependency at: ${descriptionFileRoot}`); - contexts.delete(descriptionFileRoot); - missingOptionalDependencies.add(descriptionFileRoot); } + return false; + } + } + /** + * Loads the index file from the pnpm store to discover nested package.json files in an external package + * For internal packages, assumes there are no nested package.json files. + * @param context - The context to find nested package.json files for + * @returns A promise that resolves when the nested package.json files are found, if applicable + */ + async function findNestedPackageJsonsForContextAsync(context: IResolverContext): Promise { + const { descriptionFileRoot, descriptionFileHash } = context; + + if (descriptionFileHash === undefined) { + // Assume this package has no nested package json files for now. + terminal.writeDebugLine( + `Package at ${descriptionFileRoot} does not have a file list. Assuming no nested "package.json" files.` + ); + return; + } + + let result: string[] | boolean | undefined = + oldSubPackagesByIntegrity?.get(descriptionFileHash) ?? + subPackagesByIntegrity.get(descriptionFileHash); + if (result === undefined) { + result = await tryFindNestedPackageJsonsForContextAsync(context); + } + subPackagesByIntegrity.set(descriptionFileHash, result); + if (result === true) { + // Default case. Do nothing. + } else if (result === false) { + terminal.writeLine(`Trimming missing optional dependency at: ${descriptionFileRoot}`); + contexts.delete(descriptionFileRoot); + missingOptionalDependencies.add(descriptionFileRoot); + } else { + terminal.writeDebugLine( + `Nested "package.json" files found for package at ${descriptionFileRoot}: ${result.join(', ')}` + ); + // eslint-disable-next-line require-atomic-updates + context.nestedPackageDirs = result; } } @@ -165,11 +218,20 @@ export async function afterInstallAsync( afterExternalPackagesAsync }); + const newSubPackageCache: string = JSON.stringify({ + subPackagesByIntegrity: Array.from(subPackagesByIntegrity) + }); + const serialized: string = JSON.stringify(cacheFile); - await FileSystem.writeFileAsync(cacheFilePath, serialized, { - ensureFolderExists: true - }); + await Promise.all([ + FileSystem.writeFileAsync(cacheFilePath, serialized, { + ensureFolderExists: true + }), + FileSystem.writeFileAsync(subPackageCacheFilePath, newSubPackageCache, { + ensureFolderExists: true + }) + ]); terminal.writeLine(`Resolver cache written.`); }