Skip to content
Closed
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
4 changes: 3 additions & 1 deletion packages/angular/cli/src/commands/add/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}

Expand Down Expand Up @@ -299,7 +300,8 @@ export default class AddCommandModule
task: AddCommandTaskWrapper,
): Promise<void> {
context.packageManager = await createPackageManager({
cwd: this.context.root,
cacheDirectory: getCacheConfig(this.context.workspace).path,
root: this.context.root,
logger: this.context.logger,
dryRun: context.dryRun,
});
Expand Down
4 changes: 3 additions & 1 deletion packages/angular/cli/src/commands/update/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
getProjectDependencies,
readPackageJson,
} from '../../utilities/package-tree';
import { getCacheConfig } from '../cache/utilities';
import {
checkCLIVersion,
coerceVersionNumber,
Expand Down Expand Up @@ -172,7 +173,8 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
const { logger } = this.context;
// Instantiate the package manager
const packageManager = await createPackageManager({
cwd: this.context.root,
root: this.context.root,
cacheDirectory: getCacheConfig(this.context.workspace).path,
logger,
configuredPackageManager: this.context.packageManager.name,
});
Expand Down
9 changes: 5 additions & 4 deletions packages/angular/cli/src/package-managers/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { major } from 'semver';
import { discover } from './discovery';
import { Host, NodeJS_HOST } from './host';
import { Host, createNodeJsHost } from './host';
import { Logger } from './logger';
import { PackageManager } from './package-manager';
import { PackageManagerName, SUPPORTED_PACKAGE_MANAGERS } from './package-manager-descriptor';
Expand Down Expand Up @@ -106,13 +106,14 @@ async function determinePackageManager(
* @returns A promise that resolves to a new `PackageManager` instance.
*/
export async function createPackageManager(options: {
cwd: string;
root: string;
cacheDirectory: string;
configuredPackageManager?: PackageManagerName;
logger?: Logger;
dryRun?: boolean;
}): Promise<PackageManager> {
const { cwd, configuredPackageManager, logger, dryRun } = options;
const host = NodeJS_HOST;
const { root: cwd, cacheDirectory, configuredPackageManager, logger, dryRun } = options;
const host = createNodeJsHost(cwd, cacheDirectory);

const { name, source } = await determinePackageManager(
host,
Expand Down
155 changes: 91 additions & 64 deletions packages/angular/cli/src/package-managers/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
*/

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 { platform, tmpdir } from 'node:os';
import { Stats, existsSync } from 'node:fs';
import { copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
import { platform } from 'node:os';
import { join } from 'node:path';
import { PackageManagerError } from './error';

Expand Down Expand Up @@ -87,68 +87,95 @@ export interface Host {
}

/**
* A concrete implementation of the `Host` interface that uses the Node.js APIs.
* The package manager configuration files that are copied to the temp directory.
*/
export const NodeJS_HOST: Host = {
stat,
readdir,
readFile: (path: string) => readFile(path, { encoding: 'utf8' }),
writeFile,
createTempDirectory: () => mkdtemp(join(tmpdir(), 'angular-cli-')),
deleteDirectory: (path: string) => rm(path, { recursive: true, force: true }),
runCommand: async (
command: string,
args: readonly string[],
options: {
timeout?: number;
stdio?: 'pipe' | 'ignore';
cwd?: string;
env?: Record<string, string>;
} = {},
): Promise<{ stdout: string; stderr: string }> => {
const signal = options.timeout ? AbortSignal.timeout(options.timeout) : undefined;
const isWin32 = platform() === 'win32';

return new Promise((resolve, reject) => {
const spawnOptions = {
shell: isWin32,
stdio: options.stdio ?? 'pipe',
signal,
cwd: options.cwd,
env: {
...process.env,
...options.env,
},
} satisfies SpawnOptions;
const childProcess = isWin32
? spawn(`${command} ${args.join(' ')}`, spawnOptions)
: spawn(command, args, spawnOptions);

let stdout = '';
childProcess.stdout?.on('data', (data) => (stdout += data.toString()));

let stderr = '';
childProcess.stderr?.on('data', (data) => (stderr += data.toString()));

childProcess.on('close', (code) => {
if (code === 0) {
resolve({ stdout, stderr });
} else {
const message = `Process exited with code ${code}.`;
reject(new PackageManagerError(message, stdout, stderr, code));
}
});
const PACKAGE_MANAGER_CONFIG_FILES = ['.npmrc', 'bunfig.toml'];

childProcess.on('error', (err) => {
if (err.name === 'AbortError') {
const message = `Process timed out.`;
/**
* A concrete implementation of the `Host` interface that uses the Node.js APIs.
* @param root The root directory of the project.
* @param cacheDirectory The directory to use for caching.
* @returns A host that uses the Node.js APIs.
*/
export function createNodeJsHost(root: string, cacheDirectory: string): Host {
return {
stat,
readdir,
readFile: (path: string) => readFile(path, { encoding: 'utf8' }),
writeFile,
createTempDirectory: async () => {
await mkdir(cacheDirectory, { recursive: true });
const tmpDir = await mkdtemp(join(cacheDirectory, 'package-manager-tmp-'));

// Copy the configs as bun does not read files up the tree.
await Promise.all(
PACKAGE_MANAGER_CONFIG_FILES.map((configFile) => {
const sourcePath = join(root, configFile);
const destinationPath = join(tmpDir, configFile);

if (existsSync(sourcePath)) {
return copyFile(sourcePath, destinationPath);
}
}),
);

return tmpDir;
},
deleteDirectory: (path: string) => rm(path, { recursive: true, force: true }),
runCommand: async (
command: string,
args: readonly string[],
options: {
timeout?: number;
stdio?: 'pipe' | 'ignore';
cwd?: string;
env?: Record<string, string>;
} = {},
): Promise<{ stdout: string; stderr: string }> => {
const signal = options.timeout ? AbortSignal.timeout(options.timeout) : undefined;
const isWin32 = platform() === 'win32';

return new Promise((resolve, reject) => {
const spawnOptions = {
shell: isWin32,
stdio: options.stdio ?? 'pipe',
signal,
cwd: options.cwd,
env: {
...process.env,
...options.env,
},
} satisfies SpawnOptions;
const childProcess = isWin32
? spawn(`${command} ${args.join(' ')}`, spawnOptions)
: spawn(command, args, spawnOptions);

let stdout = '';
childProcess.stdout?.on('data', (data) => (stdout += data.toString()));

let stderr = '';
childProcess.stderr?.on('data', (data) => (stderr += data.toString()));

childProcess.on('close', (code) => {
if (code === 0) {
resolve({ stdout, stderr });
} else {
const message = `Process exited with code ${code}.`;
reject(new PackageManagerError(message, stdout, stderr, code));
}
});

childProcess.on('error', (err) => {
if (err.name === 'AbortError') {
const message = `Process timed out.`;
reject(new PackageManagerError(message, stdout, stderr, null));

return;
}
const message = `Process failed with error: ${err.message}`;
reject(new PackageManagerError(message, stdout, stderr, null));

return;
}
const message = `Process failed with error: ${err.message}`;
reject(new PackageManagerError(message, stdout, stderr, null));
});
});
});
},
};
},
};
}
8 changes: 5 additions & 3 deletions packages/angular/cli/src/package-managers/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,9 +546,11 @@ export class PackageManager {
// Writing an empty package.json file beforehand prevents this.
await this.host.writeFile(join(workingDirectory, 'package.json'), '{}');

const flags = [options.ignoreScripts ? this.descriptor.ignoreScriptsFlag : ''].filter(
(flag) => flag,
);
const flags = [];
if (options.ignoreScripts) {
flags.push(this.descriptor.ignoreScriptsFlag);
}

const args: readonly string[] = [this.descriptor.addCommand, specifier, ...flags];

try {
Expand Down
29 changes: 10 additions & 19 deletions tests/e2e/utils/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,30 +70,21 @@ export async function createNpmConfigForAuthentication(
const token = invalidToken ? `invalid=` : VALID_TOKEN;
const registry = (getGlobalVariable('package-secure-registry') as string).replace(/^\w+:/, '');

// `always-auth is required for yarn classic.
// See: https://www.verdaccio.org/docs/setup-yarn#yarn-classic-1x
await writeFile(
'.npmrc',
scopedAuthentication
? `
${registry}/:_auth="${token}"
registry=http:${registry}
`
${registry}/:_auth="${token}"
registry=http:${registry}
always-auth = true
`
: `
_auth="${token}"
registry=http:${registry}
`,
);

await writeFile(
'.yarnrc',
scopedAuthentication
? `
${registry}/:_auth "${token}"
registry http:${registry}
`
: `
_auth "${token}"
registry http:${registry}
`,
_auth="${token}"
registry=http:${registry}
always-auth = true
`,
);
}

Expand Down