From 3fc79b2c3d8d49622200ea98d50f8f7ddee6def6 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 9 Jan 2026 10:49:00 -0500 Subject: [PATCH 1/4] refactor(@angular/cli): support custom temporary directory in package manager abstraction This change introduces the ability to specify a base temporary directory when creating a `PackageManager` instance. This allows for creating temporary directories within the project structure (e.g., `node_modules/.tmp`), which enables package managers to correctly inherit project-specific configurations like `.npmrc` or `.yarnrc` during operations like `acquireTempPackage`. --- packages/angular/cli/src/package-managers/factory.ts | 9 +++++++-- packages/angular/cli/src/package-managers/host.ts | 5 +++-- .../angular/cli/src/package-managers/package-manager.ts | 8 +++++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/angular/cli/src/package-managers/factory.ts b/packages/angular/cli/src/package-managers/factory.ts index 19ec32f7f886..1cd3d2462edc 100644 --- a/packages/angular/cli/src/package-managers/factory.ts +++ b/packages/angular/cli/src/package-managers/factory.ts @@ -110,8 +110,9 @@ export async function createPackageManager(options: { configuredPackageManager?: PackageManagerName; logger?: Logger; dryRun?: boolean; + tempDirectory?: string; }): Promise { - const { cwd, configuredPackageManager, logger, dryRun } = options; + const { cwd, configuredPackageManager, logger, dryRun, tempDirectory } = options; const host = NodeJS_HOST; const { name, source } = await determinePackageManager( @@ -127,7 +128,11 @@ export async function createPackageManager(options: { throw new Error(`Unsupported package manager: "${name}"`); } - const packageManager = new PackageManager(host, cwd, descriptor, { dryRun, logger }); + const packageManager = new PackageManager(host, cwd, descriptor, { + dryRun, + logger, + tempDirectory, + }); // Do not verify if the package manager is installed during a dry run. if (!dryRun) { diff --git a/packages/angular/cli/src/package-managers/host.ts b/packages/angular/cli/src/package-managers/host.ts index 82d61031d147..e063a97de03d 100644 --- a/packages/angular/cli/src/package-managers/host.ts +++ b/packages/angular/cli/src/package-managers/host.ts @@ -47,9 +47,10 @@ export interface Host { /** * Creates a new, unique temporary directory. + * @param baseDir The base directory in which to create the temporary directory. * @returns A promise that resolves to the absolute path of the created directory. */ - createTempDirectory(): Promise; + createTempDirectory(baseDir?: string): Promise; /** * Deletes a directory recursively. @@ -94,7 +95,7 @@ export const NodeJS_HOST: Host = { readdir, readFile: (path: string) => readFile(path, { encoding: 'utf8' }), writeFile, - createTempDirectory: () => mkdtemp(join(tmpdir(), 'angular-cli-')), + createTempDirectory: (baseDir?: string) => mkdtemp(join(baseDir ?? tmpdir(), 'angular-cli-')), deleteDirectory: (path: string) => rm(path, { recursive: true, force: true }), runCommand: async ( command: string, diff --git a/packages/angular/cli/src/package-managers/package-manager.ts b/packages/angular/cli/src/package-managers/package-manager.ts index 57b521615273..55444266ffb9 100644 --- a/packages/angular/cli/src/package-managers/package-manager.ts +++ b/packages/angular/cli/src/package-managers/package-manager.ts @@ -59,6 +59,12 @@ export interface PackageManagerOptions { /** A logger instance for debugging and dry run output. */ logger?: Logger; + + /** + * The path to use as the base for temporary directories. + * If not specified, the system's temporary directory will be used. + */ + tempDirectory?: string; } /** @@ -538,7 +544,7 @@ export class PackageManager { specifier: string, options: { registry?: string; ignoreScripts?: boolean } = {}, ): Promise<{ workingDirectory: string; cleanup: () => Promise }> { - const workingDirectory = await this.host.createTempDirectory(); + const workingDirectory = await this.host.createTempDirectory(this.options.tempDirectory); const cleanup = () => this.host.deleteDirectory(workingDirectory); // Some package managers, like yarn classic, do not write a package.json when adding a package. From 2fda2bb3e852bb76f5ba4db06fa655c02fce523f Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:06:56 -0500 Subject: [PATCH 2/4] fix(@angular/cli): use project-local temporary directory in ng add The 'ng add' command now attempts to use '.angular/cache' or 'node_modules' as the base for temporary directories when acquiring packages. This ensures that the package manager can inherit project-specific configuration (like '.npmrc' or '.yarnrc') during the installation of temporary packages. The implementation uses a fallback strategy, checking for candidate directories and defaulting to the system's temporary directory if none are found. --- packages/angular/cli/src/commands/add/cli.ts | 25 +++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/angular/cli/src/commands/add/cli.ts b/packages/angular/cli/src/commands/add/cli.ts index 0dae016fba12..f09ba5610dbd 100644 --- a/packages/angular/cli/src/commands/add/cli.ts +++ b/packages/angular/cli/src/commands/add/cli.ts @@ -10,7 +10,7 @@ import { Listr, ListrRenderer, ListrTaskWrapper, color, figures } from 'listr2'; import assert from 'node:assert'; import fs from 'node:fs/promises'; import { createRequire } from 'node:module'; -import { dirname, join } from 'node:path'; +import { dirname, join, relative, resolve } from 'node:path'; import npa from 'npm-package-arg'; import semver, { Range, compare, intersects, prerelease, satisfies, valid } from 'semver'; import { Argv } from 'yargs'; @@ -33,6 +33,7 @@ import { import { assertIsError } from '../../utilities/error'; import { isTTY } from '../../utilities/tty'; import { VERSION } from '../../utilities/version'; +import { getCacheConfig } from '../cache/utilities'; class CommandError extends Error {} @@ -298,10 +299,32 @@ export default class AddCommandModule context: AddCommandTaskContext, task: AddCommandTaskWrapper, ): Promise { + let tempDirectory: string | undefined; + const tempOptions = ['node_modules']; + + const cacheConfig = getCacheConfig(this.context.workspace); + if (cacheConfig.enabled) { + const cachePath = resolve(this.context.root, cacheConfig.path); + if (!relative(this.context.root, cachePath).startsWith('..')) { + tempOptions.push(cachePath); + } + } + + for (const tempOption of tempOptions) { + try { + const directory = resolve(this.context.root, tempOption); + if ((await fs.stat(directory)).isDirectory()) { + tempDirectory = directory; + break; + } + } catch {} + } + context.packageManager = await createPackageManager({ cwd: this.context.root, logger: this.context.logger, dryRun: context.dryRun, + tempDirectory, }); task.output = `Using package manager: ${color.dim(context.packageManager.name)}`; } From 353d45476f06317c36ef39d204ccaac9b1e69264 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:24:48 -0500 Subject: [PATCH 3/4] refactor(@angular/cli): improve error handling for package installation in ng add This change enhances the error handling in the 'ng add' command's 'installPackageTask'. It now specifically catches 'PackageManagerError' exceptions and surfaces the standard output or standard error from the underlying package manager process. This provides users with more actionable information when a package installation fails. --- packages/angular/cli/src/commands/add/cli.ts | 70 +++++++++++-------- .../angular/cli/src/package-managers/index.ts | 1 + 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/packages/angular/cli/src/commands/add/cli.ts b/packages/angular/cli/src/commands/add/cli.ts index f09ba5610dbd..02bc660b327d 100644 --- a/packages/angular/cli/src/commands/add/cli.ts +++ b/packages/angular/cli/src/commands/add/cli.ts @@ -26,6 +26,7 @@ import { import { NgAddSaveDependency, PackageManager, + PackageManagerError, PackageManifest, PackageMetadata, createPackageManager, @@ -576,36 +577,47 @@ export default class AddCommandModule // Only show if installation will actually occur task.title = 'Installing package'; - if (context.savePackage === false) { - task.title += ' in temporary location'; - - // Temporary packages are located in a different directory - // Hence we need to resolve them using the temp path - const { workingDirectory } = await packageManager.acquireTempPackage( - packageIdentifier.toString(), - { - registry, - }, - ); - - const tempRequire = createRequire(workingDirectory + '/'); - assert(context.collectionName, 'Collection name should always be available'); - const resolvedCollectionPath = tempRequire.resolve( - join(context.collectionName, 'package.json'), - ); + try { + if (context.savePackage === false) { + task.title += ' in temporary location'; + + // Temporary packages are located in a different directory + // Hence we need to resolve them using the temp path + const { workingDirectory } = await packageManager.acquireTempPackage( + packageIdentifier.toString(), + { + registry, + }, + ); + + const tempRequire = createRequire(workingDirectory + '/'); + assert(context.collectionName, 'Collection name should always be available'); + const resolvedCollectionPath = tempRequire.resolve( + join(context.collectionName, 'package.json'), + ); + + context.collectionName = dirname(resolvedCollectionPath); + } else { + await packageManager.add( + packageIdentifier.toString(), + 'none', + savePackage !== 'dependencies', + false, + true, + { + registry, + }, + ); + } + } catch (e) { + if (e instanceof PackageManagerError) { + const output = e.stderr || e.stdout; + if (output) { + throw new CommandError(`Package installation failed: ${e.message}\nOutput: ${output}`); + } + } - context.collectionName = dirname(resolvedCollectionPath); - } else { - await packageManager.add( - packageIdentifier.toString(), - 'none', - savePackage !== 'dependencies', - false, - true, - { - registry, - }, - ); + throw e; } } diff --git a/packages/angular/cli/src/package-managers/index.ts b/packages/angular/cli/src/package-managers/index.ts index 002ade0cdb01..c622539fec2f 100644 --- a/packages/angular/cli/src/package-managers/index.ts +++ b/packages/angular/cli/src/package-managers/index.ts @@ -9,5 +9,6 @@ export { createPackageManager } from './factory'; export type { PackageManagerName } from './package-manager-descriptor'; export { PackageManager } from './package-manager'; +export { PackageManagerError } from './error'; export type * from './package-metadata'; export type { InstalledPackage } from './package-tree'; From 30dafa2c194f6060fe016d902ad20607d41661ee Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:54:51 -0500 Subject: [PATCH 4/4] refactor(@angular/cli): support copying config files to temp package directory This change adds support for copying package manager configuration files (like `bunfig.toml`) from the project root to the temporary directory used for acquiring packages. This is particularly necessary for Bun, which does not automatically inherit configuration from parent directories when running in a separate temporary location. --- packages/angular/cli/src/package-managers/host.ts | 13 +++++++++++-- .../package-manager-descriptor.ts | 15 +++++++++++++++ .../cli/src/package-managers/package-manager.ts | 12 ++++++++++++ .../cli/src/package-managers/testing/mock-host.ts | 4 ++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/angular/cli/src/package-managers/host.ts b/packages/angular/cli/src/package-managers/host.ts index e063a97de03d..4c8744fd8781 100644 --- a/packages/angular/cli/src/package-managers/host.ts +++ b/packages/angular/cli/src/package-managers/host.ts @@ -14,8 +14,8 @@ */ import { type SpawnOptions, spawn } from 'node:child_process'; -import { Stats } from 'node:fs'; -import { mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'; +import { Stats, constants } from 'node:fs'; +import { copyFile, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'; import { platform, tmpdir } from 'node:os'; import { join } from 'node:path'; import { PackageManagerError } from './error'; @@ -45,6 +45,14 @@ export interface Host { */ readFile(path: string): Promise; + /** + * Copies a file from the source path to the destination path. + * @param src The path to the source file. + * @param dest The path to the destination file. + * @returns A promise that resolves when the copy is complete. + */ + copyFile(src: string, dest: string): Promise; + /** * Creates a new, unique temporary directory. * @param baseDir The base directory in which to create the temporary directory. @@ -94,6 +102,7 @@ export const NodeJS_HOST: Host = { stat, readdir, readFile: (path: string) => readFile(path, { encoding: 'utf8' }), + copyFile: (src, dest) => copyFile(src, dest, constants.COPYFILE_FICLONE), writeFile, createTempDirectory: (baseDir?: string) => mkdtemp(join(baseDir ?? tmpdir(), 'angular-cli-')), deleteDirectory: (path: string) => rm(path, { recursive: true, force: true }), diff --git a/packages/angular/cli/src/package-managers/package-manager-descriptor.ts b/packages/angular/cli/src/package-managers/package-manager-descriptor.ts index 631d444db93d..4bcc2f6afeed 100644 --- a/packages/angular/cli/src/package-managers/package-manager-descriptor.ts +++ b/packages/angular/cli/src/package-managers/package-manager-descriptor.ts @@ -65,6 +65,15 @@ export interface PackageManagerDescriptor { /** The flag to ignore peer dependency warnings/errors. */ readonly ignorePeerDependenciesFlag?: string; + /** The configuration files used by the package manager. */ + readonly configFiles: readonly string[]; + + /** + * Whether to copy configuration files from the project root to the temporary directory. + * This is necessary for package managers that do not inherit configuration from parent directories (e.g., bun). + */ + readonly copyConfigFromProject?: boolean; + /** A function that returns the arguments and environment variables to use a custom registry. */ readonly getRegistryOptions?: (registry: string) => { args?: string[]; @@ -144,6 +153,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = { noLockfileFlag: '--no-package-lock', ignoreScriptsFlag: '--ignore-scripts', ignorePeerDependenciesFlag: '--force', + configFiles: ['.npmrc'], getRegistryOptions: (registry: string) => ({ args: ['--registry', registry] }), versionCommand: ['--version'], listDependenciesCommand: ['list', '--depth=0', '--json=true', '--all=true'], @@ -168,6 +178,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = { saveDevFlag: '--dev', noLockfileFlag: '', ignoreScriptsFlag: '--mode=skip-build', + configFiles: ['.yarnrc.yml', '.yarnrc.yaml'], getRegistryOptions: (registry: string) => ({ env: { YARN_NPM_REGISTRY_SERVER: registry } }), versionCommand: ['--version'], listDependenciesCommand: ['list', '--depth=0', '--json', '--recursive=false'], @@ -195,6 +206,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = { saveDevFlag: '--dev', noLockfileFlag: '--no-lockfile', ignoreScriptsFlag: '--ignore-scripts', + configFiles: ['.yarnrc', '.npmrc'], getRegistryOptions: (registry: string) => ({ args: ['--registry', registry] }), versionCommand: ['--version'], listDependenciesCommand: ['list', '--depth=0', '--json'], @@ -220,6 +232,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = { noLockfileFlag: '--no-lockfile', ignoreScriptsFlag: '--ignore-scripts', ignorePeerDependenciesFlag: '--strict-peer-dependencies=false', + configFiles: ['.npmrc', 'pnpm-workspace.yaml'], getRegistryOptions: (registry: string) => ({ args: ['--registry', registry] }), versionCommand: ['--version'], listDependenciesCommand: ['list', '--depth=0', '--json'], @@ -244,6 +257,8 @@ export const SUPPORTED_PACKAGE_MANAGERS = { saveDevFlag: '--development', noLockfileFlag: '', // Bun does not have a flag for this. ignoreScriptsFlag: '--ignore-scripts', + configFiles: ['bunfig.toml', '.npmrc'], + copyConfigFromProject: true, getRegistryOptions: (registry: string) => ({ args: ['--registry', registry] }), versionCommand: ['--version'], listDependenciesCommand: ['pm', 'ls', '--json'], diff --git a/packages/angular/cli/src/package-managers/package-manager.ts b/packages/angular/cli/src/package-managers/package-manager.ts index 55444266ffb9..1faedc5b155e 100644 --- a/packages/angular/cli/src/package-managers/package-manager.ts +++ b/packages/angular/cli/src/package-managers/package-manager.ts @@ -552,6 +552,18 @@ export class PackageManager { // Writing an empty package.json file beforehand prevents this. await this.host.writeFile(join(workingDirectory, 'package.json'), '{}'); + // Copy configuration files if the package manager requires it (e.g., bun). + if (this.descriptor.copyConfigFromProject) { + for (const configFile of this.descriptor.configFiles) { + try { + const configPath = join(this.cwd, configFile); + await this.host.copyFile(configPath, join(workingDirectory, configFile)); + } catch { + // Ignore missing config files. + } + } + } + const flags = [options.ignoreScripts ? this.descriptor.ignoreScriptsFlag : ''].filter( (flag) => flag, ); diff --git a/packages/angular/cli/src/package-managers/testing/mock-host.ts b/packages/angular/cli/src/package-managers/testing/mock-host.ts index af518553a61d..2411c8917318 100644 --- a/packages/angular/cli/src/package-managers/testing/mock-host.ts +++ b/packages/angular/cli/src/package-managers/testing/mock-host.ts @@ -62,4 +62,8 @@ export class MockHost implements Host { readFile(): Promise { throw new Error('Method not implemented.'); } + + copyFile(): Promise { + throw new Error('Method not implemented.'); + } }