diff --git a/common/changes/@rushstack/package-extractor/enhance-package-extractor-collect-folders_2026-04-08-23-56.json b/common/changes/@rushstack/package-extractor/enhance-package-extractor-collect-folders_2026-04-08-23-56.json new file mode 100644 index 00000000000..4cd9425aef0 --- /dev/null +++ b/common/changes/@rushstack/package-extractor/enhance-package-extractor-collect-folders_2026-04-08-23-56.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/package-extractor", + "comment": "Update _collectFoldersAsync to process all starting folders in a single shared queue instead of serially. Add pnpmNodeModulesHoistingEnabled option to IExtractorSubspace to skip virtual store hoisting lookup when hoisting is disabled.", + "type": "minor" + } + ], + "packageName": "@rushstack/package-extractor" +} diff --git a/common/reviews/api/package-extractor.api.md b/common/reviews/api/package-extractor.api.md index f0503a30d4f..1b0cfb0139c 100644 --- a/common/reviews/api/package-extractor.api.md +++ b/common/reviews/api/package-extractor.api.md @@ -58,6 +58,7 @@ export interface IExtractorProjectConfiguration { // @public export interface IExtractorSubspace { pnpmInstallFolder?: string; + pnpmNodeModulesHoistingEnabled?: boolean; subspaceName: string; transformPackageJson?: (packageJson: IPackageJson) => IPackageJson; } diff --git a/libraries/package-extractor/src/PackageExtractor.ts b/libraries/package-extractor/src/PackageExtractor.ts index 1e7c3602111..c2f4c790f7f 100644 --- a/libraries/package-extractor/src/PackageExtractor.ts +++ b/libraries/package-extractor/src/PackageExtractor.ts @@ -108,6 +108,12 @@ export interface IExtractorSubspace { * transform the package.json prior to extraction. */ transformPackageJson?: (packageJson: IPackageJson) => IPackageJson; + /** + * Whether PNPM hoisting is enabled for this subspace. When set to `false`, + * the extractor will skip looking for hoisted packages in the PNPM virtual store, since no + * hoisting symlinks will exist. Default is `true`. + */ + pnpmNodeModulesHoistingEnabled?: boolean; } interface IExtractorState { @@ -489,10 +495,12 @@ export class PackageExtractor { } } + const startingFolders: string[] = []; for (const { projectName, projectFolder } of includedProjectsSet) { terminal.writeLine(Colorize.cyan(`Analyzing project: ${projectName}`)); - await this._collectFoldersAsync(projectFolder, options, state); + startingFolders.push(projectFolder); } + await this._collectFoldersAsync(startingFolders, options, state); if (!createArchiveOnly) { terminal.writeLine(`Copying folders to target folder "${targetRootFolder}"`); @@ -524,14 +532,14 @@ export class PackageExtractor { * Recursively crawl the node_modules dependencies and collect the result in IExtractorState.foldersToCopy. */ private async _collectFoldersAsync( - packageJsonFolder: string, + packageJsonFolders: string[], options: IExtractorOptions, state: IExtractorState ): Promise { const { terminal, subspaces } = options; const { projectConfigurationsByPath } = state; - const packageJsonFolderPathQueue: AsyncQueue = new AsyncQueue([packageJsonFolder]); + const packageJsonFolderPathQueue: AsyncQueue = new AsyncQueue(packageJsonFolders); await Async.forEachAsync( packageJsonFolderPathQueue, @@ -624,9 +632,15 @@ export class PackageExtractor { // Replicate the links to the virtual store. Note that if the package has not been hoisted by // PNPM, the package will not be resolvable from here. - // Only apply this logic for packages that were actually installed under the common/temp folder. + // Only apply this logic for packages that were actually installed under the common/temp folder, + // and only when hoisting is enabled for the subspace. const realPnpmInstallFolder: string | undefined = targetSubspace?.pnpmInstallFolder; - if (realPnpmInstallFolder && Path.isUnder(packageJsonFolderPath, realPnpmInstallFolder)) { + const hoistingEnabled: boolean = targetSubspace?.pnpmNodeModulesHoistingEnabled !== false; + if ( + hoistingEnabled && + realPnpmInstallFolder && + Path.isUnder(packageJsonFolderPath, realPnpmInstallFolder) + ) { try { // The PNPM virtual store links are created in this folder. We will resolve the current package // from that location and collect any additional links encountered along the way.