@angular/cli
Version:
CLI tool for Angular
636 lines • 28.9 kB
JavaScript
;
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const listr2_1 = require("listr2");
const node_assert_1 = __importDefault(require("node:assert"));
const promises_1 = __importDefault(require("node:fs/promises"));
const node_module_1 = require("node:module");
const node_path_1 = require("node:path");
const npm_package_arg_1 = __importDefault(require("npm-package-arg"));
const semver_1 = __importStar(require("semver"));
const schematics_command_module_1 = require("../../command-builder/schematics-command-module");
const package_managers_1 = require("../../package-managers");
const error_1 = require("../../utilities/error");
const tty_1 = require("../../utilities/tty");
const version_1 = require("../../utilities/version");
const utilities_1 = require("../cache/utilities");
class CommandError extends Error {
}
/**
* The set of packages that should have certain versions excluded from consideration
* when attempting to find a compatible version for a package.
* The key is a package name and the value is a SemVer range of versions to exclude.
*/
const packageVersionExclusions = {
// @angular/localize@9.x and earlier versions as well as @angular/localize@10.0 prereleases do not have peer dependencies setup.
'@angular/localize': '<10.0.0',
// @angular/material@7.x versions have unbounded peer dependency ranges (>=7.0.0).
'@angular/material': '7.x',
};
const DEFAULT_CONFLICT_DISPLAY_LIMIT = 5;
/**
* A map of packages to built-in schematics.
* This is used for packages that do not have a native `ng-add` schematic.
*/
const BUILT_IN_SCHEMATICS = {
tailwindcss: {
collection: '@schematics/angular',
name: 'tailwind',
},
};
class AddCommandModule extends schematics_command_module_1.SchematicsCommandModule {
command = 'add <collection>';
describe = 'Adds support for an external library to your project.';
longDescriptionPath = (0, node_path_1.join)(__dirname, 'long-description.md');
allowPrivateSchematics = true;
schematicName = 'ng-add';
rootRequire = (0, node_module_1.createRequire)(this.context.root + '/');
#projectVersionCache = new Map();
async builder(argv) {
const localYargs = (await super.builder(argv))
.positional('collection', {
description: 'The package to be added.',
type: 'string',
demandOption: true,
})
.option('registry', { description: 'The NPM registry to use.', type: 'string' })
.option('verbose', {
description: 'Display additional details about internal operations during execution.',
type: 'boolean',
default: false,
})
.option('skip-confirmation', {
description: 'Skip asking a confirmation prompt before installing and executing the package. ' +
'Ensure package name is correct prior to using this option.',
type: 'boolean',
default: false,
})
// Prior to downloading we don't know the full schema and therefore we cannot be strict on the options.
// Possibly in the future update the logic to use the following syntax:
// `ng add @angular/localize -- --package-options`.
.strict(false);
const collectionName = this.getCollectionName();
if (!collectionName) {
return localYargs;
}
const workflow = this.getOrCreateWorkflowForBuilder(collectionName);
try {
const collection = workflow.engine.createCollection(collectionName);
const options = await this.getSchematicOptions(collection, this.schematicName, workflow);
return this.addSchemaOptionsToCommand(localYargs, options);
}
catch (error) {
// During `ng add` prior to the downloading of the package
// we are not able to resolve and create a collection.
// Or when the collection value is a path to a tarball.
}
return localYargs;
}
async run(options) {
this.#projectVersionCache.clear();
const { logger } = this.context;
const { collection, skipConfirmation } = options;
let packageIdentifier;
try {
packageIdentifier = (0, npm_package_arg_1.default)(collection);
}
catch (e) {
(0, error_1.assertIsError)(e);
logger.error(e.message);
return 1;
}
if (packageIdentifier.name &&
packageIdentifier.registry &&
this.isPackageInstalled(packageIdentifier.name)) {
const validVersion = await this.isProjectVersionValid(packageIdentifier);
if (validVersion) {
// Already installed so just run schematic
logger.info('Skipping installation: Package already installed');
return this.executeSchematic({ ...options, collection: packageIdentifier.name });
}
}
const taskContext = {
packageIdentifier,
executeSchematic: this.executeSchematic.bind(this),
getPeerDependencyConflicts: this.getPeerDependencyConflicts.bind(this),
dryRun: options.dryRun,
};
const tasks = new listr2_1.Listr([
{
title: 'Determining Package Manager',
task: (context, task) => this.determinePackageManagerTask(context, task),
rendererOptions: { persistentOutput: true },
},
{
title: 'Searching for compatible package version',
enabled: packageIdentifier.type === 'range' && packageIdentifier.rawSpec === '*',
task: (context, task) => this.findCompatiblePackageVersionTask(context, task, options),
rendererOptions: { persistentOutput: true },
},
{
title: 'Loading package information',
task: (context, task) => this.loadPackageInfoTask(context, task, options),
rendererOptions: { persistentOutput: true },
},
{
title: 'Confirming installation',
enabled: !skipConfirmation && !options.dryRun,
task: (context, task) => this.confirmInstallationTask(context, task),
rendererOptions: { persistentOutput: true },
},
{
title: 'Installing package',
skip: (context) => {
if (context.dryRun) {
return `Skipping package installation. Would install package ${listr2_1.color.blue(context.packageIdentifier.toString())}.`;
}
return false;
},
task: (context, task) => this.installPackageTask(context, task, options),
rendererOptions: { bottomBar: Infinity },
},
// TODO: Rework schematic execution as a task and insert here
], {
/* options */
});
try {
const result = await tasks.run(taskContext);
(0, node_assert_1.default)(result.collectionName, 'Collection name should always be available');
// Check if the installed package has actual add actions and not just schematic support
if (result.hasSchematics && !options.dryRun) {
const workflow = this.getOrCreateWorkflowForBuilder(result.collectionName);
const collection = workflow.engine.createCollection(result.collectionName);
// listSchematicNames cannot be used here since it does not list private schematics.
// Most `ng-add` schematics are marked as private.
// TODO: Consider adding a `hasSchematic` helper to the schematic collection object.
try {
collection.createSchematic(this.schematicName, true);
}
catch {
result.hasSchematics = false;
}
}
if (!result.hasSchematics) {
// Fallback to a built-in schematic if the package does not have an `ng-add` schematic
const packageName = result.packageIdentifier.name;
if (packageName) {
const builtInSchematic = BUILT_IN_SCHEMATICS[packageName];
if (builtInSchematic) {
logger.info(`The ${listr2_1.color.blue(packageName)} package does not provide \`ng add\` actions.`);
logger.info('The Angular CLI will use built-in actions to add it to your project.');
return this.executeSchematic({
...options,
collection: builtInSchematic.collection,
schematicName: builtInSchematic.name,
});
}
}
let message = options.dryRun
? 'The package does not provide any `ng add` actions, so no further actions would be taken.'
: 'Package installed successfully. The package does not provide any `ng add` actions, so no further actions were taken.';
if (result.homepage) {
message += `\nFor more information about this package, visit its homepage at ${result.homepage}`;
}
logger.info(message);
return;
}
if (options.dryRun) {
logger.info("The package's `ng add` actions would be executed next.");
return;
}
return this.executeSchematic({ ...options, collection: result.collectionName });
}
catch (e) {
if (e instanceof CommandError) {
logger.error(e.message);
return 1;
}
throw e;
}
}
async determinePackageManagerTask(context, task) {
let tempDirectory;
const tempOptions = ['node_modules'];
const cacheConfig = (0, utilities_1.getCacheConfig)(this.context.workspace);
if (cacheConfig.enabled) {
const cachePath = (0, node_path_1.resolve)(this.context.root, cacheConfig.path);
if (!(0, node_path_1.relative)(this.context.root, cachePath).startsWith('..')) {
tempOptions.push(cachePath);
}
}
for (const tempOption of tempOptions) {
try {
const directory = (0, node_path_1.resolve)(this.context.root, tempOption);
if ((await promises_1.default.stat(directory)).isDirectory()) {
tempDirectory = directory;
break;
}
}
catch { }
}
context.packageManager = await (0, package_managers_1.createPackageManager)({
cwd: this.context.root,
logger: this.context.logger,
dryRun: context.dryRun,
tempDirectory,
});
task.output = `Using package manager: ${listr2_1.color.dim(context.packageManager.name)}`;
}
async findCompatiblePackageVersionTask(context, task, options) {
const { registry, verbose } = options;
const { packageManager, packageIdentifier } = context;
const packageName = packageIdentifier.name;
(0, node_assert_1.default)(packageName, 'Registry package identifiers should always have a name.');
const rejectionReasons = [];
// Attempt to use the 'latest' tag from the registry.
try {
const latestManifest = await packageManager.getManifest(`${packageName}@latest`, {
registry,
});
if (latestManifest) {
const conflicts = await this.getPeerDependencyConflicts(latestManifest);
if (!conflicts) {
context.packageIdentifier = npm_package_arg_1.default.resolve(latestManifest.name, latestManifest.version);
task.output = `Found compatible package version: ${listr2_1.color.blue(latestManifest.version)}.`;
return;
}
rejectionReasons.push(...conflicts);
}
}
catch (e) {
(0, error_1.assertIsError)(e);
throw new CommandError(`Unable to load package information from registry: ${e.message}`);
}
// 'latest' is invalid or not found, search for most recent matching package.
task.output =
'Could not find a compatible version with `latest`. Searching for a compatible version.';
let packageMetadata;
try {
packageMetadata = await packageManager.getRegistryMetadata(packageName, {
registry,
});
}
catch (e) {
(0, error_1.assertIsError)(e);
throw new CommandError(`Unable to load package information from registry: ${e.message}`);
}
if (!packageMetadata) {
throw new CommandError('Unable to load package information from registry.');
}
// Allow prelease versions if the CLI itself is a prerelease or locally built.
const allowPrereleases = !!(0, semver_1.prerelease)(version_1.VERSION.full) || version_1.VERSION.full === '0.0.0';
const potentialVersions = this.#getPotentialVersions(packageMetadata, allowPrereleases);
// Heuristic-based search: Check the latest release of each major version first.
const majorVersions = this.#getMajorVersions(potentialVersions);
let found = await this.#findCompatibleVersion(context, majorVersions, {
registry,
verbose,
rejectionReasons,
});
// Exhaustive search: If no compatible major version is found, fall back to checking all versions.
if (!found) {
const checkedVersions = new Set(majorVersions);
const remainingVersions = potentialVersions.filter((v) => !checkedVersions.has(v));
found = await this.#findCompatibleVersion(context, remainingVersions, {
registry,
verbose,
rejectionReasons,
});
}
if (!found) {
let message = `Unable to find compatible package.`;
if (rejectionReasons.length > 0) {
message +=
'\nThis is often because of incompatible peer dependencies.\n' +
'These versions were rejected due to the following conflicts:\n' +
rejectionReasons
.slice(0, verbose ? undefined : DEFAULT_CONFLICT_DISPLAY_LIMIT)
.map((r) => ` - ${r}`)
.join('\n');
}
task.output = message;
}
else {
task.output = `Found compatible package version: ${listr2_1.color.blue(context.packageIdentifier.toString())}.`;
}
}
async #findCompatibleVersion(context, versions, options) {
const { packageManager, packageIdentifier } = context;
const { registry, verbose, rejectionReasons } = options;
const packageName = packageIdentifier.name;
(0, node_assert_1.default)(packageName, 'Package name must be defined.');
for (const version of versions) {
const manifest = await packageManager.getManifest(`${packageName}@${version}`, {
registry,
});
if (!manifest) {
continue;
}
const conflicts = await this.getPeerDependencyConflicts(manifest);
if (conflicts) {
if (verbose || rejectionReasons.length < DEFAULT_CONFLICT_DISPLAY_LIMIT) {
rejectionReasons.push(...conflicts);
}
continue;
}
context.packageIdentifier = npm_package_arg_1.default.resolve(manifest.name, manifest.version);
return manifest;
}
return null;
}
#getPotentialVersions(packageMetadata, allowPrereleases) {
const versionExclusions = packageVersionExclusions[packageMetadata.name];
const latestVersion = packageMetadata['dist-tags']['latest'];
const versions = Object.values(packageMetadata.versions).filter((version) => {
// Latest tag has already been checked
if (latestVersion && version === latestVersion) {
return false;
}
// Prerelease versions are not stable and should not be considered by default
if (!allowPrereleases && (0, semver_1.prerelease)(version)) {
return false;
}
// Excluded package versions should not be considered
if (versionExclusions && (0, semver_1.satisfies)(version, versionExclusions, { includePrerelease: true })) {
return false;
}
return true;
});
// Sort in reverse SemVer order so that the newest compatible version is chosen
return versions.sort((a, b) => (0, semver_1.compare)(b, a, true));
}
#getMajorVersions(versions) {
const majorVersions = new Map();
for (const version of versions) {
const major = semver_1.default.major(version);
const existing = majorVersions.get(major);
if (!existing || semver_1.default.gt(version, existing)) {
majorVersions.set(major, version);
}
}
return [...majorVersions.values()].sort((a, b) => (0, semver_1.compare)(b, a, true));
}
async loadPackageInfoTask(context, task, options) {
const { registry } = options;
let manifest;
try {
manifest = await context.packageManager.getManifest(context.packageIdentifier.toString(), {
registry,
});
}
catch (e) {
(0, error_1.assertIsError)(e);
throw new CommandError(`Unable to fetch package information for '${context.packageIdentifier}': ${e.message}`);
}
if (!manifest) {
throw new CommandError(`Unable to fetch package information for '${context.packageIdentifier}'.`);
}
context.hasSchematics = !!manifest.schematics;
context.savePackage = manifest['ng-add']?.save;
context.collectionName = manifest.name;
context.homepage = manifest.homepage;
if (await this.getPeerDependencyConflicts(manifest)) {
task.output = listr2_1.color.yellow(listr2_1.figures.warning +
' Package has unmet peer dependencies. Adding the package may not succeed.');
}
}
async confirmInstallationTask(context, task) {
if (!(0, tty_1.isTTY)()) {
task.output =
`'--skip-confirmation' can be used to bypass installation confirmation. ` +
`Ensure package name is correct prior to '--skip-confirmation' option usage.`;
throw new CommandError('No terminal detected');
}
const { ListrInquirerPromptAdapter } = await Promise.resolve().then(() => __importStar(require('@listr2/prompt-adapter-inquirer')));
const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
const shouldProceed = await task.prompt(ListrInquirerPromptAdapter).run(confirm, {
message: `The package ${listr2_1.color.blue(context.packageIdentifier.toString())} will be installed and executed.\n` +
'Would you like to proceed?',
default: true,
theme: { prefix: '' },
});
if (!shouldProceed) {
throw new CommandError('Command aborted');
}
}
async installPackageTask(context, task, options) {
const { registry } = options;
const { packageManager, packageIdentifier, savePackage } = context;
// Only show if installation will actually occur
task.title = 'Installing package';
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 = (0, node_module_1.createRequire)(workingDirectory + '/');
(0, node_assert_1.default)(context.collectionName, 'Collection name should always be available');
const resolvedCollectionPath = tempRequire.resolve((0, node_path_1.join)(context.collectionName, 'package.json'));
context.collectionName = (0, node_path_1.dirname)(resolvedCollectionPath);
}
else {
await packageManager.add(packageIdentifier.toString(), 'none', savePackage !== 'dependencies', false, true, {
registry,
});
}
}
catch (e) {
if (e instanceof package_managers_1.PackageManagerError) {
const output = e.stderr || e.stdout;
if (output) {
throw new CommandError(`Package installation failed: ${e.message}\nOutput: ${output}`);
}
}
throw e;
}
}
async isProjectVersionValid(packageIdentifier) {
if (!packageIdentifier.name) {
return false;
}
const installedVersion = await this.findProjectVersion(packageIdentifier.name);
if (!installedVersion) {
return false;
}
if (packageIdentifier.rawSpec === '*') {
return true;
}
if (packageIdentifier.type === 'range' &&
packageIdentifier.fetchSpec &&
packageIdentifier.fetchSpec !== '*') {
return (0, semver_1.satisfies)(installedVersion, packageIdentifier.fetchSpec);
}
if (packageIdentifier.type === 'version') {
const v1 = (0, semver_1.valid)(packageIdentifier.fetchSpec);
const v2 = (0, semver_1.valid)(installedVersion);
return v1 !== null && v1 === v2;
}
return false;
}
getCollectionName() {
const [, collectionName] = this.context.args.positional;
if (!collectionName) {
return undefined;
}
// The CLI argument may specify also a version, like `ng add @my/lib@13.0.0`,
// but here we need only the name of the package, like `@my/lib`.
try {
const packageName = (0, npm_package_arg_1.default)(collectionName).name;
if (packageName) {
return packageName;
}
}
catch (e) {
(0, error_1.assertIsError)(e);
this.context.logger.error(e.message);
}
return collectionName;
}
isPackageInstalled(name) {
try {
this.rootRequire.resolve((0, node_path_1.join)(name, 'package.json'));
return true;
}
catch (e) {
(0, error_1.assertIsError)(e);
if (e.code !== 'MODULE_NOT_FOUND') {
throw e;
}
}
return false;
}
executeSchematic(options) {
const { verbose, skipConfirmation, interactive, force, dryRun, registry, defaults, collection: collectionName, schematicName, ...schematicOptions } = options;
return this.runSchematic({
schematicOptions,
schematicName: schematicName ?? this.schematicName,
collectionName,
executionOptions: {
interactive,
force,
dryRun,
defaults,
packageRegistry: registry,
},
});
}
async findProjectVersion(name) {
const cachedVersion = this.#projectVersionCache.get(name);
if (cachedVersion !== undefined) {
return cachedVersion;
}
const { root } = this.context;
let installedPackagePath;
try {
installedPackagePath = this.rootRequire.resolve((0, node_path_1.join)(name, 'package.json'));
}
catch { }
if (installedPackagePath) {
try {
const installedPackage = JSON.parse(await promises_1.default.readFile(installedPackagePath, 'utf-8'));
this.#projectVersionCache.set(name, installedPackage.version);
return installedPackage.version;
}
catch { }
}
let projectManifest;
try {
projectManifest = JSON.parse(await promises_1.default.readFile((0, node_path_1.join)(root, 'package.json'), 'utf-8'));
}
catch { }
if (projectManifest) {
const version = projectManifest.dependencies?.[name] || projectManifest.devDependencies?.[name];
if (version) {
this.#projectVersionCache.set(name, version);
return version;
}
}
this.#projectVersionCache.set(name, null);
return null;
}
async getPeerDependencyConflicts(manifest) {
if (!manifest.peerDependencies) {
return false;
}
const checks = Object.entries(manifest.peerDependencies).map(async ([peer, range]) => {
let peerIdentifier;
try {
peerIdentifier = npm_package_arg_1.default.resolve(peer, range);
}
catch {
this.context.logger.warn(`Invalid peer dependency ${peer} found in package.`);
return null;
}
if (peerIdentifier.type !== 'version' && peerIdentifier.type !== 'range') {
// type === 'tag' | 'file' | 'directory' | 'remote' | 'git'
// Cannot accurately compare these as the tag/location may have changed since install.
return null;
}
try {
const version = await this.findProjectVersion(peer);
if (!version) {
return null;
}
const options = { includePrerelease: true };
if (!(0, semver_1.intersects)(version, peerIdentifier.rawSpec, options) &&
!(0, semver_1.satisfies)(version, peerIdentifier.rawSpec, options)) {
return (`Package "${manifest.name}@${manifest.version}" has an incompatible peer dependency to "` +
`${peer}@${peerIdentifier.rawSpec}" (requires "${version}" in project).`);
}
}
catch {
// Not found or invalid so ignore
}
return null;
});
const conflicts = (await Promise.all(checks)).filter((result) => !!result);
return conflicts.length > 0 && conflicts;
}
}
exports.default = AddCommandModule;
//# sourceMappingURL=cli.js.map