diff --git a/packages/angular/cli/src/package-managers/package-manager.ts b/packages/angular/cli/src/package-managers/package-manager.ts index 32f63e51deac..234b54851232 100644 --- a/packages/angular/cli/src/package-managers/package-manager.ts +++ b/packages/angular/cli/src/package-managers/package-manager.ts @@ -724,8 +724,15 @@ export class PackageManager { } } + // To prevent Yarn modern from traversing up the directory tree and failing because the temporary + // directory is not part of the project's workspace, write an empty `yarn.lock` to act as a project boundary. + if (this.name === 'yarn') { + await this.host.writeFile(join(workingDirectory, 'yarn.lock'), ''); + } + // Copy configuration files if the package manager requires it (e.g., bun, yarn). if (this.descriptor.copyConfigFromProject) { + let copiedYarnConfig = false; for (const configFile of this.descriptor.configFiles) { try { const configPath = join(this.cwd, configFile); @@ -734,10 +741,20 @@ export class PackageManager { content = sanitizeYarnRc(content); } await this.host.writeFile(join(workingDirectory, configFile), content); + if (this.name === 'yarn') { + copiedYarnConfig = true; + } } catch { // Ignore missing config files. } } + + if (this.name === 'yarn' && !copiedYarnConfig) { + await this.host.writeFile( + join(workingDirectory, '.yarnrc.yml'), + 'nodeLinker: node-modules\n', + ); + } } const flags = [options.ignoreScripts ? this.descriptor.ignoreScriptsFlag : ''].filter( diff --git a/packages/angular/cli/src/package-managers/package-manager_spec.ts b/packages/angular/cli/src/package-managers/package-manager_spec.ts index 83709af7182b..d500a435d0df 100644 --- a/packages/angular/cli/src/package-managers/package-manager_spec.ts +++ b/packages/angular/cli/src/package-managers/package-manager_spec.ts @@ -192,6 +192,10 @@ describe('PackageManager', () => { '/tmp/project/node_modules/angular-cli-tmp-packages-abc/pnpm-workspace.yaml', '', ); + expect(writeFileSpy).not.toHaveBeenCalledWith( + '/tmp/project/node_modules/angular-cli-tmp-packages-abc/yarn.lock', + '', + ); }); it('should copy and sanitize .yarnrc.yml when package manager is yarn and it exists', async () => { @@ -244,6 +248,10 @@ describe('PackageManager', () => { 'nodeLinker: node-modules', ].join('\n'); + expect(writeFileSpy).toHaveBeenCalledWith( + '/tmp/project/node_modules/angular-cli-tmp-packages-abc/yarn.lock', + '', + ); expect(writeFileSpy).toHaveBeenCalledWith( '/tmp/project/node_modules/angular-cli-tmp-packages-abc/.yarnrc.yml', expectedYarnRcContent, @@ -254,6 +262,45 @@ describe('PackageManager', () => { ); }); + it('should write empty yarn.lock and default .yarnrc.yml when package manager is yarn and config does not exist', async () => { + const yarnDescriptor = SUPPORTED_PACKAGE_MANAGERS['yarn']; + const testHost = new MockHost({ + '/tmp/project/node_modules': true, + }); + const pm = new PackageManager(testHost, '/tmp/project', yarnDescriptor); + + 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, 'readFile').and.callFake(async (filePath) => { + if (filePath.replace(/\\/g, '/').endsWith('package.json')) { + return JSON.stringify({ packageManager: 'yarn@4.4.1' }); + } + throw new Error(`ENOENT: no such file or directory, open '${filePath}'`); + }); + spyOn(testHost, 'runCommand').and.resolveTo({ stdout: '4.4.1', stderr: '' }); + + const { workingDirectory } = await pm.acquireTempPackage('foo@1.0.0'); + + 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/yarn.lock', + '', + ); + expect(writeFileSpy).toHaveBeenCalledWith( + '/tmp/project/node_modules/angular-cli-tmp-packages-abc/.yarnrc.yml', + 'nodeLinker: node-modules\n', + ); + expect(writeFileSpy).toHaveBeenCalledWith( + '/tmp/project/node_modules/angular-cli-tmp-packages-abc/package.json', + JSON.stringify({ packageManager: 'yarn@4.4.1' }, null, 2), + ); + }); + it('should copy packageManager field to temp package.json if version is resolved', async () => { const npmDescriptor = SUPPORTED_PACKAGE_MANAGERS['npm']; const testHost = new MockHost({