Skip to content
Open
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
7 changes: 7 additions & 0 deletions packages/angular/cli/src/package-managers/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ import { PackageManagerError } from './error';
* An abstraction layer for side-effectful operations.
*/
export interface Host {
/**
* Whether shell quoting is required for package manager specifiers.
* This is typically true on Windows, where commands are executed in a shell.
*/
readonly requiresQuoting?: boolean;

/**
* Creates a directory.
* @param path The path to the directory.
Expand Down Expand Up @@ -101,6 +107,7 @@ export interface Host {
*/
export const NodeJS_HOST: Host = {
stat,
requiresQuoting: platform() === 'win32',
mkdir,
readFile: (path: string) => readFile(path, { encoding: 'utf8' }),
copyFile: (src, dest) => copyFile(src, dest, constants.COPYFILE_FICLONE),
Expand Down
6 changes: 4 additions & 2 deletions packages/angular/cli/src/package-managers/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const METADATA_FIELDS = ['name', 'dist-tags', 'versions', 'time'] as const;
* This is a performance optimization to avoid downloading unnecessary data.
* These fields are the ones required by the CLI for operations like `ng add` and `ng update`.
*/
const MANIFEST_FIELDS = [
export const MANIFEST_FIELDS = [
'name',
'version',
'deprecated',
Expand Down Expand Up @@ -444,7 +444,9 @@ export class PackageManager {
version: string,
options: { timeout?: number; registry?: string; bypassCache?: boolean } = {},
): Promise<PackageManifest | null> {
const specifier = `${packageName}@${version}`;
const specifier = this.host.requiresQuoting
? `"${packageName}@${version}"`
: `${packageName}@${version}`;
const commandArgs = [...this.descriptor.getManifestCommand, specifier];
const formatter = this.descriptor.viewCommandFieldArgFormatter;
if (formatter) {
Expand Down
42 changes: 38 additions & 4 deletions packages/angular/cli/src/package-managers/package-manager_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,54 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { Host } from './host';
import { PackageManager } from './package-manager';
import { MANIFEST_FIELDS, PackageManager } from './package-manager';
import { SUPPORTED_PACKAGE_MANAGERS } from './package-manager-descriptor';
import { MockHost } from './testing/mock-host';

describe('PackageManager', () => {
let host: Host;
let host: MockHost;
let runCommandSpy: jasmine.Spy;
const descriptor = SUPPORTED_PACKAGE_MANAGERS['npm'];

beforeEach(() => {
host = new MockHost();
runCommandSpy = spyOn(host, 'runCommand').and.resolveTo({ stdout: '1.2.3', stderr: '' });
host.runCommand = runCommandSpy;
});

describe('getRegistryManifest', () => {
it('should quote complex range specifiers when required by the host', async () => {
// Simulate a quoting host
Object.assign(host, { requiresQuoting: true });

const pm = new PackageManager(host, '/tmp', descriptor);
const manifest = { name: 'foo', version: '1.0.0' };
runCommandSpy.and.resolveTo({ stdout: JSON.stringify(manifest), stderr: '' });

await pm.getRegistryManifest('foo', '>=1.0.0 <2.0.0');

expect(runCommandSpy).toHaveBeenCalledWith(
descriptor.binary,
[...descriptor.getManifestCommand, '"foo@>=1.0.0 <2.0.0"', ...MANIFEST_FIELDS],
jasmine.anything(),
);
});

it('should NOT quote complex range specifiers when not required by the host', async () => {
// Simulate a non-quoting host
Object.assign(host, { requiresQuoting: false });

const pm = new PackageManager(host, '/tmp', descriptor);
const manifest = { name: 'foo', version: '1.0.0' };
runCommandSpy.and.resolveTo({ stdout: JSON.stringify(manifest), stderr: '' });

await pm.getRegistryManifest('foo', '>=1.0.0 <2.0.0');

expect(runCommandSpy).toHaveBeenCalledWith(
descriptor.binary,
[...descriptor.getManifestCommand, 'foo@>=1.0.0 <2.0.0', ...MANIFEST_FIELDS],
jasmine.anything(),
);
});
});

describe('getVersion', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Host } from '../host';
* This class allows for simulating a file system in memory.
*/
export class MockHost implements Host {
readonly requiresQuoting = false;
private readonly fs = new Map<string, string[] | true>();

constructor(files: Record<string, string[] | true> = {}) {
Expand Down