Skip to content
Draft
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
82 changes: 53 additions & 29 deletions packages/angular/cli/src/commands/add/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import {
NgAddSaveDependency,
PackageManager,
PackageManagerError,
PackageManifest,
PackageMetadata,
createPackageManager,
Expand Down Expand Up @@ -298,10 +299,22 @@ export default class AddCommandModule
context: AddCommandTaskContext,
task: AddCommandTaskWrapper,
): Promise<void> {
let tempDirectory: string | undefined;
for (const path of ['.angular/cache', 'node_modules']) {
try {
const directory = join(this.context.root, path);
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)}`;
}
Expand Down Expand Up @@ -553,36 +566,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;
}
}

Expand Down
9 changes: 7 additions & 2 deletions packages/angular/cli/src/package-managers/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ export async function createPackageManager(options: {
configuredPackageManager?: PackageManagerName;
logger?: Logger;
dryRun?: boolean;
tempDirectory?: string;
}): Promise<PackageManager> {
const { cwd, configuredPackageManager, logger, dryRun } = options;
const { cwd, configuredPackageManager, logger, dryRun, tempDirectory } = options;
const host = NodeJS_HOST;

const { name, source } = await determinePackageManager(
Expand All @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions packages/angular/cli/src/package-managers/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
createTempDirectory(baseDir?: string): Promise<string>;

/**
* Deletes a directory recursively.
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/angular/cli/src/package-managers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ export interface PackageManagerDescriptor {
/** The flag to ignore peer dependency warnings/errors. */
readonly ignorePeerDependenciesFlag?: string;

/**
* The strategy to use when acquiring a temporary package.
* - `temporary-directory`: The package is installed in a separate temporary directory. (Default)
* - `project-root`: The package is installed in the project root (e.g. `node_modules`), but not saved to `package.json`.
*/
readonly tempPackageStrategy?: 'temporary-directory' | 'project-root';

/** The flag to install a package without saving it to `package.json`. Used with `project-root` strategy. */
readonly noSaveFlag?: string;

/** A function that returns the arguments and environment variables to use a custom registry. */
readonly getRegistryOptions?: (registry: string) => {
args?: string[];
Expand Down Expand Up @@ -141,6 +151,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = {
saveExactFlag: '--save-exact',
saveTildeFlag: '--save-tilde',
saveDevFlag: '--save-dev',
noSaveFlag: '--no-save',
noLockfileFlag: '--no-package-lock',
ignoreScriptsFlag: '--ignore-scripts',
ignorePeerDependenciesFlag: '--force',
Expand Down Expand Up @@ -242,6 +253,8 @@ export const SUPPORTED_PACKAGE_MANAGERS = {
saveExactFlag: '--exact',
saveTildeFlag: '', // Bun does not have a flag for tilde, it defaults to caret.
saveDevFlag: '--development',
noSaveFlag: '--no-save',
tempPackageStrategy: 'project-root',
noLockfileFlag: '', // Bun does not have a flag for this.
ignoreScriptsFlag: '--ignore-scripts',
getRegistryOptions: (registry: string) => ({ args: ['--registry', registry] }),
Expand Down
20 changes: 19 additions & 1 deletion packages/angular/cli/src/package-managers/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -538,7 +544,19 @@ export class PackageManager {
specifier: string,
options: { registry?: string; ignoreScripts?: boolean } = {},
): Promise<{ workingDirectory: string; cleanup: () => Promise<void> }> {
const workingDirectory = await this.host.createTempDirectory();
if (this.descriptor.tempPackageStrategy === 'project-root') {
const flags = [
options.ignoreScripts ? this.descriptor.ignoreScriptsFlag : '',
this.descriptor.noSaveFlag,
].filter((flag): flag is string => !!flag);
const args = [this.descriptor.addCommand, specifier, ...flags];

await this.#run(args, { ...options, cwd: this.cwd });

return { workingDirectory: this.cwd, cleanup: async () => {} };
}

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.
Expand Down
Loading