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
6 changes: 6 additions & 0 deletions packages/angular/cli/src/package-managers/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,12 @@ export class PackageManager {
// Writing an empty package.json file beforehand prevents this.
await this.host.writeFile(join(workingDirectory, 'package.json'), '{}');

// To prevent pnpm from traversing up the directory tree and modifying the project's workspace lockfile,
// write a blank `pnpm-workspace.yaml` in the temporary directory.
if (this.name === 'pnpm') {
await this.host.writeFile(join(workingDirectory, 'pnpm-workspace.yaml'), '');
}

// Copy configuration files if the package manager requires it (e.g., bun).
if (this.descriptor.copyConfigFromProject) {
for (const configFile of this.descriptor.configFiles) {
Expand Down
52 changes: 52 additions & 0 deletions packages/angular/cli/src/package-managers/package-manager_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,58 @@ describe('PackageManager', () => {
});
});

describe('acquireTempPackage', () => {
it('should write pnpm-workspace.yaml when package manager is pnpm', async () => {
const pnpmDescriptor = SUPPORTED_PACKAGE_MANAGERS['pnpm'];
const testHost = new MockHost({ '/tmp/project/node_modules': true });
const pm = new PackageManager(testHost, '/tmp/project', pnpmDescriptor);

const createTempDirectorySpy = spyOn(testHost, 'createTempDirectory').and.resolveTo(
'/tmp/project/node_modules/angular-cli-tmp-packages-abc',
);
const writeFileSpy = spyOn(testHost, 'writeFile').and.resolveTo();
spyOn(testHost, 'runCommand').and.resolveTo({ stdout: '', stderr: '' });

const { workingDirectory } = await pm.acquireTempPackage('foo@1.0.0');
Comment thread
clydin marked this conversation as resolved.

expect(workingDirectory).toBe('/tmp/project/node_modules/angular-cli-tmp-packages-abc');
expect(createTempDirectorySpy).toHaveBeenCalledWith('/tmp/project/node_modules');
expect(writeFileSpy).toHaveBeenCalledWith(
'/tmp/project/node_modules/angular-cli-tmp-packages-abc/package.json',
'{}',
);
expect(writeFileSpy).toHaveBeenCalledWith(
'/tmp/project/node_modules/angular-cli-tmp-packages-abc/pnpm-workspace.yaml',
'',
);
});

it('should NOT write pnpm-workspace.yaml when package manager is npm', async () => {
const npmDescriptor = SUPPORTED_PACKAGE_MANAGERS['npm'];
const testHost = new MockHost({ '/tmp/project/node_modules': true });
const pm = new PackageManager(testHost, '/tmp/project', npmDescriptor);

const createTempDirectorySpy = spyOn(testHost, 'createTempDirectory').and.resolveTo(
'/tmp/project/node_modules/angular-cli-tmp-packages-abc',
);
const writeFileSpy = spyOn(testHost, 'writeFile').and.resolveTo();
spyOn(testHost, 'runCommand').and.resolveTo({ stdout: '', stderr: '' });

const { workingDirectory } = await pm.acquireTempPackage('foo@1.0.0');
Comment thread
clydin marked this conversation as resolved.

expect(workingDirectory).toBe('/tmp/project/node_modules/angular-cli-tmp-packages-abc');
expect(createTempDirectorySpy).toHaveBeenCalledWith('/tmp/project/node_modules');
expect(writeFileSpy).toHaveBeenCalledWith(
'/tmp/project/node_modules/angular-cli-tmp-packages-abc/package.json',
'{}',
);
expect(writeFileSpy).not.toHaveBeenCalledWith(
'/tmp/project/node_modules/angular-cli-tmp-packages-abc/pnpm-workspace.yaml',
'',
);
});
});

describe('initializationError', () => {
it('should throw initializationError when running commands', async () => {
const error = new Error('Not installed');
Expand Down
21 changes: 15 additions & 6 deletions packages/angular/cli/src/package-managers/testing/mock-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,27 +51,36 @@ export class MockHost implements Host {
} as Stats);
}

runCommand(): Promise<{ stdout: string; stderr: string }> {
runCommand(
command: string,
args: readonly string[],
options?: {
timeout?: number;
stdio?: 'pipe' | 'ignore';
cwd?: string;
env?: Record<string, string>;
},
): Promise<{ stdout: string; stderr: string }> {
throw new Error('Method not implemented.');
}

createTempDirectory(): Promise<string> {
createTempDirectory(baseDir?: string): Promise<string> {
throw new Error('Method not implemented.');
}

deleteDirectory(): Promise<void> {
deleteDirectory(path: string): Promise<void> {
throw new Error('Method not implemented.');
}

writeFile(): Promise<void> {
writeFile(path: string, content: string): Promise<void> {
throw new Error('Method not implemented.');
}

readFile(): Promise<string> {
readFile(path: string): Promise<string> {
throw new Error('Method not implemented.');
}

copyFile(): Promise<void> {
copyFile(src: string, dest: string): Promise<void> {
throw new Error('Method not implemented.');
}
}
33 changes: 33 additions & 0 deletions tests/e2e/tests/update/update-pnpm-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createProjectFromAsset } from '../../utils/assets';
import { readFile, writeFile } from '../../utils/fs';
import { getActivePackageManager } from '../../utils/packages';
import { ng } from '../../utils/process';

export default async function () {
if (getActivePackageManager() !== 'pnpm') {
return;
}

let restoreRegistry: (() => Promise<void>) | undefined;

try {
// Setup project from older asset using the public registry
restoreRegistry = await createProjectFromAsset('20.0-project', true);

// Create pnpm-workspace.yaml inside the project directory
await writeFile('pnpm-workspace.yaml', "packages:\n - '.'\n");

// Run ng update on @angular/cli to trigger the update from version 20 to the next major version
await ng('update', '@angular/cli@21', '@angular/core@21');

// Verify that the pnpm lockfile does not contain references to the temporary package directory
const lockfileContent = await readFile('pnpm-lock.yaml');
if (lockfileContent.includes('angular-cli-tmp-packages-')) {
throw new Error(
'pnpm-lock.yaml contains reference to temporary package directory, isolation failed!',
);
}
} finally {
await restoreRegistry?.();
}
}
Loading