44import * as path from 'node:path' ;
55import type * as child_process from 'node:child_process' ;
66
7- import { FileSystem , Executable , JsonFile , type JsonObject } from '@rushstack/node-core-library' ;
7+ import { FileSystem , Executable , JsonFile , Path , type JsonObject } from '@rushstack/node-core-library' ;
88import type { ITerminal } from '@rushstack/terminal' ;
99
1010/**
@@ -23,14 +23,19 @@ export class TestHelper {
2323 /**
2424 * Execute a Rush command using the locally-built Rush
2525 */
26- public async executeRushAsync ( args : string [ ] , workingDirectory : string ) : Promise < void > {
26+ public async executeRushAsync (
27+ args : string [ ] ,
28+ workingDirectory : string ,
29+ environment ?: NodeJS . ProcessEnv
30+ ) : Promise < void > {
2731 this . _terminal . writeLine ( `Executing: ${ process . argv0 } ${ this . _rushBinPath } ${ args . join ( ' ' ) } ` ) ;
2832
2933 const childProcess : child_process . ChildProcess = Executable . spawn (
3034 process . argv0 ,
3135 [ this . _rushBinPath , ...args ] ,
3236 {
3337 currentWorkingDirectory : workingDirectory ,
38+ environment,
3439 stdio : 'inherit'
3540 }
3641 ) ;
@@ -45,7 +50,7 @@ export class TestHelper {
4550 */
4651 public async createTestRepoAsync (
4752 testRepoPath : string ,
48- packageManagerType : 'npm' | 'yarn' ,
53+ packageManagerType : 'npm' | 'pnpm' | ' yarn',
4954 packageManagerVersion : string
5055 ) : Promise < void > {
5156 // Clean up previous test run and create empty test repo directory
@@ -66,6 +71,10 @@ export class TestHelper {
6671 delete rushJson . pnpmVersion ;
6772 delete rushJson . yarnVersion ;
6873 rushJson . npmVersion = packageManagerVersion ;
74+ } else if ( packageManagerType === 'pnpm' ) {
75+ delete rushJson . npmVersion ;
76+ delete rushJson . yarnVersion ;
77+ rushJson . pnpmVersion = packageManagerVersion ;
6978 } else if ( packageManagerType === 'yarn' ) {
7079 delete rushJson . pnpmVersion ;
7180 delete rushJson . npmVersion ;
@@ -151,7 +160,9 @@ export class TestHelper {
151160 // Verify symlinks resolve correctly for local dependencies
152161 if ( dep . startsWith ( 'test-project-' ) ) {
153162 const depRealPath : string = await FileSystem . getRealPathAsync ( depPath ) ;
154- const expectedRealPath : string = path . join ( testRepoPath , 'projects' , dep ) ;
163+ const expectedRealPath : string = await FileSystem . getRealPathAsync (
164+ path . join ( testRepoPath , 'projects' , dep )
165+ ) ;
155166 if ( depRealPath !== expectedRealPath ) {
156167 throw new Error (
157168 `ERROR: Symlink for ${ dep } does not resolve correctly!\n` +
@@ -164,6 +175,75 @@ export class TestHelper {
164175 this . _terminal . writeLine ( '✓ Dependencies installed correctly' ) ;
165176 }
166177
178+ /**
179+ * Verify that PNPM's global virtual store was enabled and moved out of the workspace node_modules folder.
180+ */
181+ public async verifyPnpmGlobalVirtualStoreAsync (
182+ testRepoPath : string ,
183+ sharedStorePath : string ,
184+ externalDependencyPaths : string [ ]
185+ ) : Promise < void > {
186+ this . _terminal . writeLine ( '\nVerifying PNPM global virtual store structure...' ) ;
187+
188+ const workspaceFilePath : string = path . join ( testRepoPath , 'common/temp/pnpm-workspace.yaml' ) ;
189+ const workspaceFileContents : string = await FileSystem . readFileAsync ( workspaceFilePath ) ;
190+ if ( ! workspaceFileContents . includes ( 'enableGlobalVirtualStore: true' ) ) {
191+ throw new Error ( `ERROR: enableGlobalVirtualStore was not written to ${ workspaceFilePath } ` ) ;
192+ }
193+
194+ const localVirtualStorePath : string = path . join ( testRepoPath , 'common/temp/node_modules/.pnpm' ) ;
195+ if ( await FileSystem . existsAsync ( localVirtualStorePath ) ) {
196+ const localVirtualStoreItemNames : string [ ] =
197+ await FileSystem . readFolderItemNamesAsync ( localVirtualStorePath ) ;
198+ const unexpectedLocalPackageFolders : string [ ] = localVirtualStoreItemNames . filter (
199+ ( itemName ) => itemName !== 'lock.yaml' && itemName !== 'node_modules'
200+ ) ;
201+ if ( unexpectedLocalPackageFolders . length > 0 ) {
202+ throw new Error (
203+ `ERROR: Expected ${ localVirtualStorePath } to omit package instance folders, but found: ` +
204+ unexpectedLocalPackageFolders . join ( ', ' )
205+ ) ;
206+ }
207+ }
208+
209+ if ( ! ( await FileSystem . existsAsync ( sharedStorePath ) ) ) {
210+ throw new Error ( `ERROR: Shared PNPM store was not created at ${ sharedStorePath } ` ) ;
211+ }
212+
213+ const sharedStoreItemNames : string [ ] = await FileSystem . readFolderItemNamesAsync ( sharedStorePath ) ;
214+ if ( sharedStoreItemNames . length === 0 ) {
215+ throw new Error ( `ERROR: Shared PNPM store is empty at ${ sharedStorePath } ` ) ;
216+ }
217+
218+ const sharedStoreRealPath : string = await FileSystem . getRealPathAsync ( sharedStorePath ) ;
219+ const localVirtualStoreRealPath : string | undefined = ( await FileSystem . existsAsync (
220+ localVirtualStorePath
221+ ) )
222+ ? await FileSystem . getRealPathAsync ( localVirtualStorePath )
223+ : undefined ;
224+ for ( const externalDependencyPath of externalDependencyPaths ) {
225+ const dependencyPath : string = path . join ( testRepoPath , externalDependencyPath ) ;
226+ const dependencyRealPath : string = await FileSystem . getRealPathAsync ( dependencyPath ) ;
227+ if ( ! Path . isUnderOrEqual ( dependencyRealPath , sharedStoreRealPath ) ) {
228+ throw new Error (
229+ `ERROR: Expected ${ dependencyPath } to resolve under the shared PNPM store.\n` +
230+ `Shared store: ${ sharedStoreRealPath } \n` +
231+ `Actual: ${ dependencyRealPath } `
232+ ) ;
233+ }
234+
235+ if ( localVirtualStoreRealPath && Path . isUnderOrEqual ( dependencyRealPath , localVirtualStoreRealPath ) ) {
236+ throw new Error (
237+ `ERROR: Expected ${ dependencyPath } to resolve outside the local virtual store.\n` +
238+ `Local virtual store: ${ localVirtualStoreRealPath } \n` +
239+ `Actual: ${ dependencyRealPath } `
240+ ) ;
241+ }
242+ }
243+
244+ this . _terminal . writeLine ( '✓ PNPM global virtual store structure verified' ) ;
245+ }
246+
167247 /**
168248 * Verify that build outputs were created
169249 */
0 commit comments