Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
}
86 changes: 74 additions & 12 deletions rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<string, string[] | boolean> | undefined;
const subPackagesByIntegrity: Map<string, string[] | boolean> = 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<string, IResolverContext>,
missingOptionalDependencies: Set<string>
Expand All @@ -91,17 +107,19 @@ 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<void> {
async function tryFindNestedPackageJsonsForContextAsync(
context: IResolverContext
): Promise<string[] | boolean> {
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;
return true;
}

// Convert an integrity hash like
Expand All @@ -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) {
Expand All @@ -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<void> {
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;
}
}

Expand All @@ -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.`);
}
Loading