diff --git a/scopes/dependencies/dependency-resolver/dependency-installer.ts b/scopes/dependencies/dependency-resolver/dependency-installer.ts index f54db81764e7..78682dfe88fe 100644 --- a/scopes/dependencies/dependency-resolver/dependency-installer.ts +++ b/scopes/dependencies/dependency-resolver/dependency-installer.ts @@ -40,7 +40,6 @@ export type InstallOptions = { packageManagerConfigRootDir?: string; resolveVersionsFromDependenciesOnly?: boolean; linkedDependencies?: Record>; - pruneNodeModules?: boolean; }; export type GetComponentManifestsOptions = { @@ -180,7 +179,6 @@ export class DependencyInstaller { engineStrict: this.engineStrict, packageManagerConfigRootDir: options.packageManagerConfigRootDir, peerDependencyRules: this.peerDependencyRules, - pruneNodeModules: options.pruneNodeModules, hidePackageManagerOutput, ...packageManagerOptions, }; @@ -233,6 +231,13 @@ export class DependencyInstaller { return installResult; } + public async pruneModules(rootDir: string): Promise { + if (!this.packageManager.pruneModules) { + return; + } + await this.packageManager.pruneModules(rootDir); + } + /** * Compute all the component manifests (a.k.a. package.json files) that should be passed to the package manager * in order to install the dependencies. diff --git a/scopes/dependencies/dependency-resolver/package-manager.ts b/scopes/dependencies/dependency-resolver/package-manager.ts index 4a23611af27c..fb8650a774b3 100644 --- a/scopes/dependencies/dependency-resolver/package-manager.ts +++ b/scopes/dependencies/dependency-resolver/package-manager.ts @@ -53,8 +53,6 @@ export type PackageManagerInstallOptions = { updateAll?: boolean; hidePackageManagerOutput?: boolean; - - pruneNodeModules?: boolean; }; export type PackageManagerGetPeerDependencyIssuesOptions = PackageManagerInstallOptions; @@ -95,6 +93,8 @@ export interface PackageManager { options: PackageManagerInstallOptions ): Promise<{ dependenciesChanged: boolean }>; + pruneModules?(rootDir: string): Promise; + resolveRemoteVersion( packageName: string, options: PackageManagerResolveRemoteVersionOptions diff --git a/scopes/dependencies/pnpm/lynx.ts b/scopes/dependencies/pnpm/lynx.ts index 126c64ee4210..2adaf884ddd5 100644 --- a/scopes/dependencies/pnpm/lynx.ts +++ b/scopes/dependencies/pnpm/lynx.ts @@ -166,7 +166,6 @@ export async function install( updateAll?: boolean; nodeLinker?: 'hoisted' | 'isolated'; overrides?: Record; - pruneNodeModules?: boolean; rootComponents?: boolean; rootComponentsForCapsules?: boolean; includeOptionalDeps?: boolean; @@ -226,7 +225,7 @@ export async function install( workspacePackages, preferFrozenLockfile: true, pruneLockfileImporters: true, - modulesCacheMaxAge: options.pruneNodeModules ? 0 : undefined, + modulesCacheMaxAge: Infinity, // pnpm should never prune the virtual store. Bit does it on its own. neverBuiltDependencies: ['core-js'], registries: registriesMap, resolutionMode: 'highest', diff --git a/scopes/dependencies/pnpm/pnpm-prune-modules.ts b/scopes/dependencies/pnpm/pnpm-prune-modules.ts new file mode 100644 index 000000000000..84bda035f4c8 --- /dev/null +++ b/scopes/dependencies/pnpm/pnpm-prune-modules.ts @@ -0,0 +1,23 @@ +import fs from 'fs-extra'; +import path from 'path'; +import { difference } from 'lodash'; +import { readCurrentLockfile } from '@pnpm/lockfile-file'; +import { depPathToFilename } from '@pnpm/dependency-path'; + +/** + * Reads the private lockfile at node_modules/.pnpm/lock.yaml + * and removes any directories from node_modules/.pnpm that are not listed in the lockfile. + */ +export async function pnpmPruneModules(rootDir: string): Promise { + const virtualStoreDir = path.join(rootDir, 'node_modules/.pnpm'); + const pkgDirs = await readPackageDirsFromVirtualStore(virtualStoreDir); + if (pkgDirs.length === 0) return; + const lockfile = await readCurrentLockfile(virtualStoreDir, { ignoreIncompatible: false }); + const dirsShouldBePresent = Object.keys(lockfile?.packages ?? {}).map(depPathToFilename); + await Promise.all(difference(pkgDirs, dirsShouldBePresent).map((dir) => fs.remove(path.join(virtualStoreDir, dir)))); +} + +async function readPackageDirsFromVirtualStore(virtualStoreDir: string): Promise { + const allDirs = await fs.readdir(virtualStoreDir); + return allDirs.filter((dir) => dir !== 'lock.yaml' && dir !== 'node_modules'); +} diff --git a/scopes/dependencies/pnpm/pnpm.package-manager.ts b/scopes/dependencies/pnpm/pnpm.package-manager.ts index c3ac54389e7c..91c70c5b7f3f 100644 --- a/scopes/dependencies/pnpm/pnpm.package-manager.ts +++ b/scopes/dependencies/pnpm/pnpm.package-manager.ts @@ -19,6 +19,7 @@ import { readModulesManifest } from '@pnpm/modules-yaml'; import { ProjectManifest } from '@pnpm/types'; import { join } from 'path'; import { readConfig } from './read-config'; +import { pnpmPruneModules } from './pnpm-prune-modules'; export class PnpmPackageManager implements PackageManager { readonly name = 'pnpm'; @@ -74,7 +75,6 @@ export class PnpmPackageManager implements PackageManager { sideEffectsCacheRead: installOptions.sideEffectsCache ?? true, sideEffectsCacheWrite: installOptions.sideEffectsCache ?? true, pnpmHomeDir: config.pnpmHomeDir, - pruneNodeModules: installOptions.pruneNodeModules, updateAll: installOptions.updateAll, hidePackageManagerOutput: installOptions.hidePackageManagerOutput, }, @@ -198,4 +198,8 @@ export class PnpmPackageManager implements PackageManager { getWorkspaceDepsOfBitRoots(manifests: ProjectManifest[]): Record { return Object.fromEntries(manifests.map((manifest) => [manifest.name, 'workspace:*'])); } + + async pruneModules(rootDir: string): Promise { + return pnpmPruneModules(rootDir); + } } diff --git a/scopes/workspace/install/install.main.runtime.ts b/scopes/workspace/install/install.main.runtime.ts index b5a80af3976b..78f14c3f1894 100644 --- a/scopes/workspace/install/install.main.runtime.ts +++ b/scopes/workspace/install/install.main.runtime.ts @@ -248,9 +248,6 @@ export class InstallMain { current.componentDirectoryMap, { installTeambitBit: false, - // We clean node_modules only on the first install. - // Otherwise, we might load an env from a location that we later remove. - pruneNodeModules: installCycle === 0, }, pmInstallOptions ); @@ -277,6 +274,9 @@ export class InstallMain { current = await this._getComponentsManifests(installer, mergedRootPolicy, pmInstallOptions); installCycle += 1; } while ((!prevManifests.has(manifestsHash(current.manifests)) || hasMissingLocalComponents) && installCycle < 5); + // We clean node_modules only after the last install. + // Otherwise, we might load an env from a location that we later remove. + await installer.pruneModules(this.workspace.path); await this.workspace.consumer.componentFsCache.deleteAllDependenciesDataCache(); /* eslint-enable no-await-in-loop */ return current.componentDirectoryMap;