Skip to content
Open
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
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ These GitHub repositories provide supplementary resources for Rush Stack:
| [/build-tests/rush-amazon-s3-build-cache-plugin-integration-test](./build-tests/rush-amazon-s3-build-cache-plugin-integration-test/) | Tests connecting to an amazon S3 endpoint |
| [/build-tests/rush-lib-declaration-paths-test](./build-tests/rush-lib-declaration-paths-test/) | This project ensures all of the paths in rush-lib/lib/... have imports that resolve correctly. If this project builds, all `lib/**/*.d.ts` files in the `@microsoft/rush-lib` package are valid. |
| [/build-tests/rush-mcp-example-plugin](./build-tests/rush-mcp-example-plugin/) | Example showing how to create a plugin for @rushstack/mcp-server |
| [/build-tests/rush-package-manager-integration-test](./build-tests/rush-package-manager-integration-test/) | Integration tests for non-pnpm package managers in Rush. |
| [/build-tests/rush-package-manager-integration-test](./build-tests/rush-package-manager-integration-test/) | Integration tests for package managers in Rush. |
| [/build-tests/rush-project-change-analyzer-test](./build-tests/rush-project-change-analyzer-test/) | This is an example project that uses rush-lib's ProjectChangeAnalyzer to |
| [/build-tests/rush-redis-cobuild-plugin-integration-test](./build-tests/rush-redis-cobuild-plugin-integration-test/) | Tests connecting to an redis server |
| [/build-tests/set-webpack-public-path-plugin-test](./build-tests/set-webpack-public-path-plugin-test/) | Building this project tests the set-webpack-public-path-plugin |
Expand Down Expand Up @@ -258,4 +258,3 @@ provided by the bot. You will only need to do this once across all repos using o
This repo has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

15 changes: 14 additions & 1 deletion build-tests/rush-package-manager-integration-test/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Rush Package Manager Integration Tests

This directory contains integration tests for verifying Rush works correctly with different package managers after the tar 7.x upgrade.
This directory contains integration tests for verifying Rush works correctly with different package managers.

## Background

Expand All @@ -10,6 +10,8 @@ Rush's npm and yarn modes use temp project tarballs (stored in `common/temp/proj

These tests ensure the tar 7.x upgrade works correctly with these workflows.

The PNPM test additionally verifies Rush's workspace install integration with PNPM's global virtual store.

## Tests

The test suite is written in TypeScript using `@rushstack/node-core-library` for cross-platform compatibility.
Expand All @@ -30,6 +32,14 @@ Tests Rush yarn mode by:
- Running `rush install`
- Running `rush build` (verifies everything works end-to-end)

### testPnpmGlobalVirtualStore.ts
Tests Rush pnpm workspace mode with global virtual store by:
- Initializing a Rush repo with `pnpmVersion` configured
- Enabling `useWorkspaces` and `enableGlobalVirtualStore`
- Running `rush update`
- Running `rush install`
- Verifying the generated workspace file, shared PNPM store, dependency links, and build output

## Prerequisites

Before running these tests:
Expand Down Expand Up @@ -62,6 +72,7 @@ These integration tests verify:
- ✓ Tarballs are extracted correctly during `rush install`
- ✓ File permissions are preserved (tar filter function works)
- ✓ Dependencies are linked properly between projects
- ✓ PNPM global virtual store is passed through to a real workspace install
- ✓ The complete workflow (update → install → build) succeeds
- ✓ Built code executes correctly

Expand All @@ -70,6 +81,7 @@ These integration tests verify:
Each test creates a temporary Rush repository in `/tmp/rush-package-manager-test/`:
- `/tmp/rush-package-manager-test/npm-test-repo/` - npm mode test repository
- `/tmp/rush-package-manager-test/yarn-test-repo/` - yarn mode test repository
- `/tmp/rush-package-manager-test/pnpm-global-virtual-store-test-repo/` - pnpm global virtual store test repository

These directories are cleaned up at the start of each test run.

Expand All @@ -86,6 +98,7 @@ The tests use:
The tar library is used in:
- `libraries/rush-lib/src/logic/TempProjectHelper.ts` - Creates tarballs
- `libraries/rush-lib/src/logic/npm/NpmLinkManager.ts` - Extracts tarballs
- `libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts` - Generates PNPM workspace files

## Troubleshooting

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "rush-package-manager-integration-test",
"version": "1.0.0",
"private": true,
"description": "Integration tests for non-pnpm package managers in Rush.",
"description": "Integration tests for package managers in Rush.",
"license": "MIT",
"scripts": {
"_phase:build": "heft build --clean",
Expand Down
105 changes: 100 additions & 5 deletions build-tests/rush-package-manager-integration-test/src/TestHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
import * as path from 'node:path';
import type * as child_process from 'node:child_process';

import { FileSystem, Executable, JsonFile, type JsonObject } from '@rushstack/node-core-library';
import {
FileSystem,
Executable,
JsonFile,
type FileSystemStats,
type JsonObject
} from '@rushstack/node-core-library';
import type { ITerminal } from '@rushstack/terminal';

/**
Expand All @@ -23,14 +29,19 @@ export class TestHelper {
/**
* Execute a Rush command using the locally-built Rush
*/
public async executeRushAsync(args: string[], workingDirectory: string): Promise<void> {
public async executeRushAsync(
args: string[],
workingDirectory: string,
environment?: NodeJS.ProcessEnv
): Promise<void> {
this._terminal.writeLine(`Executing: ${process.argv0} ${this._rushBinPath} ${args.join(' ')}`);

const childProcess: child_process.ChildProcess = Executable.spawn(
process.argv0,
[this._rushBinPath, ...args],
{
currentWorkingDirectory: workingDirectory,
environment,
stdio: 'inherit'
}
);
Expand All @@ -45,7 +56,7 @@ export class TestHelper {
*/
public async createTestRepoAsync(
testRepoPath: string,
packageManagerType: 'npm' | 'yarn',
packageManagerType: 'npm' | 'pnpm' | 'yarn',
packageManagerVersion: string
): Promise<void> {
// Clean up previous test run and create empty test repo directory
Expand All @@ -66,6 +77,10 @@ export class TestHelper {
delete rushJson.pnpmVersion;
delete rushJson.yarnVersion;
rushJson.npmVersion = packageManagerVersion;
} else if (packageManagerType === 'pnpm') {
delete rushJson.npmVersion;
delete rushJson.yarnVersion;
rushJson.pnpmVersion = packageManagerVersion;
} else if (packageManagerType === 'yarn') {
delete rushJson.pnpmVersion;
delete rushJson.npmVersion;
Expand Down Expand Up @@ -151,8 +166,9 @@ export class TestHelper {
// Verify symlinks resolve correctly for local dependencies
if (dep.startsWith('test-project-')) {
const depRealPath: string = await FileSystem.getRealPathAsync(depPath);
const expectedRealPath: string = path.join(testRepoPath, 'projects', dep);
if (depRealPath !== expectedRealPath) {
const expectedPath: string = path.join(testRepoPath, 'projects', dep);
const expectedRealPath: string = await FileSystem.getRealPathAsync(expectedPath);
if (!(await this._doPathsReferToSameObjectAsync(depPath, expectedPath))) {
throw new Error(
`ERROR: Symlink for ${dep} does not resolve correctly!\n` +
`Expected: ${expectedRealPath}\n` +
Expand All @@ -164,6 +180,85 @@ export class TestHelper {
this._terminal.writeLine('✓ Dependencies installed correctly');
}

private async _doPathsReferToSameObjectAsync(path1: string, path2: string): Promise<boolean> {
const path1Stats: FileSystemStats = await FileSystem.getStatisticsAsync(path1);
const path2Stats: FileSystemStats = await FileSystem.getStatisticsAsync(path2);
return path1Stats.dev === path2Stats.dev && path1Stats.ino === path2Stats.ino;
}

/**
* Verify that PNPM's global virtual store was enabled and moved out of the workspace node_modules folder.
*/
public async verifyPnpmGlobalVirtualStoreAsync(
testRepoPath: string,
sharedStorePath: string
): Promise<void> {
this._terminal.writeLine('\nVerifying PNPM global virtual store structure...');

const workspaceFilePath: string = path.join(testRepoPath, 'common/temp/pnpm-workspace.yaml');
const workspaceFileContents: string = await FileSystem.readFileAsync(workspaceFilePath);
if (!workspaceFileContents.includes('enableGlobalVirtualStore: true')) {
throw new Error(`ERROR: enableGlobalVirtualStore was not written to ${workspaceFilePath}`);
}

const localVirtualStorePath: string = path.join(testRepoPath, 'common/temp/node_modules/.pnpm');
if (await FileSystem.existsAsync(localVirtualStorePath)) {
const localVirtualStoreItemNames: string[] =
await FileSystem.readFolderItemNamesAsync(localVirtualStorePath);
const unexpectedLocalPackageFolders: string[] = localVirtualStoreItemNames.filter(
(itemName) => itemName !== 'lock.yaml' && itemName !== 'node_modules'
);
if (unexpectedLocalPackageFolders.length > 0) {
throw new Error(
`ERROR: Expected ${localVirtualStorePath} to omit package instance folders, but found: ` +
unexpectedLocalPackageFolders.join(', ')
);
}
}

if (!(await FileSystem.existsAsync(sharedStorePath))) {
throw new Error(`ERROR: Shared PNPM store was not created at ${sharedStorePath}`);
}

const sharedStoreItemNames: string[] = await FileSystem.readFolderItemNamesAsync(sharedStorePath);
if (sharedStoreItemNames.length === 0) {
throw new Error(`ERROR: Shared PNPM store is empty at ${sharedStorePath}`);
}

const globalVirtualStorePath: string = await this._findPnpmGlobalVirtualStorePathAsync(sharedStorePath);
if (!(await FileSystem.existsAsync(globalVirtualStorePath))) {
throw new Error(
`ERROR: Expected PNPM global virtual store package links under ${sharedStorePath}, ` +
`but ${globalVirtualStorePath} was not found.`
);
}

const globalVirtualStoreItemNames: string[] =
await FileSystem.readFolderItemNamesAsync(globalVirtualStorePath);
if (globalVirtualStoreItemNames.length === 0) {
throw new Error(
`ERROR: PNPM global virtual store package links folder is empty at ${globalVirtualStorePath}`
);
}

this._terminal.writeLine('✓ PNPM global virtual store structure verified');
}

private async _findPnpmGlobalVirtualStorePathAsync(sharedStorePath: string): Promise<string> {
const sharedStoreVersionFolderNames: string[] =
await FileSystem.readFolderItemNamesAsync(sharedStorePath);
for (const folderName of sharedStoreVersionFolderNames) {
if (folderName.startsWith('v')) {
const linksPath: string = path.join(sharedStorePath, folderName, 'links');
if (await FileSystem.existsAsync(linksPath)) {
return linksPath;
}
}
}

return path.join(sharedStorePath, '<version>', 'links');
}

/**
* Verify that build outputs were created
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { Terminal, ConsoleTerminalProvider } from '@rushstack/terminal';

import { testNpmModeAsync } from './testNpmMode';
import { testPnpmGlobalVirtualStoreAsync } from './testPnpmGlobalVirtualStore';
import { testYarnModeAsync } from './testYarnMode';

/**
Expand All @@ -17,7 +18,7 @@ async function runTestsAsync(): Promise<void> {
terminal.writeLine('==========================================');
terminal.writeLine('');
terminal.writeLine('These tests verify that the tar 7.x upgrade works correctly');
terminal.writeLine('with different Rush package managers (npm, yarn).');
terminal.writeLine('with different Rush package managers (npm, pnpm, yarn).');
terminal.writeLine('');
terminal.writeLine('Tests will:');
terminal.writeLine(' 1. Create Rush repos using locally-built Rush');
Expand Down Expand Up @@ -45,6 +46,20 @@ async function runTestsAsync(): Promise<void> {
terminal.writeErrorLine(String(error));
}

// Run pnpm global virtual store test
terminal.writeLine('==========================================');
terminal.writeLine('Running PNPM global virtual store test...');
terminal.writeLine('==========================================');
try {
await testPnpmGlobalVirtualStoreAsync(terminal);
testsPassed++;
} catch (error) {
testsFailed++;
failedTests.push('PNPM global virtual store');
terminal.writeErrorLine('⚠️ PNPM global virtual store test FAILED');
terminal.writeErrorLine(String(error));
}

// Run yarn mode test
terminal.writeLine('==========================================');
terminal.writeLine('Running Yarn mode test...');
Expand Down Expand Up @@ -81,6 +96,7 @@ async function runTestsAsync(): Promise<void> {
terminal.writeLine('');
terminal.writeLine('The tar 7.x upgrade is working correctly with:');
terminal.writeLine(' - NPM package manager');
terminal.writeLine(' - PNPM global virtual store');
terminal.writeLine(' - Yarn package manager');
terminal.writeLine('');
process.exit(0);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import * as os from 'node:os';
import * as path from 'node:path';

import { FileSystem, JsonFile, type JsonObject } from '@rushstack/node-core-library';
import type { ITerminal } from '@rushstack/terminal';

import { TestHelper } from './TestHelper';

/**
* Integration test for Rush PNPM workspace mode with PNPM's global virtual store.
* This test verifies that Rush passes enableGlobalVirtualStore through to a real PNPM install.
*/
export async function testPnpmGlobalVirtualStoreAsync(terminal: ITerminal): Promise<void> {
const helper: TestHelper = new TestHelper(terminal);
// Use system temp directory to avoid rush init detecting parent rush.json
const testRepoPath: string = path.join(
os.tmpdir(),
'rush-package-manager-test',
'pnpm-global-virtual-store-test-repo'
);
const sharedStorePath: string = path.join(os.tmpdir(), 'rush-package-manager-test', 'shared-pnpm-store');
const rushEnvironment: NodeJS.ProcessEnv = {
...process.env,
CI: 'false',
PNPM_CONFIG_CI: 'false',
RUSH_PNPM_STORE_PATH: sharedStorePath
};

terminal.writeLine('==========================================');
terminal.writeLine('Rush PNPM Global Virtual Store Integration Test');
terminal.writeLine('==========================================');
terminal.writeLine('');
terminal.writeLine(
'This test verifies that Rush can enable PNPM global virtual store during workspace installs.'
);
terminal.writeLine('');

await helper.createTestRepoAsync(testRepoPath, 'pnpm', '10.12.1');

const pnpmConfigPath: string = path.join(testRepoPath, 'common/config/rush/pnpm-config.json');
const pnpmConfigJson: JsonObject = await JsonFile.loadAsync(pnpmConfigPath);
pnpmConfigJson.useWorkspaces = true;
pnpmConfigJson.enableGlobalVirtualStore = true;
await JsonFile.saveAsync(pnpmConfigJson, pnpmConfigPath, { updateExistingFile: true });

terminal.writeLine('Creating test-project-a...');
await helper.createTestProjectAsync(
testRepoPath,
'test-project-a',
'1.0.0',
{ semver: '^7.5.4' },
`node -e "const fs = require('fs'); fs.mkdirSync('lib', {recursive: true}); fs.writeFileSync('lib/index.js', 'module.exports = { greet: () => \\"Hello from A\\" };');"`
);

terminal.writeLine('Creating test-project-b...');
await helper.createTestProjectAsync(
testRepoPath,
'test-project-b',
'1.0.0',
{
'test-project-a': 'workspace:*',
moment: '^2.29.4'
},
`node -e "const fs = require('fs'); fs.mkdirSync('lib', {recursive: true}); fs.writeFileSync('lib/index.js', 'module.exports = { test: () => \\"Using: \\" + require(\\'test-project-a\\').greet() };');"`
);

await FileSystem.ensureEmptyFolderAsync(sharedStorePath);

terminal.writeLine('');
terminal.writeLine("Running 'rush update' with PNPM global virtual store enabled...");
await helper.executeRushAsync(['update'], testRepoPath, rushEnvironment);

terminal.writeLine('');
terminal.writeLine("Running 'rush install' with PNPM global virtual store enabled...");
await helper.executeRushAsync(['install'], testRepoPath, rushEnvironment);

await helper.verifyPnpmGlobalVirtualStoreAsync(testRepoPath, sharedStorePath);
await helper.verifyDependenciesAsync(testRepoPath, 'test-project-a', ['semver']);
await helper.verifyDependenciesAsync(testRepoPath, 'test-project-b', ['test-project-a']);

terminal.writeLine('');
terminal.writeLine("Running 'rush build'...");
await helper.executeRushAsync(['build'], testRepoPath, rushEnvironment);

await helper.verifyBuildOutputsAsync(testRepoPath, ['test-project-a', 'test-project-b']);
await helper.testBuiltCodeAsync(testRepoPath, 'test-project-b');

terminal.writeLine('');
terminal.writeLine('==========================================');
terminal.writeLine('✓ PNPM Global Virtual Store Integration Test PASSED');
terminal.writeLine('==========================================');
terminal.writeLine('');
terminal.writeLine('PNPM global virtual store works correctly with Rush workspace installs:');
terminal.writeLine(' - Workspace file includes enableGlobalVirtualStore');
terminal.writeLine(' - Shared PNPM store is populated');
terminal.writeLine(' - Dependencies link and resolve correctly');
terminal.writeLine(' - Build completed successfully');
terminal.writeLine('');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Add PNPM global virtual store support for workspace installs and avoid unnecessary global package manager lock acquisition.",
"type": "minor"
}
],
"packageName": "@microsoft/rush",
"email": "EscapeB@users.noreply.github.com"
}
Loading
Loading