Skip to content

Commit c703465

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 c703465

File tree

4 files changed

+87
-69
lines changed

4 files changed

+87
-69
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: 76 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515

1616
import { type SpawnOptions, spawn } from 'node:child_process';
1717
import { Stats } from 'node:fs';
18-
import { mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
19-
import { platform, tmpdir } from 'node:os';
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

@@ -88,67 +88,80 @@ export interface Host {
8888

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

0 commit comments

Comments
 (0)