Skip to content

Commit 0d55280

Browse files
committed
fix(@angular/cli): use dedicated cache directory for temporary package installs
Relocates temporary package installations to a dedicated cache directory. This ensures that the project's `.npmrc` is correctly respected, as the installer does relies on the current working directory (CWD) rather than flags like `--prefix`.
1 parent 1944008 commit 0d55280

File tree

4 files changed

+104
-70
lines changed

4 files changed

+104
-70
lines changed

packages/angular/cli/src/commands/add/cli.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
import { assertIsError } from '../../utilities/error';
3434
import { isTTY } from '../../utilities/tty';
3535
import { VERSION } from '../../utilities/version';
36+
import { getCacheConfig } from '../cache/utilities';
3637

3738
class CommandError extends Error {}
3839

@@ -299,7 +300,8 @@ export default class AddCommandModule
299300
task: AddCommandTaskWrapper,
300301
): Promise<void> {
301302
context.packageManager = await createPackageManager({
302-
cwd: this.context.root,
303+
cacheDirectory: getCacheConfig(this.context.workspace).path,
304+
root: this.context.root,
303305
logger: this.context.logger,
304306
dryRun: context.dryRun,
305307
});

packages/angular/cli/src/commands/update/cli.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
getProjectDependencies,
3131
readPackageJson,
3232
} from '../../utilities/package-tree';
33+
import { getCacheConfig } from '../cache/utilities';
3334
import {
3435
checkCLIVersion,
3536
coerceVersionNumber,
@@ -172,7 +173,8 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
172173
const { logger } = this.context;
173174
// Instantiate the package manager
174175
const packageManager = await createPackageManager({
175-
cwd: this.context.root,
176+
root: this.context.root,
177+
cacheDirectory: getCacheConfig(this.context.workspace).path,
176178
logger,
177179
configuredPackageManager: this.context.packageManager.name,
178180
});

packages/angular/cli/src/package-managers/factory.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { major } from 'semver';
1010
import { discover } from './discovery';
11-
import { Host, NodeJS_HOST } from './host';
11+
import { Host, createNodeJsHost } from './host';
1212
import { Logger } from './logger';
1313
import { PackageManager } from './package-manager';
1414
import { PackageManagerName, SUPPORTED_PACKAGE_MANAGERS } from './package-manager-descriptor';
@@ -106,13 +106,14 @@ async function determinePackageManager(
106106
* @returns A promise that resolves to a new `PackageManager` instance.
107107
*/
108108
export async function createPackageManager(options: {
109-
cwd: string;
109+
root: string;
110+
cacheDirectory: string;
110111
configuredPackageManager?: PackageManagerName;
111112
logger?: Logger;
112113
dryRun?: boolean;
113114
}): Promise<PackageManager> {
114-
const { cwd, configuredPackageManager, logger, dryRun } = options;
115-
const host = NodeJS_HOST;
115+
const { root: cwd, cacheDirectory, configuredPackageManager, logger, dryRun } = options;
116+
const host = createNodeJsHost(cwd, cacheDirectory);
116117

117118
const { name, source } = await determinePackageManager(
118119
host,

packages/angular/cli/src/package-managers/host.ts

Lines changed: 93 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
*/
1515

1616
import { type SpawnOptions, spawn } from 'node:child_process';
17-
import { Stats } from 'node:fs';
18-
import { mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
19-
import { platform, tmpdir } from 'node:os';
17+
import { existsSync, Stats } from 'node:fs';
18+
import { copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
19+
import { platform } from 'node:os';
2020
import { join } from 'node:path';
2121
import { PackageManagerError } from './error';
2222

@@ -87,68 +87,97 @@ export interface Host {
8787
}
8888

8989
/**
90-
* A concrete implementation of the `Host` interface that uses the Node.js APIs.
90+
* The package manager configuration files that are copied to the temp directory.
9191
*/
92-
export const NodeJS_HOST: Host = {
93-
stat,
94-
readdir,
95-
readFile: (path: string) => readFile(path, { encoding: 'utf8' }),
96-
writeFile,
97-
createTempDirectory: () => mkdtemp(join(tmpdir(), 'angular-cli-')),
98-
deleteDirectory: (path: string) => rm(path, { recursive: true, force: true }),
99-
runCommand: async (
100-
command: string,
101-
args: readonly string[],
102-
options: {
103-
timeout?: number;
104-
stdio?: 'pipe' | 'ignore';
105-
cwd?: string;
106-
env?: Record<string, string>;
107-
} = {},
108-
): Promise<{ stdout: string; stderr: string }> => {
109-
const signal = options.timeout ? AbortSignal.timeout(options.timeout) : undefined;
110-
const isWin32 = platform() === 'win32';
111-
112-
return new Promise((resolve, reject) => {
113-
const spawnOptions = {
114-
shell: isWin32,
115-
stdio: options.stdio ?? 'pipe',
116-
signal,
117-
cwd: options.cwd,
118-
env: {
119-
...process.env,
120-
...options.env,
121-
},
122-
} satisfies SpawnOptions;
123-
const childProcess = isWin32
124-
? spawn(`${command} ${args.join(' ')}`, spawnOptions)
125-
: spawn(command, args, spawnOptions);
126-
127-
let stdout = '';
128-
childProcess.stdout?.on('data', (data) => (stdout += data.toString()));
129-
130-
let stderr = '';
131-
childProcess.stderr?.on('data', (data) => (stderr += data.toString()));
132-
133-
childProcess.on('close', (code) => {
134-
if (code === 0) {
135-
resolve({ stdout, stderr });
136-
} else {
137-
const message = `Process exited with code ${code}.`;
138-
reject(new PackageManagerError(message, stdout, stderr, code));
139-
}
140-
});
92+
const PACKAGE_MANAGER_CONFIG_FILES = ['.npmrc', '.yarnrc', 'bunfig.toml'];
14193

142-
childProcess.on('error', (err) => {
143-
if (err.name === 'AbortError') {
144-
const message = `Process timed out.`;
94+
/**
95+
* A concrete implementation of the `Host` interface that uses the Node.js APIs.
96+
* @param root The root directory of the project.
97+
* @param cacheDirectory The directory to use for caching.
98+
* @returns A host that uses the Node.js APIs.
99+
*/
100+
export function createNodeJsHost(root: string, cacheDirectory: string): Host {
101+
return {
102+
stat,
103+
readdir,
104+
readFile: (path: string) => readFile(path, { encoding: 'utf8' }),
105+
writeFile,
106+
createTempDirectory: async () => {
107+
await mkdir(cacheDirectory, { recursive: true });
108+
const tmpDir = await mkdtemp(join(cacheDirectory, 'package-manager-tmp-'));
109+
110+
// Copy the .npmrc and .yarnrc files to the temp directory if they exist.
111+
await Promise.all(
112+
PACKAGE_MANAGER_CONFIG_FILES.map((configFile) => {
113+
const sourcePath = join(root, configFile);
114+
const destinationPath = join(tmpDir, configFile);
115+
116+
if (!existsSync(sourcePath)) {
117+
return;
118+
}
119+
120+
return copyFile(sourcePath, destinationPath);
121+
}),
122+
);
123+
124+
return tmpDir;
125+
},
126+
deleteDirectory: (path: string) => rm(path, { recursive: true, force: true }),
127+
runCommand: async (
128+
command: string,
129+
args: readonly string[],
130+
options: {
131+
timeout?: number;
132+
stdio?: 'pipe' | 'ignore';
133+
cwd?: string;
134+
env?: Record<string, string>;
135+
} = {},
136+
): Promise<{ stdout: string; stderr: string }> => {
137+
const signal = options.timeout ? AbortSignal.timeout(options.timeout) : undefined;
138+
const isWin32 = platform() === 'win32';
139+
140+
return new Promise((resolve, reject) => {
141+
const spawnOptions = {
142+
shell: isWin32,
143+
stdio: options.stdio ?? 'pipe',
144+
signal,
145+
cwd: options.cwd,
146+
env: {
147+
...process.env,
148+
...options.env,
149+
},
150+
} satisfies SpawnOptions;
151+
const childProcess = isWin32
152+
? spawn(`${command} ${args.join(' ')}`, spawnOptions)
153+
: spawn(command, args, spawnOptions);
154+
155+
let stdout = '';
156+
childProcess.stdout?.on('data', (data) => (stdout += data.toString()));
157+
158+
let stderr = '';
159+
childProcess.stderr?.on('data', (data) => (stderr += data.toString()));
160+
161+
childProcess.on('close', (code) => {
162+
if (code === 0) {
163+
resolve({ stdout, stderr });
164+
} else {
165+
const message = `Process exited with code ${code}.`;
166+
reject(new PackageManagerError(message, stdout, stderr, code));
167+
}
168+
});
169+
170+
childProcess.on('error', (err) => {
171+
if (err.name === 'AbortError') {
172+
const message = `Process timed out.`;
173+
reject(new PackageManagerError(message, stdout, stderr, null));
174+
175+
return;
176+
}
177+
const message = `Process failed with error: ${err.message}`;
145178
reject(new PackageManagerError(message, stdout, stderr, null));
146-
147-
return;
148-
}
149-
const message = `Process failed with error: ${err.message}`;
150-
reject(new PackageManagerError(message, stdout, stderr, null));
179+
});
151180
});
152-
});
153-
},
154-
};
181+
},
182+
};
183+
}

0 commit comments

Comments
 (0)