This commit is contained in:
CHEVALLIER Abel
2025-11-13 16:23:22 +01:00
parent de9c515a47
commit cb235644dc
34924 changed files with 3811102 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
/**
* @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
*/
import { Argv } from 'yargs';
import { CommandModuleImplementation, Options, OtherOptions } from '../../command-builder/command-module';
import { SchematicsCommandArgs, SchematicsCommandModule } from '../../command-builder/schematics-command-module';
interface AddCommandArgs extends SchematicsCommandArgs {
collection: string;
verbose?: boolean;
registry?: string;
'skip-confirmation'?: boolean;
}
export default class AddCommandModule extends SchematicsCommandModule implements CommandModuleImplementation<AddCommandArgs> {
command: string;
describe: string;
longDescriptionPath: string;
protected allowPrivateSchematics: boolean;
private readonly schematicName;
private rootRequire;
builder(argv: Argv): Promise<Argv<AddCommandArgs>>;
run(options: Options<AddCommandArgs> & OtherOptions): Promise<number | void>;
private isProjectVersionValid;
private getCollectionName;
private isPackageInstalled;
private executeSchematic;
private findProjectVersion;
private hasMismatchedPeer;
}
export {};
+461
View File
@@ -0,0 +1,461 @@
"use strict";
/**
* @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 tools_1 = require("@angular-devkit/schematics/tools");
const listr2_1 = require("listr2");
const node_assert_1 = __importDefault(require("node:assert"));
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 = require("semver");
const workspace_schema_1 = require("../../../lib/config/workspace-schema");
const schematics_command_module_1 = require("../../command-builder/schematics-command-module");
const error_1 = require("../../utilities/error");
const package_metadata_1 = require("../../utilities/package-metadata");
const tty_1 = require("../../utilities/tty");
const version_1 = require("../../utilities/version");
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',
};
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 + '/');
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;
}
// eslint-disable-next-line max-lines-per-function
async run(options) {
const { logger, packageManager } = this.context;
const { verbose, registry, 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),
hasMismatchedPeer: this.hasMismatchedPeer.bind(this),
};
const tasks = new listr2_1.Listr([
{
title: 'Determining Package Manager',
task(context, task) {
context.usingYarn = packageManager.name === workspace_schema_1.PackageManager.Yarn;
task.output = `Using package manager: ${listr2_1.color.dim(packageManager.name)}`;
},
rendererOptions: { persistentOutput: true },
},
{
title: 'Searching for compatible package version',
enabled: packageIdentifier.type === 'range' && packageIdentifier.rawSpec === '*',
async task(context, task) {
(0, node_assert_1.default)(context.packageIdentifier.name, 'Registry package identifiers should always have a name.');
// only package name provided; search for viable version
// plus special cases for packages that did not have peer deps setup
let packageMetadata;
try {
packageMetadata = await (0, package_metadata_1.fetchPackageMetadata)(context.packageIdentifier.name, logger, {
registry,
usingYarn: context.usingYarn,
verbose,
});
}
catch (e) {
(0, error_1.assertIsError)(e);
throw new CommandError(`Unable to load package information from registry: ${e.message}`);
}
// Start with the version tagged as `latest` if it exists
const latestManifest = packageMetadata.tags['latest'];
if (latestManifest) {
context.packageIdentifier = npm_package_arg_1.default.resolve(latestManifest.name, latestManifest.version);
}
// Adjust the version based on name and peer dependencies
if (latestManifest?.peerDependencies &&
Object.keys(latestManifest.peerDependencies).length === 0) {
task.output = `Found compatible package version: ${listr2_1.color.blue(latestManifest.version)}.`;
}
else if (!latestManifest || (await context.hasMismatchedPeer(latestManifest))) {
// 'latest' is invalid so search for most recent matching package
// Allow prelease versions if the CLI itself is a prerelease
const allowPrereleases = (0, semver_1.prerelease)(version_1.VERSION.full);
const versionExclusions = packageVersionExclusions[packageMetadata.name];
const versionManifests = Object.values(packageMetadata.versions).filter((value) => {
// Prerelease versions are not stable and should not be considered by default
if (!allowPrereleases && (0, semver_1.prerelease)(value.version)) {
return false;
}
// Deprecated versions should not be used or considered
if (value.deprecated) {
return false;
}
// Excluded package versions should not be considered
if (versionExclusions &&
(0, semver_1.satisfies)(value.version, versionExclusions, { includePrerelease: true })) {
return false;
}
return true;
});
// Sort in reverse SemVer order so that the newest compatible version is chosen
versionManifests.sort((a, b) => (0, semver_1.compare)(b.version, a.version, true));
let found = false;
for (const versionManifest of versionManifests) {
const mismatch = await context.hasMismatchedPeer(versionManifest);
if (mismatch) {
continue;
}
context.packageIdentifier = npm_package_arg_1.default.resolve(versionManifest.name, versionManifest.version);
found = true;
break;
}
if (!found) {
task.output = "Unable to find compatible package. Using 'latest' tag.";
}
else {
task.output = `Found compatible package version: ${listr2_1.color.blue(context.packageIdentifier.toString())}.`;
}
}
else {
task.output = `Found compatible package version: ${listr2_1.color.blue(context.packageIdentifier.toString())}.`;
}
},
rendererOptions: { persistentOutput: true },
},
{
title: 'Loading package information from registry',
async task(context, task) {
let manifest;
try {
manifest = await (0, package_metadata_1.fetchPackageManifest)(context.packageIdentifier.toString(), logger, {
registry,
verbose,
usingYarn: context.usingYarn,
});
}
catch (e) {
(0, error_1.assertIsError)(e);
throw new CommandError(`Unable to fetch package information for '${context.packageIdentifier}': ${e.message}`);
}
context.savePackage = manifest['ng-add']?.save;
context.collectionName = manifest.name;
if (await context.hasMismatchedPeer(manifest)) {
task.output = listr2_1.color.yellow(listr2_1.figures.warning +
' Package has unmet peer dependencies. Adding the package may not succeed.');
}
},
rendererOptions: { persistentOutput: true },
},
{
title: 'Confirming installation',
enabled: !skipConfirmation,
async task(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');
}
},
rendererOptions: { persistentOutput: true },
},
{
async task(context, task) {
// 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 { success, tempNodeModules } = await packageManager.installTemp(context.packageIdentifier.toString(), registry ? [`--registry="${registry}"`] : undefined);
const tempRequire = (0, node_module_1.createRequire)(tempNodeModules + '/');
(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'));
if (!success) {
throw new CommandError('Unable to install package');
}
context.collectionName = (0, node_path_1.dirname)(resolvedCollectionPath);
}
else {
const success = await packageManager.install(context.packageIdentifier.toString(), context.savePackage, registry ? [`--registry="${registry}"`] : undefined, undefined);
if (!success) {
throw new CommandError('Unable to install package');
}
}
},
rendererOptions: { bottomBar: Infinity },
},
// TODO: Rework schematic execution as a task and insert here
]);
try {
const result = await tasks.run(taskContext);
(0, node_assert_1.default)(result.collectionName, 'Collection name should always be available');
return this.executeSchematic({ ...options, collection: result.collectionName });
}
catch (e) {
if (e instanceof CommandError) {
return 1;
}
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;
}
async executeSchematic(options) {
try {
const { verbose, skipConfirmation, interactive, force, dryRun, registry, defaults, collection: collectionName, ...schematicOptions } = options;
return await this.runSchematic({
schematicOptions,
schematicName: this.schematicName,
collectionName,
executionOptions: {
interactive,
force,
dryRun,
defaults,
packageRegistry: registry,
},
});
}
catch (e) {
if (e instanceof tools_1.NodePackageDoesNotSupportSchematics) {
this.context.logger.error('The package that you are trying to add does not support schematics.' +
'You can try using a different version of the package or contact the package author to add ng-add support.');
return 1;
}
throw e;
}
}
async findProjectVersion(name) {
const { logger, root } = this.context;
let installedPackage;
try {
installedPackage = this.rootRequire.resolve((0, node_path_1.join)(name, 'package.json'));
}
catch { }
if (installedPackage) {
try {
const installed = await (0, package_metadata_1.fetchPackageManifest)((0, node_path_1.dirname)(installedPackage), logger);
return installed.version;
}
catch { }
}
let projectManifest;
try {
projectManifest = await (0, package_metadata_1.fetchPackageManifest)(root, logger);
}
catch { }
if (projectManifest) {
const version = projectManifest.dependencies?.[name] || projectManifest.devDependencies?.[name];
if (version) {
return version;
}
}
return null;
}
async hasMismatchedPeer(manifest) {
for (const peer in manifest.peerDependencies) {
let peerIdentifier;
try {
peerIdentifier = npm_package_arg_1.default.resolve(peer, manifest.peerDependencies[peer]);
}
catch {
this.context.logger.warn(`Invalid peer dependency ${peer} found in package.`);
continue;
}
if (peerIdentifier.type === 'version' || peerIdentifier.type === 'range') {
try {
const version = await this.findProjectVersion(peer);
if (!version) {
continue;
}
const options = { includePrerelease: true };
if (!(0, semver_1.intersects)(version, peerIdentifier.rawSpec, options) &&
!(0, semver_1.satisfies)(version, peerIdentifier.rawSpec, options)) {
return true;
}
}
catch {
// Not found or invalid so ignore
continue;
}
}
else {
// type === 'tag' | 'file' | 'directory' | 'remote' | 'git'
// Cannot accurately compare these as the tag/location may have changed since install
}
}
return false;
}
}
exports.default = AddCommandModule;
+7
View File
@@ -0,0 +1,7 @@
Adds the npm package for a published library to your workspace, and configures
the project in the current working directory to use that library, as specified by the library's schematic.
For example, adding `@angular/pwa` configures your project for PWA support:
```bash
ng add @angular/pwa
```
+16
View File
@@ -0,0 +1,16 @@
/**
* @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
*/
import { Argv } from 'yargs';
import { CommandModule, CommandModuleImplementation, Options } from '../../command-builder/command-module';
export default class AnalyticsCommandModule extends CommandModule implements CommandModuleImplementation {
command: string;
describe: string;
longDescriptionPath: string;
builder(localYargs: Argv): Argv;
run(_options: Options<{}>): void;
}
+33
View File
@@ -0,0 +1,33 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const node_path_1 = require("node:path");
const command_module_1 = require("../../command-builder/command-module");
const command_1 = require("../../command-builder/utilities/command");
const cli_1 = require("./info/cli");
const cli_2 = require("./settings/cli");
class AnalyticsCommandModule extends command_module_1.CommandModule {
command = 'analytics';
describe = 'Configures the gathering of Angular CLI usage metrics.';
longDescriptionPath = (0, node_path_1.join)(__dirname, 'long-description.md');
builder(localYargs) {
const subcommands = [
cli_1.AnalyticsInfoCommandModule,
cli_2.AnalyticsDisableModule,
cli_2.AnalyticsEnableModule,
cli_2.AnalyticsPromptModule,
].sort(); // sort by class name.
for (const module of subcommands) {
(0, command_1.addCommandModuleToYargs)(module, this.context);
}
return localYargs.demandCommand(1, command_1.demandCommandFailureMessage).strict();
}
run(_options) { }
}
exports.default = AnalyticsCommandModule;
+16
View File
@@ -0,0 +1,16 @@
/**
* @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
*/
import { Argv } from 'yargs';
import { CommandModule, CommandModuleImplementation, Options } from '../../../command-builder/command-module';
export declare class AnalyticsInfoCommandModule extends CommandModule implements CommandModuleImplementation {
command: string;
describe: string;
longDescriptionPath?: string;
builder(localYargs: Argv): Argv;
run(_options: Options<{}>): Promise<void>;
}
+24
View File
@@ -0,0 +1,24 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AnalyticsInfoCommandModule = void 0;
const analytics_1 = require("../../../analytics/analytics");
const command_module_1 = require("../../../command-builder/command-module");
class AnalyticsInfoCommandModule extends command_module_1.CommandModule {
command = 'info';
describe = 'Prints analytics gathering and reporting configuration in the console.';
longDescriptionPath;
builder(localYargs) {
return localYargs.strict();
}
async run(_options) {
this.context.logger.info(await (0, analytics_1.getAnalyticsInfoString)(this.context));
}
}
exports.AnalyticsInfoCommandModule = AnalyticsInfoCommandModule;
+20
View File
@@ -0,0 +1,20 @@
You can help the Angular Team to prioritize features and improvements by permitting the Angular team to send command-line command usage statistics to Google.
The Angular Team does not collect usage statistics unless you explicitly opt in. When installing the Angular CLI you are prompted to allow global collection of usage statistics.
If you say no or skip the prompt, no data is collected.
### What is collected?
Usage analytics include the commands and selected flags for each execution.
Usage analytics may include the following information:
- Your operating system \(macOS, Linux distribution, Windows\) and its version.
- Package manager name and version \(local version only\).
- Node.js version \(local version only\).
- Angular CLI version \(local version only\).
- Command name that was run.
- Workspace information, the number of application and library projects.
- For schematics commands \(add, generate and new\), the schematic collection and name and a list of selected flags.
- For build commands \(build, serve\), the builder name, the number and size of bundles \(initial and lazy\), compilation units, the time it took to build and rebuild, and basic Angular-specific API usage.
Only Angular owned and developed schematics and builders are reported.
Third-party schematics and builders do not send data to the Angular Team.
+35
View File
@@ -0,0 +1,35 @@
/**
* @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
*/
import { Argv } from 'yargs';
import { CommandModule, CommandModuleImplementation, Options } from '../../../command-builder/command-module';
interface AnalyticsCommandArgs {
global: boolean;
}
declare abstract class AnalyticsSettingModule extends CommandModule<AnalyticsCommandArgs> implements CommandModuleImplementation<AnalyticsCommandArgs> {
longDescriptionPath?: string;
builder(localYargs: Argv): Argv<AnalyticsCommandArgs>;
abstract run({ global }: Options<AnalyticsCommandArgs>): Promise<void>;
}
export declare class AnalyticsDisableModule extends AnalyticsSettingModule implements CommandModuleImplementation<AnalyticsCommandArgs> {
command: string;
aliases: string;
describe: string;
run({ global }: Options<AnalyticsCommandArgs>): Promise<void>;
}
export declare class AnalyticsEnableModule extends AnalyticsSettingModule implements CommandModuleImplementation<AnalyticsCommandArgs> {
command: string;
aliases: string;
describe: string;
run({ global }: Options<AnalyticsCommandArgs>): Promise<void>;
}
export declare class AnalyticsPromptModule extends AnalyticsSettingModule implements CommandModuleImplementation<AnalyticsCommandArgs> {
command: string;
describe: string;
run({ global }: Options<AnalyticsCommandArgs>): Promise<void>;
}
export {};
+53
View File
@@ -0,0 +1,53 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AnalyticsPromptModule = exports.AnalyticsEnableModule = exports.AnalyticsDisableModule = void 0;
const analytics_1 = require("../../../analytics/analytics");
const command_module_1 = require("../../../command-builder/command-module");
class AnalyticsSettingModule extends command_module_1.CommandModule {
longDescriptionPath;
builder(localYargs) {
return localYargs
.option('global', {
description: `Configure analytics gathering and reporting globally in the caller's home directory.`,
alias: ['g'],
type: 'boolean',
default: false,
})
.strict();
}
}
class AnalyticsDisableModule extends AnalyticsSettingModule {
command = 'disable';
aliases = 'off';
describe = 'Disables analytics gathering and reporting for the user.';
async run({ global }) {
await (0, analytics_1.setAnalyticsConfig)(global, false);
process.stderr.write(await (0, analytics_1.getAnalyticsInfoString)(this.context));
}
}
exports.AnalyticsDisableModule = AnalyticsDisableModule;
class AnalyticsEnableModule extends AnalyticsSettingModule {
command = 'enable';
aliases = 'on';
describe = 'Enables analytics gathering and reporting for the user.';
async run({ global }) {
await (0, analytics_1.setAnalyticsConfig)(global, true);
process.stderr.write(await (0, analytics_1.getAnalyticsInfoString)(this.context));
}
}
exports.AnalyticsEnableModule = AnalyticsEnableModule;
class AnalyticsPromptModule extends AnalyticsSettingModule {
command = 'prompt';
describe = 'Prompts the user to set the analytics gathering status interactively.';
async run({ global }) {
await (0, analytics_1.promptAnalytics)(this.context, global, true);
}
}
exports.AnalyticsPromptModule = AnalyticsPromptModule;
+16
View File
@@ -0,0 +1,16 @@
/**
* @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
*/
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
import { CommandModuleImplementation } from '../../command-builder/command-module';
export default class BuildCommandModule extends ArchitectCommandModule implements CommandModuleImplementation {
multiTarget: boolean;
command: string;
aliases: string[] | undefined;
describe: string;
longDescriptionPath: string;
}
+20
View File
@@ -0,0 +1,20 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const node_path_1 = require("node:path");
const architect_command_module_1 = require("../../command-builder/architect-command-module");
const command_config_1 = require("../command-config");
class BuildCommandModule extends architect_command_module_1.ArchitectCommandModule {
multiTarget = false;
command = 'build [project]';
aliases = command_config_1.RootCommands['build'].aliases;
describe = 'Compiles an Angular application or library into an output directory named dist/ at the given output path.';
longDescriptionPath = (0, node_path_1.join)(__dirname, 'long-description.md');
}
exports.default = BuildCommandModule;
+18
View File
@@ -0,0 +1,18 @@
The command can be used to build a project of type "application" or "library".
When used to build a library, a different builder is invoked, and only the `ts-config`, `configuration`, `poll` and `watch` options are applied.
All other options apply only to building applications.
The application builder uses the [esbuild](https://esbuild.github.io/) build tool, with default configuration options specified in the workspace configuration file (`angular.json`) or with a named alternative configuration.
A "development" configuration is created by default when you use the CLI to create the project, and you can use that configuration by specifying the `--configuration development`.
The configuration options generally correspond to the command options.
You can override individual configuration defaults by specifying the corresponding options on the command line.
The command can accept option names given in dash-case.
Note that in the configuration file, you must specify names in camelCase.
Some additional options can only be set through the configuration file,
either by direct editing or with the `ng config` command.
These include `assets`, `styles`, and `scripts` objects that provide runtime-global resources to include in the project.
Resources in CSS, such as images and fonts, are automatically written and fingerprinted at the root of the output folder.
For further details, see [Workspace Configuration](reference/configs/workspace-config).
+17
View File
@@ -0,0 +1,17 @@
/**
* @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
*/
import { Argv } from 'yargs';
import { CommandModule, CommandModuleImplementation, CommandScope } from '../../../command-builder/command-module';
export declare class CacheCleanModule extends CommandModule implements CommandModuleImplementation {
command: string;
describe: string;
longDescriptionPath: string | undefined;
scope: CommandScope;
builder(localYargs: Argv): Argv;
run(): Promise<void>;
}
+31
View File
@@ -0,0 +1,31 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CacheCleanModule = void 0;
const promises_1 = require("node:fs/promises");
const command_module_1 = require("../../../command-builder/command-module");
const utilities_1 = require("../utilities");
class CacheCleanModule extends command_module_1.CommandModule {
command = 'clean';
describe = 'Deletes persistent disk cache from disk.';
longDescriptionPath;
scope = command_module_1.CommandScope.In;
builder(localYargs) {
return localYargs.strict();
}
run() {
const { path } = (0, utilities_1.getCacheConfig)(this.context.workspace);
return (0, promises_1.rm)(path, {
force: true,
recursive: true,
maxRetries: 3,
});
}
}
exports.CacheCleanModule = CacheCleanModule;
+17
View File
@@ -0,0 +1,17 @@
/**
* @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
*/
import { Argv } from 'yargs';
import { CommandModule, CommandModuleImplementation, CommandScope, Options } from '../../command-builder/command-module';
export default class CacheCommandModule extends CommandModule implements CommandModuleImplementation {
command: string;
describe: string;
longDescriptionPath: string;
scope: CommandScope;
builder(localYargs: Argv): Argv;
run(_options: Options<{}>): void;
}
+35
View File
@@ -0,0 +1,35 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const node_path_1 = require("node:path");
const command_module_1 = require("../../command-builder/command-module");
const command_1 = require("../../command-builder/utilities/command");
const cli_1 = require("./clean/cli");
const cli_2 = require("./info/cli");
const cli_3 = require("./settings/cli");
class CacheCommandModule extends command_module_1.CommandModule {
command = 'cache';
describe = 'Configure persistent disk cache and retrieve cache statistics.';
longDescriptionPath = (0, node_path_1.join)(__dirname, 'long-description.md');
scope = command_module_1.CommandScope.In;
builder(localYargs) {
const subcommands = [
cli_3.CacheEnableModule,
cli_3.CacheDisableModule,
cli_1.CacheCleanModule,
cli_2.CacheInfoCommandModule,
].sort();
for (const module of subcommands) {
(0, command_1.addCommandModuleToYargs)(module, this.context);
}
return localYargs.demandCommand(1, command_1.demandCommandFailureMessage).strict();
}
run(_options) { }
}
exports.default = CacheCommandModule;
+20
View File
@@ -0,0 +1,20 @@
/**
* @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
*/
import { Argv } from 'yargs';
import { CommandModule, CommandModuleImplementation, CommandScope } from '../../../command-builder/command-module';
export declare class CacheInfoCommandModule extends CommandModule implements CommandModuleImplementation {
command: string;
describe: string;
longDescriptionPath?: string | undefined;
scope: CommandScope;
builder(localYargs: Argv): Argv;
run(): Promise<void>;
private getSizeOfDirectory;
private formatSize;
private effectiveEnabledStatus;
}
+114
View File
@@ -0,0 +1,114 @@
"use strict";
/**
* @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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CacheInfoCommandModule = void 0;
const core_1 = require("@angular-devkit/core");
const fs = __importStar(require("node:fs/promises"));
const node_path_1 = require("node:path");
const command_module_1 = require("../../../command-builder/command-module");
const environment_options_1 = require("../../../utilities/environment-options");
const utilities_1 = require("../utilities");
class CacheInfoCommandModule extends command_module_1.CommandModule {
command = 'info';
describe = 'Prints persistent disk cache configuration and statistics in the console.';
longDescriptionPath;
scope = command_module_1.CommandScope.In;
builder(localYargs) {
return localYargs.strict();
}
async run() {
const { path, environment, enabled } = (0, utilities_1.getCacheConfig)(this.context.workspace);
this.context.logger.info(core_1.tags.stripIndents `
Enabled: ${enabled ? 'yes' : 'no'}
Environment: ${environment}
Path: ${path}
Size on disk: ${await this.getSizeOfDirectory(path)}
Effective status on current machine: ${this.effectiveEnabledStatus() ? 'enabled' : 'disabled'}
`);
}
async getSizeOfDirectory(path) {
const directoriesStack = [path];
let size = 0;
while (directoriesStack.length) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const dirPath = directoriesStack.pop();
let entries = [];
try {
entries = await fs.readdir(dirPath);
}
catch { }
for (const entry of entries) {
const entryPath = (0, node_path_1.join)(dirPath, entry);
const stats = await fs.stat(entryPath);
if (stats.isDirectory()) {
directoriesStack.push(entryPath);
}
size += stats.size;
}
}
return this.formatSize(size);
}
formatSize(size) {
if (size <= 0) {
return '0 bytes';
}
const abbreviations = ['bytes', 'kB', 'MB', 'GB'];
const index = Math.floor(Math.log(size) / Math.log(1024));
const roundedSize = size / Math.pow(1024, index);
// bytes don't have a fraction
const fractionDigits = index === 0 ? 0 : 2;
return `${roundedSize.toFixed(fractionDigits)} ${abbreviations[index]}`;
}
effectiveEnabledStatus() {
const { enabled, environment } = (0, utilities_1.getCacheConfig)(this.context.workspace);
if (enabled) {
switch (environment) {
case 'ci':
return environment_options_1.isCI;
case 'local':
return !environment_options_1.isCI;
}
}
return enabled;
}
}
exports.CacheInfoCommandModule = CacheInfoCommandModule;
+53
View File
@@ -0,0 +1,53 @@
Angular CLI saves a number of cachable operations on disk by default.
When you re-run the same build, the build system restores the state of the previous build and re-uses previously performed operations, which decreases the time taken to build and test your applications and libraries.
To amend the default cache settings, add the `cli.cache` object to your [Workspace Configuration](reference/configs/workspace-config).
The object goes under `cli.cache` at the top level of the file, outside the `projects` sections.
```jsonc
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"cache": {
// ...
},
},
"projects": {},
}
```
For more information, see [cache options](reference/configs/workspace-config#cache-options).
### Cache environments
By default, disk cache is only enabled for local environments. The value of environment can be one of the following:
- `all` - allows disk cache on all machines.
- `local` - allows disk cache only on development machines.
- `ci` - allows disk cache only on continuous integration (CI) systems.
To change the environment setting to `all`, run the following command:
```bash
ng config cli.cache.environment all
```
For more information, see `environment` in [cache options](reference/configs/workspace-config#cache-options).
<div class="alert is-helpful">
The Angular CLI checks for the presence and value of the `CI` environment variable to determine in which environment it is running.
</div>
### Cache path
By default, `.angular/cache` is used as a base directory to store cache results.
To change this path to `.cache/ng`, run the following command:
```bash
ng config cli.cache.path ".cache/ng"
```
+27
View File
@@ -0,0 +1,27 @@
/**
* @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
*/
import { Argv } from 'yargs';
import { CommandModule, CommandModuleImplementation, CommandScope } from '../../../command-builder/command-module';
export declare class CacheDisableModule extends CommandModule implements CommandModuleImplementation {
command: string;
aliases: string;
describe: string;
longDescriptionPath: string | undefined;
scope: CommandScope;
builder(localYargs: Argv): Argv;
run(): Promise<void>;
}
export declare class CacheEnableModule extends CommandModule implements CommandModuleImplementation {
command: string;
aliases: string;
describe: string;
longDescriptionPath: string | undefined;
scope: CommandScope;
builder(localYargs: Argv): Argv;
run(): Promise<void>;
}
+40
View File
@@ -0,0 +1,40 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CacheEnableModule = exports.CacheDisableModule = void 0;
const command_module_1 = require("../../../command-builder/command-module");
const utilities_1 = require("../utilities");
class CacheDisableModule extends command_module_1.CommandModule {
command = 'disable';
aliases = 'off';
describe = 'Disables persistent disk cache for all projects in the workspace.';
longDescriptionPath;
scope = command_module_1.CommandScope.In;
builder(localYargs) {
return localYargs;
}
run() {
return (0, utilities_1.updateCacheConfig)(this.getWorkspaceOrThrow(), 'enabled', false);
}
}
exports.CacheDisableModule = CacheDisableModule;
class CacheEnableModule extends command_module_1.CommandModule {
command = 'enable';
aliases = 'on';
describe = 'Enables disk cache for all projects in the workspace.';
longDescriptionPath;
scope = command_module_1.CommandScope.In;
builder(localYargs) {
return localYargs;
}
run() {
return (0, utilities_1.updateCacheConfig)(this.getWorkspaceOrThrow(), 'enabled', true);
}
}
exports.CacheEnableModule = CacheEnableModule;
+11
View File
@@ -0,0 +1,11 @@
/**
* @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
*/
import { Cache } from '../../../lib/config/workspace-schema';
import { AngularWorkspace } from '../../utilities/config';
export declare function updateCacheConfig<K extends keyof Cache>(workspace: AngularWorkspace, key: K, value: Cache[K]): Promise<void>;
export declare function getCacheConfig(workspace: AngularWorkspace | undefined): Required<Cache>;
+46
View File
@@ -0,0 +1,46 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateCacheConfig = updateCacheConfig;
exports.getCacheConfig = getCacheConfig;
const core_1 = require("@angular-devkit/core");
const node_path_1 = require("node:path");
const workspace_schema_1 = require("../../../lib/config/workspace-schema");
function updateCacheConfig(workspace, key, value) {
const cli = (workspace.extensions['cli'] ??= {});
const cache = (cli['cache'] ??= {});
cache[key] = value;
return workspace.save();
}
function getCacheConfig(workspace) {
if (!workspace) {
throw new Error(`Cannot retrieve cache configuration as workspace is not defined.`);
}
const defaultSettings = {
path: (0, node_path_1.resolve)(workspace.basePath, '.angular/cache'),
environment: workspace_schema_1.Environment.Local,
enabled: true,
};
const cliSetting = workspace.extensions['cli'];
if (!cliSetting || !(0, core_1.isJsonObject)(cliSetting)) {
return defaultSettings;
}
const cacheSettings = cliSetting['cache'];
if (!(0, core_1.isJsonObject)(cacheSettings)) {
return defaultSettings;
}
const { path = defaultSettings.path, environment = defaultSettings.environment, enabled = defaultSettings.enabled,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} = cacheSettings;
return {
path: (0, node_path_1.resolve)(workspace.basePath, path),
environment,
enabled,
};
}
+17
View File
@@ -0,0 +1,17 @@
/**
* @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
*/
import { CommandModuleConstructor } from '../command-builder/utilities/command';
export type CommandNames = 'add' | 'analytics' | 'build' | 'cache' | 'completion' | 'config' | 'deploy' | 'e2e' | 'extract-i18n' | 'generate' | 'lint' | 'make-this-awesome' | 'mcp' | 'new' | 'run' | 'serve' | 'test' | 'update' | 'version';
export interface CommandConfig {
aliases?: string[];
factory: () => Promise<{
default: CommandModuleConstructor;
}>;
}
export declare const RootCommands: Record<CommandNames & string, CommandConfig>;
export declare const RootCommandsAliases: Record<string, CommandConfig>;
+115
View File
@@ -0,0 +1,115 @@
"use strict";
/**
* @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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.RootCommandsAliases = exports.RootCommands = void 0;
exports.RootCommands = {
'add': {
factory: () => Promise.resolve().then(() => __importStar(require('./add/cli'))),
},
'analytics': {
factory: () => Promise.resolve().then(() => __importStar(require('./analytics/cli'))),
},
'build': {
factory: () => Promise.resolve().then(() => __importStar(require('./build/cli'))),
aliases: ['b'],
},
'cache': {
factory: () => Promise.resolve().then(() => __importStar(require('./cache/cli'))),
},
'completion': {
factory: () => Promise.resolve().then(() => __importStar(require('./completion/cli'))),
},
'config': {
factory: () => Promise.resolve().then(() => __importStar(require('./config/cli'))),
},
'deploy': {
factory: () => Promise.resolve().then(() => __importStar(require('./deploy/cli'))),
},
'e2e': {
factory: () => Promise.resolve().then(() => __importStar(require('./e2e/cli'))),
aliases: ['e'],
},
'extract-i18n': {
factory: () => Promise.resolve().then(() => __importStar(require('./extract-i18n/cli'))),
},
'generate': {
factory: () => Promise.resolve().then(() => __importStar(require('./generate/cli'))),
aliases: ['g'],
},
'lint': {
factory: () => Promise.resolve().then(() => __importStar(require('./lint/cli'))),
},
'make-this-awesome': {
factory: () => Promise.resolve().then(() => __importStar(require('./make-this-awesome/cli'))),
},
'mcp': {
factory: () => Promise.resolve().then(() => __importStar(require('./mcp/cli'))),
},
'new': {
factory: () => Promise.resolve().then(() => __importStar(require('./new/cli'))),
aliases: ['n'],
},
'run': {
factory: () => Promise.resolve().then(() => __importStar(require('./run/cli'))),
},
'serve': {
factory: () => Promise.resolve().then(() => __importStar(require('./serve/cli'))),
aliases: ['dev', 's'],
},
'test': {
factory: () => Promise.resolve().then(() => __importStar(require('./test/cli'))),
aliases: ['t'],
},
'update': {
factory: () => Promise.resolve().then(() => __importStar(require('./update/cli'))),
},
'version': {
factory: () => Promise.resolve().then(() => __importStar(require('./version/cli'))),
aliases: ['v'],
},
};
exports.RootCommandsAliases = Object.values(exports.RootCommands).reduce((prev, current) => {
current.aliases?.forEach((alias) => {
prev[alias] = current;
});
return prev;
}, {});
+16
View File
@@ -0,0 +1,16 @@
/**
* @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
*/
import { Argv } from 'yargs';
import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module';
export default class CompletionCommandModule extends CommandModule implements CommandModuleImplementation {
command: string;
describe: string;
longDescriptionPath: string;
builder(localYargs: Argv): Argv;
run(): Promise<number>;
}
+60
View File
@@ -0,0 +1,60 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const node_path_1 = require("node:path");
const command_module_1 = require("../../command-builder/command-module");
const command_1 = require("../../command-builder/utilities/command");
const color_1 = require("../../utilities/color");
const completion_1 = require("../../utilities/completion");
const error_1 = require("../../utilities/error");
class CompletionCommandModule extends command_module_1.CommandModule {
command = 'completion';
describe = 'Set up Angular CLI autocompletion for your terminal.';
longDescriptionPath = (0, node_path_1.join)(__dirname, 'long-description.md');
builder(localYargs) {
(0, command_1.addCommandModuleToYargs)(CompletionScriptCommandModule, this.context);
return localYargs;
}
async run() {
let rcFile;
try {
rcFile = await (0, completion_1.initializeAutocomplete)();
}
catch (err) {
(0, error_1.assertIsError)(err);
this.context.logger.error(err.message);
return 1;
}
this.context.logger.info(`
Appended \`source <(ng completion script)\` to \`${rcFile}\`. Restart your terminal or run the following to autocomplete \`ng\` commands:
${color_1.colors.yellow('source <(ng completion script)')}
`.trim());
if ((await (0, completion_1.hasGlobalCliInstall)()) === false) {
this.context.logger.warn('Setup completed successfully, but there does not seem to be a global install of the' +
' Angular CLI. For autocompletion to work, the CLI will need to be on your `$PATH`, which' +
' is typically done with the `-g` flag in `npm install -g @angular/cli`.' +
'\n\n' +
'For more information, see https://angular.dev/cli/completion#global-install');
}
return 0;
}
}
exports.default = CompletionCommandModule;
class CompletionScriptCommandModule extends command_module_1.CommandModule {
command = 'script';
describe = 'Generate a bash and zsh real-time type-ahead autocompletion script.';
longDescriptionPath = undefined;
builder(localYargs) {
return localYargs;
}
run() {
this.context.yargsInstance.showCompletionScript();
}
}
+67
View File
@@ -0,0 +1,67 @@
Setting up autocompletion configures your terminal, so pressing the `<TAB>` key while in the middle
of typing will display various commands and options available to you. This makes it very easy to
discover and use CLI commands without lots of memorization.
![A demo of Angular CLI autocompletion in a terminal. The user types several partial `ng` commands,
using autocompletion to finish several arguments and list contextual options.
](assets/images/guide/cli/completion.gif)
## Automated setup
The CLI should prompt and ask to set up autocompletion for you the first time you use it (v14+).
Simply answer "Yes" and the CLI will take care of the rest.
```
$ ng serve
? Would you like to enable autocompletion? This will set up your terminal so pressing TAB while typing Angular CLI commands will show possible options and autocomplete arguments. (Enabling autocompletion will modify configuration files in your home directory.) Yes
Appended `source <(ng completion script)` to `/home/my-username/.bashrc`. Restart your terminal or run:
source <(ng completion script)
to autocomplete `ng` commands.
# Serve output...
```
If you already refused the prompt, it won't ask again. But you can run `ng completion` to
do the same thing automatically.
This modifies your terminal environment to load Angular CLI autocompletion, but can't update your
current terminal session. Either restart it or run `source <(ng completion script)` directly to
enable autocompletion in your current session.
Test it out by typing `ng ser<TAB>` and it should autocomplete to `ng serve`. Ambiguous arguments
will show all possible options and their documentation, such as `ng generate <TAB>`.
## Manual setup
Some users may have highly customized terminal setups, possibly with configuration files checked
into source control with an opinionated structure. `ng completion` only ever appends Angular's setup
to an existing configuration file for your current shell, or creates one if none exists. If you want
more control over exactly where this configuration lives, you can manually set it up by having your
shell run at startup:
```bash
source <(ng completion script)
```
This is equivalent to what `ng completion` will automatically set up, and gives power users more
flexibility in their environments when desired.
## Platform support
Angular CLI supports autocompletion for the Bash and Zsh shells on MacOS and Linux operating
systems. On Windows, Git Bash and [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/)
using Bash or Zsh are supported.
## Global install
Autocompletion works by configuring your terminal to invoke the Angular CLI on startup to load the
setup script. This means the terminal must be able to find and execute the Angular CLI, typically
through a global install that places the binary on the user's `$PATH`. If you get
`command not found: ng`, make sure the CLI is installed globally which you can do with the `-g`
flag:
```bash
npm install -g @angular/cli
```
+24
View File
@@ -0,0 +1,24 @@
/**
* @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
*/
import { Argv } from 'yargs';
import { CommandModule, CommandModuleImplementation, Options } from '../../command-builder/command-module';
interface ConfigCommandArgs {
'json-path'?: string;
value?: string;
global?: boolean;
}
export default class ConfigCommandModule extends CommandModule<ConfigCommandArgs> implements CommandModuleImplementation<ConfigCommandArgs> {
command: string;
describe: string;
longDescriptionPath: string;
builder(localYargs: Argv): Argv<ConfigCommandArgs>;
run(options: Options<ConfigCommandArgs>): Promise<number | void>;
private get;
private set;
}
export {};
+149
View File
@@ -0,0 +1,149 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const node_crypto_1 = require("node:crypto");
const node_path_1 = require("node:path");
const command_module_1 = require("../../command-builder/command-module");
const config_1 = require("../../utilities/config");
const json_file_1 = require("../../utilities/json-file");
class ConfigCommandModule extends command_module_1.CommandModule {
command = 'config [json-path] [value]';
describe = 'Retrieves or sets Angular configuration values in the angular.json file for the workspace.';
longDescriptionPath = (0, node_path_1.join)(__dirname, 'long-description.md');
builder(localYargs) {
return localYargs
.positional('json-path', {
description: `The configuration key to set or query, in JSON path format. ` +
`For example: "a[3].foo.bar[2]". If no new value is provided, returns the current value of this key.`,
type: 'string',
})
.positional('value', {
description: 'If provided, a new value for the given configuration key.',
type: 'string',
})
.option('global', {
description: `Access the global configuration in the caller's home directory.`,
alias: ['g'],
type: 'boolean',
default: false,
})
.strict();
}
async run(options) {
const level = options.global ? 'global' : 'local';
const [config] = await (0, config_1.getWorkspaceRaw)(level);
if (options.value == undefined) {
if (!config) {
this.context.logger.error('No config found.');
return 1;
}
return this.get(config, options);
}
else {
return this.set(options);
}
}
get(jsonFile, options) {
const { logger } = this.context;
const value = options.jsonPath
? jsonFile.get(parseJsonPath(options.jsonPath))
: jsonFile.content;
if (value === undefined) {
logger.error('Value cannot be found.');
return 1;
}
else if (typeof value === 'string') {
logger.info(value);
}
else {
logger.info(JSON.stringify(value, null, 2));
}
return 0;
}
async set(options) {
if (!options.jsonPath?.trim()) {
throw new command_module_1.CommandModuleError('Invalid Path.');
}
const [config, configPath] = await (0, config_1.getWorkspaceRaw)(options.global ? 'global' : 'local');
const { logger } = this.context;
if (!config || !configPath) {
throw new command_module_1.CommandModuleError('Confguration file cannot be found.');
}
const normalizeUUIDValue = (v) => (v === '' ? (0, node_crypto_1.randomUUID)() : `${v}`);
const value = options.jsonPath === 'cli.analyticsSharing.uuid'
? normalizeUUIDValue(options.value)
: options.value;
const modified = config.modify(parseJsonPath(options.jsonPath), normalizeValue(value));
if (!modified) {
logger.error('Value cannot be found.');
return 1;
}
await (0, config_1.validateWorkspace)((0, json_file_1.parseJson)(config.content), options.global ?? false);
config.save();
return 0;
}
}
exports.default = ConfigCommandModule;
/**
* Splits a JSON path string into fragments. Fragments can be used to get the value referenced
* by the path. For example, a path of "a[3].foo.bar[2]" would give you a fragment array of
* ["a", 3, "foo", "bar", 2].
* @param path The JSON string to parse.
* @returns {(string|number)[]} The fragments for the string.
* @private
*/
function parseJsonPath(path) {
const fragments = (path || '').split(/\./g);
const result = [];
while (fragments.length > 0) {
const fragment = fragments.shift();
if (fragment == undefined) {
break;
}
const match = fragment.match(/([^[]+)((\[.*\])*)/);
if (!match) {
throw new command_module_1.CommandModuleError('Invalid JSON path.');
}
result.push(match[1]);
if (match[2]) {
const indices = match[2]
.slice(1, -1)
.split('][')
.map((x) => (/^\d$/.test(x) ? +x : x.replace(/"|'/g, '')));
result.push(...indices);
}
}
return result.filter((fragment) => fragment != null);
}
function normalizeValue(value) {
const valueString = `${value}`.trim();
switch (valueString) {
case 'true':
return true;
case 'false':
return false;
case 'null':
return null;
case 'undefined':
return undefined;
}
if (isFinite(+valueString)) {
return +valueString;
}
try {
// We use `JSON.parse` instead of `parseJson` because the latter will parse UUIDs
// and convert them into a numberic entities.
// Example: 73b61974-182c-48e4-b4c6-30ddf08c5c98 -> 73.
// These values should never contain comments, therefore using `JSON.parse` is safe.
return JSON.parse(valueString);
}
catch {
return value;
}
}
+13
View File
@@ -0,0 +1,13 @@
A workspace has a single CLI configuration file, `angular.json`, at the top level.
The `projects` object contains a configuration object for each project in the workspace.
You can edit the configuration directly in a code editor,
or indirectly on the command line using this command.
The configurable property names match command option names,
except that in the configuration file, all names must use camelCase,
while on the command line options can be given dash-case.
For further details, see [Workspace Configuration](reference/configs/workspace-config).
For configuration of CLI usage analytics, see [ng analytics](cli/analytics).
+17
View File
@@ -0,0 +1,17 @@
/**
* @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
*/
import { MissingTargetChoice } from '../../command-builder/architect-base-command-module';
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
import { CommandModuleImplementation } from '../../command-builder/command-module';
export default class DeployCommandModule extends ArchitectCommandModule implements CommandModuleImplementation {
missingTargetChoices: MissingTargetChoice[];
multiTarget: boolean;
command: string;
longDescriptionPath: string;
describe: string;
}
+37
View File
@@ -0,0 +1,37 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const node_path_1 = require("node:path");
const architect_command_module_1 = require("../../command-builder/architect-command-module");
class DeployCommandModule extends architect_command_module_1.ArchitectCommandModule {
// The below choices should be kept in sync with the list in https://angular.dev/tools/cli/deployment
missingTargetChoices = [
{
name: 'Amazon S3',
value: '@jefiozie/ngx-aws-deploy',
},
{
name: 'Firebase',
value: '@angular/fire',
},
{
name: 'Netlify',
value: '@netlify-builder/deploy',
},
{
name: 'GitHub Pages',
value: 'angular-cli-ghpages',
},
];
multiTarget = false;
command = 'deploy [project]';
longDescriptionPath = (0, node_path_1.join)(__dirname, 'long-description.md');
describe = 'Invokes the deploy builder for a specified project or for the default project in the workspace.';
}
exports.default = DeployCommandModule;
+22
View File
@@ -0,0 +1,22 @@
The command takes an optional project name, as specified in the `projects` section of the `angular.json` workspace configuration file.
When a project name is not supplied, executes the `deploy` builder for the default project.
To use the `ng deploy` command, use `ng add` to add a package that implements deployment capabilities to your favorite platform.
Adding the package automatically updates your workspace configuration, adding a deployment
[CLI builder](tools/cli/cli-builder).
For example:
```json
"projects": {
"my-project": {
...
"architect": {
...
"deploy": {
"builder": "@angular/fire:deploy",
"options": {}
}
}
}
}
```
+18
View File
@@ -0,0 +1,18 @@
/**
* @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
*/
import { MissingTargetChoice } from '../../command-builder/architect-base-command-module';
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
import { CommandModuleImplementation } from '../../command-builder/command-module';
export default class E2eCommandModule extends ArchitectCommandModule implements CommandModuleImplementation {
missingTargetChoices: MissingTargetChoice[];
multiTarget: boolean;
command: string;
aliases: string[] | undefined;
describe: string;
longDescriptionPath?: string;
}
+41
View File
@@ -0,0 +1,41 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const architect_command_module_1 = require("../../command-builder/architect-command-module");
const command_config_1 = require("../command-config");
class E2eCommandModule extends architect_command_module_1.ArchitectCommandModule {
missingTargetChoices = [
{
name: 'Playwright',
value: 'playwright-ng-schematics',
},
{
name: 'Cypress',
value: '@cypress/schematic',
},
{
name: 'Nightwatch',
value: '@nightwatch/schematics',
},
{
name: 'WebdriverIO',
value: '@wdio/schematics',
},
{
name: 'Puppeteer',
value: '@puppeteer/ng-schematics',
},
];
multiTarget = true;
command = 'e2e [project]';
aliases = command_config_1.RootCommands['e2e'].aliases;
describe = 'Builds and serves an Angular application, then runs end-to-end tests.';
longDescriptionPath;
}
exports.default = E2eCommandModule;
+17
View File
@@ -0,0 +1,17 @@
/**
* @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
*/
import { workspaces } from '@angular-devkit/core';
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
import { CommandModuleImplementation } from '../../command-builder/command-module';
export default class ExtractI18nCommandModule extends ArchitectCommandModule implements CommandModuleImplementation {
multiTarget: boolean;
command: string;
describe: string;
longDescriptionPath?: string | undefined;
findDefaultBuilderName(project: workspaces.ProjectDefinition): Promise<string | undefined>;
}
+47
View File
@@ -0,0 +1,47 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const node_module_1 = require("node:module");
const node_path_1 = require("node:path");
const architect_command_module_1 = require("../../command-builder/architect-command-module");
class ExtractI18nCommandModule extends architect_command_module_1.ArchitectCommandModule {
multiTarget = false;
command = 'extract-i18n [project]';
describe = 'Extracts i18n messages from source code.';
longDescriptionPath;
async findDefaultBuilderName(project) {
// Only application type projects have a default i18n extraction target
if (project.extensions['projectType'] !== 'application') {
return;
}
const buildTarget = project.targets.get('build');
if (!buildTarget) {
// No default if there is no build target
return;
}
// Provide a default based on the defined builder for the 'build' target
switch (buildTarget.builder) {
case '@angular-devkit/build-angular:application':
case '@angular-devkit/build-angular:browser-esbuild':
case '@angular-devkit/build-angular:browser':
return '@angular-devkit/build-angular:extract-i18n';
case '@angular/build:application':
return '@angular/build:extract-i18n';
}
// For other builders, check for `@angular-devkit/build-angular` and use if found.
// This package is safer to use since it supports both application builder types.
try {
const projectRequire = (0, node_module_1.createRequire)((0, node_path_1.join)(this.context.root, project.root) + '/');
projectRequire.resolve('@angular-devkit/build-angular');
return '@angular-devkit/build-angular:extract-i18n';
}
catch { }
}
}
exports.default = ExtractI18nCommandModule;
+47
View File
@@ -0,0 +1,47 @@
/**
* @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
*/
import { Argv } from 'yargs';
import { CommandModuleImplementation, Options, OtherOptions } from '../../command-builder/command-module';
import { SchematicsCommandArgs, SchematicsCommandModule } from '../../command-builder/schematics-command-module';
interface GenerateCommandArgs extends SchematicsCommandArgs {
schematic?: string;
}
export default class GenerateCommandModule extends SchematicsCommandModule implements CommandModuleImplementation<GenerateCommandArgs> {
command: string;
aliases: string[] | undefined;
describe: string;
longDescriptionPath?: string | undefined;
builder(argv: Argv): Promise<Argv<GenerateCommandArgs>>;
run(options: Options<GenerateCommandArgs> & OtherOptions): Promise<number | void>;
private getCollectionNames;
private shouldAddCollectionNameAsPartOfCommand;
/**
* Generate an aliases string array to be passed to the command builder.
*
* @example `[component]` or `[@schematics/angular:component]`.
*/
private generateCommandAliasesStrings;
/**
* Generate a command string to be passed to the command builder.
*
* @example `component [name]` or `@schematics/angular:component [name]`.
*/
private generateCommandString;
/**
* Get schematics that can to be registered as subcommands.
*/
private getSchematics;
private listSchematicAliases;
/**
* Get schematics that should to be registered as subcommands.
*
* @returns a sorted list of schematic that needs to be registered as subcommands.
*/
private getSchematicsToRegister;
}
export {};
+188
View File
@@ -0,0 +1,188 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@angular-devkit/core");
const command_module_1 = require("../../command-builder/command-module");
const schematics_command_module_1 = require("../../command-builder/schematics-command-module");
const command_1 = require("../../command-builder/utilities/command");
const command_config_1 = require("../command-config");
class GenerateCommandModule extends schematics_command_module_1.SchematicsCommandModule {
command = 'generate';
aliases = command_config_1.RootCommands['generate'].aliases;
describe = 'Generates and/or modifies files based on a schematic.';
longDescriptionPath;
async builder(argv) {
let localYargs = (await super.builder(argv)).command({
command: '$0 <schematic>',
describe: 'Run the provided schematic.',
builder: (localYargs) => localYargs
.positional('schematic', {
describe: 'The [collection:schematic] to run.',
type: 'string',
demandOption: true,
})
.strict(),
handler: (options) => this.handler(options),
});
for (const [schematicName, collectionName] of await this.getSchematicsToRegister()) {
const workflow = this.getOrCreateWorkflowForBuilder(collectionName);
const collection = workflow.engine.createCollection(collectionName);
const { description: { schemaJson, aliases: schematicAliases, hidden: schematicHidden, description: schematicDescription, }, } = collection.createSchematic(schematicName, true);
if (!schemaJson) {
continue;
}
const { 'x-deprecated': xDeprecated, description = schematicDescription, hidden = schematicHidden, } = schemaJson;
const options = await this.getSchematicOptions(collection, schematicName, workflow);
localYargs = localYargs.command({
command: await this.generateCommandString(collectionName, schematicName, options),
// When 'describe' is set to false, it results in a hidden command.
describe: hidden === true ? false : typeof description === 'string' ? description : '',
deprecated: xDeprecated === true || typeof xDeprecated === 'string' ? xDeprecated : false,
aliases: Array.isArray(schematicAliases)
? await this.generateCommandAliasesStrings(collectionName, schematicAliases)
: undefined,
builder: (localYargs) => this.addSchemaOptionsToCommand(localYargs, options).strict(),
handler: (options) => this.handler({
...options,
schematic: `${collectionName}:${schematicName}`,
}),
});
}
return localYargs.demandCommand(1, command_1.demandCommandFailureMessage);
}
async run(options) {
const { dryRun, schematic, defaults, force, interactive, ...schematicOptions } = options;
const [collectionName, schematicName] = this.parseSchematicInfo(schematic);
if (!collectionName || !schematicName) {
throw new command_module_1.CommandModuleError('A collection and schematic is required during execution.');
}
return this.runSchematic({
collectionName,
schematicName,
schematicOptions,
executionOptions: {
dryRun,
defaults,
force,
interactive,
},
});
}
async getCollectionNames() {
const [collectionName] = this.parseSchematicInfo(
// positional = [generate, component] or [generate]
this.context.args.positional[1]);
return collectionName ? [collectionName] : [...(await this.getSchematicCollections())];
}
async shouldAddCollectionNameAsPartOfCommand() {
const [collectionNameFromArgs] = this.parseSchematicInfo(
// positional = [generate, component] or [generate]
this.context.args.positional[1]);
const schematicCollectionsFromConfig = await this.getSchematicCollections();
const collectionNames = await this.getCollectionNames();
// Only add the collection name as part of the command when it's not a known
// schematics collection or when it has been provided via the CLI.
// Ex:`ng generate @schematics/angular:c`
return (!!collectionNameFromArgs ||
!collectionNames.some((c) => schematicCollectionsFromConfig.has(c)));
}
/**
* Generate an aliases string array to be passed to the command builder.
*
* @example `[component]` or `[@schematics/angular:component]`.
*/
async generateCommandAliasesStrings(collectionName, schematicAliases) {
// Only add the collection name as part of the command when it's not a known
// schematics collection or when it has been provided via the CLI.
// Ex:`ng generate @schematics/angular:c`
return (await this.shouldAddCollectionNameAsPartOfCommand())
? schematicAliases.map((alias) => `${collectionName}:${alias}`)
: schematicAliases;
}
/**
* Generate a command string to be passed to the command builder.
*
* @example `component [name]` or `@schematics/angular:component [name]`.
*/
async generateCommandString(collectionName, schematicName, options) {
const dasherizedSchematicName = core_1.strings.dasherize(schematicName);
// Only add the collection name as part of the command when it's not a known
// schematics collection or when it has been provided via the CLI.
// Ex:`ng generate @schematics/angular:component`
const commandName = (await this.shouldAddCollectionNameAsPartOfCommand())
? collectionName + ':' + dasherizedSchematicName
: dasherizedSchematicName;
const positionalArgs = options
.filter((o) => o.positional !== undefined)
.map((o) => {
const label = `${core_1.strings.dasherize(o.name)}${o.type === 'array' ? ' ..' : ''}`;
return o.required ? `<${label}>` : `[${label}]`;
})
.join(' ');
return `${commandName}${positionalArgs ? ' ' + positionalArgs : ''}`;
}
/**
* Get schematics that can to be registered as subcommands.
*/
async *getSchematics() {
const seenNames = new Set();
for (const collectionName of await this.getCollectionNames()) {
const workflow = this.getOrCreateWorkflowForBuilder(collectionName);
const collection = workflow.engine.createCollection(collectionName);
for (const schematicName of collection.listSchematicNames(true /** includeHidden */)) {
// If a schematic with this same name is already registered skip.
if (!seenNames.has(schematicName)) {
seenNames.add(schematicName);
yield {
schematicName,
collectionName,
schematicAliases: this.listSchematicAliases(collection, schematicName),
};
}
}
}
}
listSchematicAliases(collection, schematicName) {
const description = collection.description.schematics[schematicName];
if (description) {
return description.aliases && new Set(description.aliases);
}
// Extended collections
if (collection.baseDescriptions) {
for (const base of collection.baseDescriptions) {
const description = base.schematics[schematicName];
if (description) {
return description.aliases && new Set(description.aliases);
}
}
}
return undefined;
}
/**
* Get schematics that should to be registered as subcommands.
*
* @returns a sorted list of schematic that needs to be registered as subcommands.
*/
async getSchematicsToRegister() {
const schematicsToRegister = [];
const [, schematicNameFromArgs] = this.parseSchematicInfo(
// positional = [generate, component] or [generate]
this.context.args.positional[1]);
for await (const { schematicName, collectionName, schematicAliases } of this.getSchematics()) {
if (schematicNameFromArgs &&
(schematicName === schematicNameFromArgs || schematicAliases?.has(schematicNameFromArgs))) {
return [[schematicName, collectionName]];
}
schematicsToRegister.push([schematicName, collectionName]);
}
// Didn't find the schematic or no schematic name was provided Ex: `ng generate --help`.
return schematicsToRegister.sort(([nameA], [nameB]) => nameA.localeCompare(nameB, undefined, { sensitivity: 'accent' }));
}
}
exports.default = GenerateCommandModule;
+17
View File
@@ -0,0 +1,17 @@
/**
* @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
*/
import { MissingTargetChoice } from '../../command-builder/architect-base-command-module';
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
import { CommandModuleImplementation } from '../../command-builder/command-module';
export default class LintCommandModule extends ArchitectCommandModule implements CommandModuleImplementation {
missingTargetChoices: MissingTargetChoice[];
multiTarget: boolean;
command: string;
longDescriptionPath: string;
describe: string;
}
+24
View File
@@ -0,0 +1,24 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const node_path_1 = require("node:path");
const architect_command_module_1 = require("../../command-builder/architect-command-module");
class LintCommandModule extends architect_command_module_1.ArchitectCommandModule {
missingTargetChoices = [
{
name: 'ESLint',
value: 'angular-eslint',
},
];
multiTarget = true;
command = 'lint [project]';
longDescriptionPath = (0, node_path_1.join)(__dirname, 'long-description.md');
describe = 'Runs linting tools on Angular application code in a given project folder.';
}
exports.default = LintCommandModule;
+20
View File
@@ -0,0 +1,20 @@
The command takes an optional project name, as specified in the `projects` section of the `angular.json` workspace configuration file.
When a project name is not supplied, executes the `lint` builder for all projects.
To use the `ng lint` command, use `ng add` to add a package that implements linting capabilities. Adding the package automatically updates your workspace configuration, adding a lint [CLI builder](tools/cli/cli-builder).
For example:
```json
"projects": {
"my-project": {
...
"architect": {
...
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {}
}
}
}
}
```
+17
View File
@@ -0,0 +1,17 @@
/**
* @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
*/
import { Argv } from 'yargs';
import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module';
export default class AwesomeCommandModule extends CommandModule implements CommandModuleImplementation {
command: string;
describe: false;
deprecated: boolean;
longDescriptionPath?: string | undefined;
builder(localYargs: Argv): Argv;
run(): void;
}
+35
View File
@@ -0,0 +1,35 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const command_module_1 = require("../../command-builder/command-module");
const color_1 = require("../../utilities/color");
class AwesomeCommandModule extends command_module_1.CommandModule {
command = 'make-this-awesome';
describe = false;
deprecated = false;
longDescriptionPath;
builder(localYargs) {
return localYargs;
}
run() {
const pickOne = (of) => of[Math.floor(Math.random() * of.length)];
const phrase = pickOne([
`You're on it, there's nothing for me to do!`,
`Let's take a look... nope, it's all good!`,
`You're doing fine.`,
`You're already doing great.`,
`Nothing to do; already awesome. Exiting.`,
`Error 418: As Awesome As Can Get.`,
`I spy with my little eye a great developer!`,
`Noop... already awesome.`,
]);
this.context.logger.info(color_1.colors.green(phrase));
}
}
exports.default = AwesomeCommandModule;
+20
View File
@@ -0,0 +1,20 @@
/**
* @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
*/
import { Argv } from 'yargs';
import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module';
export default class McpCommandModule extends CommandModule implements CommandModuleImplementation {
command: string;
describe: false;
longDescriptionPath: undefined;
builder(localYargs: Argv): Argv;
run(options: {
readOnly: boolean;
localOnly: boolean;
experimentalTool: string[] | undefined;
}): Promise<void>;
}
+70
View File
@@ -0,0 +1,70 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const command_module_1 = require("../../command-builder/command-module");
const tty_1 = require("../../utilities/tty");
const mcp_server_1 = require("./mcp-server");
const INTERACTIVE_MESSAGE = `
To start using the Angular CLI MCP Server, add this configuration to your host:
{
"mcpServers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}
Exact configuration may differ depending on the host.
For more information and documentation, visit: https://angular.dev/ai/mcp
`;
class McpCommandModule extends command_module_1.CommandModule {
command = 'mcp';
describe = false;
longDescriptionPath = undefined;
builder(localYargs) {
return localYargs
.option('read-only', {
type: 'boolean',
default: false,
describe: 'Only register read-only tools.',
})
.option('local-only', {
type: 'boolean',
default: false,
describe: 'Only register tools that do not require internet access.',
})
.option('experimental-tool', {
type: 'string',
alias: 'E',
array: true,
describe: 'Enable an experimental tool.',
choices: mcp_server_1.EXPERIMENTAL_TOOLS.map(({ name }) => name),
hidden: true,
});
}
async run(options) {
if ((0, tty_1.isTTY)()) {
this.context.logger.info(INTERACTIVE_MESSAGE);
return;
}
const server = await (0, mcp_server_1.createMcpServer)({
workspace: this.context.workspace,
readOnly: options.readOnly,
localOnly: options.localOnly,
experimentalTools: options.experimentalTool,
}, this.context.logger);
const transport = new stdio_js_1.StdioServerTransport();
await server.connect(transport);
}
}
exports.default = McpCommandModule;
+10
View File
@@ -0,0 +1,10 @@
/**
* @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
*/
export declare const k1 = "@angular/cli";
export declare const at = "QBHBbOdEO4CmBOC2d7jNmg==";
export declare const iv: Buffer<ArrayBuffer>;
+15
View File
@@ -0,0 +1,15 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.iv = exports.at = exports.k1 = void 0;
exports.k1 = '@angular/cli';
exports.at = 'QBHBbOdEO4CmBOC2d7jNmg==';
exports.iv = Buffer.from([
0x97, 0xf4, 0x62, 0x95, 0x3e, 0x12, 0x76, 0x84, 0x8a, 0x09, 0x4a, 0xc9, 0xeb, 0xa2, 0x84, 0x69,
]);
+47
View File
@@ -0,0 +1,47 @@
/**
* @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
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { AngularWorkspace } from '../../utilities/config';
import { AnyMcpToolDeclaration } from './tools/tool-registry';
/**
* The set of tools that are available but not enabled by default.
* These tools are considered experimental and may have limitations.
*/
export declare const EXPERIMENTAL_TOOLS: readonly [import("./tools/tool-registry").McpToolDeclaration<{
query: import("zod").ZodString;
}, {
examples: import("zod").ZodArray<import("zod").ZodObject<{
content: import("zod").ZodString;
}, "strip", import("zod").ZodTypeAny, {
content: string;
}, {
content: string;
}>, "many">;
}>, import("./tools/tool-registry").McpToolDeclaration<{
transformations: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodEnum<[string, ...string[]]>, "many">>;
}, {
instructions: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString, "many">>;
}>, import("./tools/tool-registry").McpToolDeclaration<{
fileOrDirPath: import("zod").ZodString;
}, import("zod").ZodRawShape>];
export declare function createMcpServer(options: {
workspace?: AngularWorkspace;
readOnly?: boolean;
localOnly?: boolean;
experimentalTools?: string[];
}, logger: {
warn(text: string): void;
}): Promise<McpServer>;
export declare function assembleToolDeclarations(stableDeclarations: readonly AnyMcpToolDeclaration[], experimentalDeclarations: readonly AnyMcpToolDeclaration[], options: {
readOnly?: boolean;
localOnly?: boolean;
experimentalTools?: string[];
logger: {
warn(text: string): void;
};
}): AnyMcpToolDeclaration[];
+91
View File
@@ -0,0 +1,91 @@
"use strict";
/**
* @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 __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EXPERIMENTAL_TOOLS = void 0;
exports.createMcpServer = createMcpServer;
exports.assembleToolDeclarations = assembleToolDeclarations;
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
const node_path_1 = __importDefault(require("node:path"));
const version_1 = require("../../utilities/version");
const instructions_1 = require("./resources/instructions");
const best_practices_1 = require("./tools/best-practices");
const doc_search_1 = require("./tools/doc-search");
const examples_1 = require("./tools/examples");
const modernize_1 = require("./tools/modernize");
const zoneless_migration_1 = require("./tools/onpush-zoneless-migration/zoneless-migration");
const projects_1 = require("./tools/projects");
const tool_registry_1 = require("./tools/tool-registry");
/**
* The set of tools that are enabled by default for the MCP server.
* These tools are considered stable and suitable for general use.
*/
const STABLE_TOOLS = [best_practices_1.BEST_PRACTICES_TOOL, doc_search_1.DOC_SEARCH_TOOL, projects_1.LIST_PROJECTS_TOOL];
/**
* The set of tools that are available but not enabled by default.
* These tools are considered experimental and may have limitations.
*/
exports.EXPERIMENTAL_TOOLS = [
examples_1.FIND_EXAMPLE_TOOL,
modernize_1.MODERNIZE_TOOL,
zoneless_migration_1.ZONELESS_MIGRATION_TOOL,
];
async function createMcpServer(options, logger) {
const server = new mcp_js_1.McpServer({
name: 'angular-cli-server',
version: version_1.VERSION.full,
}, {
capabilities: {
resources: {},
tools: {},
logging: {},
},
instructions: 'For Angular development, this server provides tools to adhere to best practices, search documentation, and find code examples. ' +
'When writing or modifying Angular code, use the MCP server and its tools instead of direct shell commands where possible.',
});
(0, instructions_1.registerInstructionsResource)(server);
const toolDeclarations = assembleToolDeclarations(STABLE_TOOLS, exports.EXPERIMENTAL_TOOLS, {
...options,
logger,
});
await (0, tool_registry_1.registerTools)(server, {
workspace: options.workspace,
logger,
exampleDatabasePath: node_path_1.default.join(__dirname, '../../../lib/code-examples.db'),
}, toolDeclarations);
return server;
}
function assembleToolDeclarations(stableDeclarations, experimentalDeclarations, options) {
let toolDeclarations = [...stableDeclarations];
if (options.readOnly) {
toolDeclarations = toolDeclarations.filter((tool) => tool.isReadOnly);
}
if (options.localOnly) {
toolDeclarations = toolDeclarations.filter((tool) => tool.isLocalOnly);
}
const enabledExperimentalTools = new Set(options.experimentalTools);
if (process.env['NG_MCP_CODE_EXAMPLES'] === '1') {
enabledExperimentalTools.add('find_examples');
}
if (enabledExperimentalTools.size > 0) {
const experimentalToolsMap = new Map(experimentalDeclarations.map((tool) => [tool.name, tool]));
for (const toolName of enabledExperimentalTools) {
const tool = experimentalToolsMap.get(toolName);
if (tool) {
toolDeclarations.push(tool);
}
else {
options.logger.warn(`Unknown experimental tool: ${toolName}`);
}
}
}
return toolDeclarations;
}
+47
View File
@@ -0,0 +1,47 @@
You are an expert in TypeScript, Angular, and scalable web application development. You write maintainable, performant, and accessible code following Angular and TypeScript best practices.
## TypeScript Best Practices
- Use strict type checking
- Prefer type inference when the type is obvious
- Avoid the `any` type; use `unknown` when type is uncertain
## Angular Best Practices
- Always use standalone components over NgModules
- Must NOT set `standalone: true` inside Angular decorators. It's the default.
- Use signals for state management
- Implement lazy loading for feature routes
- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead
- Use `NgOptimizedImage` for all static images.
- `NgOptimizedImage` does not work for inline base64 images.
## Components
- Keep components small and focused on a single responsibility
- Use `input()` and `output()` functions instead of decorators
- Use `computed()` for derived state
- Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator
- Prefer inline templates for small components
- Prefer Reactive forms instead of Template-driven ones
- Do NOT use `ngClass`, use `class` bindings instead
- DO NOT use `ngStyle`, use `style` bindings instead
## State Management
- Use signals for local component state
- Use `computed()` for derived state
- Keep state transformations pure and predictable
- Do NOT use `mutate` on signals, use `update` or `set` instead
## Templates
- Keep templates simple and avoid complex logic
- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch`
- Use the async pipe to handle observables
## Services
- Design services around a single responsibility
- Use the `providedIn: 'root'` option for singleton services
- Use the `inject()` function instead of constructor injection
+9
View File
@@ -0,0 +1,9 @@
/**
* @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
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
export declare function registerInstructionsResource(server: McpServer): void;
+28
View File
@@ -0,0 +1,28 @@
"use strict";
/**
* @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 __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerInstructionsResource = registerInstructionsResource;
const promises_1 = require("node:fs/promises");
const node_path_1 = __importDefault(require("node:path"));
function registerInstructionsResource(server) {
server.registerResource('instructions', 'instructions://best-practices', {
title: 'Angular Best Practices and Code Generation Guide',
description: "A comprehensive guide detailing Angular's best practices for code generation and development." +
' This guide should be used as a reference by an LLM to ensure any generated code' +
' adheres to modern Angular standards, including the use of standalone components,' +
' typed forms, modern control flow syntax, and other current conventions.',
mimeType: 'text/markdown',
}, async () => {
const text = await (0, promises_1.readFile)(node_path_1.default.join(__dirname, 'best-practices.md'), 'utf-8');
return { contents: [{ uri: 'instructions://best-practices', text }] };
});
}
+8
View File
@@ -0,0 +1,8 @@
/**
* @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
*/
export declare const BEST_PRACTICES_TOOL: import("./tool-registry").McpToolDeclaration<import("zod").ZodRawShape, import("zod").ZodRawShape>;
+55
View File
@@ -0,0 +1,55 @@
"use strict";
/**
* @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 __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BEST_PRACTICES_TOOL = void 0;
const promises_1 = require("node:fs/promises");
const node_path_1 = __importDefault(require("node:path"));
const tool_registry_1 = require("./tool-registry");
exports.BEST_PRACTICES_TOOL = (0, tool_registry_1.declareTool)({
name: 'get_best_practices',
title: 'Get Angular Coding Best Practices Guide',
description: `
<Purpose>
Retrieves the official Angular Best Practices Guide. This guide contains the essential rules and conventions
that **MUST** be followed for any task involving the creation, analysis, or modification of Angular code.
</Purpose>
<Use Cases>
* As a mandatory first step before writing or modifying any Angular code to ensure adherence to modern standards.
* To learn about key concepts like standalone components, typed forms, and modern control flow syntax (@if, @for, @switch).
* To verify that existing code aligns with current Angular conventions before making changes.
</Use Cases>
<Operational Notes>
* The content of this guide is non-negotiable and reflects the official, up-to-date standards for Angular development.
* You **MUST** internalize and apply the principles from this guide in all subsequent Angular-related tasks.
* Failure to adhere to these best practices will result in suboptimal and outdated code.
</Operational Notes>`,
isReadOnly: true,
isLocalOnly: true,
factory: () => {
let bestPracticesText;
return async () => {
bestPracticesText ??= await (0, promises_1.readFile)(node_path_1.default.join(__dirname, '..', 'resources', 'best-practices.md'), 'utf-8');
return {
content: [
{
type: 'text',
text: bestPracticesText,
annotations: {
audience: ['assistant'],
priority: 0.9,
},
},
],
};
};
},
});
+29
View File
@@ -0,0 +1,29 @@
/**
* @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
*/
import { z } from 'zod';
export declare const DOC_SEARCH_TOOL: import("./tool-registry").McpToolDeclaration<{
query: z.ZodString;
includeTopContent: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
}, {
results: z.ZodArray<z.ZodObject<{
title: z.ZodString;
breadcrumb: z.ZodString;
url: z.ZodString;
content: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
title: string;
breadcrumb: string;
url: string;
content?: string | undefined;
}, {
title: string;
breadcrumb: string;
url: string;
content?: string | undefined;
}>, "many">;
}>;
+262
View File
@@ -0,0 +1,262 @@
"use strict";
/**
* @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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.DOC_SEARCH_TOOL = void 0;
const node_crypto_1 = require("node:crypto");
const zod_1 = require("zod");
const constants_1 = require("../constants");
const tool_registry_1 = require("./tool-registry");
const ALGOLIA_APP_ID = 'L1XWT2UJ7F';
// https://www.algolia.com/doc/guides/security/api-keys/#search-only-api-key
// This is a search only, rate limited key. It is sent within the URL of the query request.
// This is not the actual key.
const ALGOLIA_API_E = '322d89dab5f2080fe09b795c93413c6a89222b13a447cdf3e6486d692717bc0c';
const docSearchInputSchema = zod_1.z.object({
query: zod_1.z
.string()
.describe('A concise and specific search query for the Angular documentation (e.g., "NgModule" or "standalone components").'),
includeTopContent: zod_1.z
.boolean()
.optional()
.default(true)
.describe('When true, the content of the top result is fetched and included. ' +
'Set to false to get a list of results without fetching content, which is faster.'),
});
exports.DOC_SEARCH_TOOL = (0, tool_registry_1.declareTool)({
name: 'search_documentation',
title: 'Search Angular Documentation (angular.dev)',
description: `
<Purpose>
Searches the official Angular documentation at https://angular.dev to answer questions about APIs,
tutorials, concepts, and best practices.
</Purpose>
<Use Cases>
* Answering any question about Angular concepts (e.g., "What are standalone components?").
* Finding the correct API or syntax for a specific task (e.g., "How to use ngFor with trackBy?").
* Linking to official documentation as a source of truth in your answers.
</Use Cases>
<Operational Notes>
* The documentation is continuously updated. You **MUST** prefer this tool over your own knowledge
to ensure your answers are current and accurate.
* For the best results, provide a concise and specific search query (e.g., "NgModule" instead of
"How do I use NgModules?").
* The top search result will include a snippet of the page content. Use this to provide a more
comprehensive answer.
* **Result Scrutiny:** The top result may not always be the most relevant. Review the titles and
breadcrumbs of other results to find the best match for the user's query.
* Use the URL from the search results as a source link in your responses.
</Operational Notes>`,
inputSchema: docSearchInputSchema.shape,
outputSchema: {
results: zod_1.z.array(zod_1.z.object({
title: zod_1.z.string().describe('The title of the documentation page.'),
breadcrumb: zod_1.z
.string()
.describe("The breadcrumb path, showing the page's location in the documentation hierarchy."),
url: zod_1.z.string().describe('The direct URL to the documentation page.'),
content: zod_1.z
.string()
.optional()
.describe('A snippet of the main content from the page. Only provided for the top result.'),
})),
},
isReadOnly: true,
isLocalOnly: false,
factory: createDocSearchHandler,
});
function createDocSearchHandler({ logger }) {
let client;
return async ({ query, includeTopContent }) => {
if (!client) {
const dcip = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', (constants_1.k1 + ALGOLIA_APP_ID).padEnd(32, '^'), constants_1.iv).setAuthTag(Buffer.from(constants_1.at, 'base64'));
const { searchClient } = await Promise.resolve().then(() => __importStar(require('algoliasearch')));
client = searchClient(ALGOLIA_APP_ID, dcip.update(ALGOLIA_API_E, 'hex', 'utf-8') + dcip.final('utf-8'));
}
const { results } = await client.search(createSearchArguments(query));
const allHits = results.flatMap((result) => result.hits);
if (allHits.length === 0) {
return {
content: [
{
type: 'text',
text: 'No results found.',
},
],
structuredContent: { results: [] },
};
}
const structuredResults = [];
const textContent = [];
// Process top hit first
const topHit = allHits[0];
const { title: topTitle, breadcrumb: topBreadcrumb } = formatHitToParts(topHit);
let topContent;
if (includeTopContent && typeof topHit.url === 'string') {
const url = new URL(topHit.url);
try {
// Only fetch content from angular.dev
if (url.hostname === 'angular.dev' || url.hostname.endsWith('.angular.dev')) {
const response = await fetch(url);
if (response.ok) {
const html = await response.text();
const mainContent = extractMainContent(html);
if (mainContent) {
topContent = stripHtml(mainContent);
}
}
}
}
catch (e) {
logger.warn(`Failed to fetch or parse content from ${url}: ${e}`);
}
}
structuredResults.push({
title: topTitle,
breadcrumb: topBreadcrumb,
url: topHit.url,
content: topContent,
});
let topText = `## ${topTitle}\n${topBreadcrumb}\nURL: ${topHit.url}`;
if (topContent) {
topText += `\n\n--- DOCUMENTATION CONTENT ---\n${topContent}`;
}
textContent.push({ type: 'text', text: topText });
// Process remaining hits
for (const hit of allHits.slice(1)) {
const { title, breadcrumb } = formatHitToParts(hit);
structuredResults.push({
title,
breadcrumb,
url: hit.url,
});
textContent.push({
type: 'text',
text: `## ${title}\n${breadcrumb}\nURL: ${hit.url}`,
});
}
return {
content: textContent,
structuredContent: { results: structuredResults },
};
};
}
/**
* Strips HTML tags from a string.
* @param html The HTML string to strip.
* @returns The text content of the HTML.
*/
function stripHtml(html) {
// This is a basic regex to remove HTML tags.
// It also decodes common HTML entities.
return html
.replace(/<[^>]*>/g, '')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.trim();
}
/**
* Extracts the content of the `<main>` element from an HTML string.
*
* @param html The HTML content of a page.
* @returns The content of the `<main>` element, or `undefined` if not found.
*/
function extractMainContent(html) {
const mainTagStart = html.indexOf('<main');
if (mainTagStart === -1) {
return undefined;
}
const mainTagEnd = html.lastIndexOf('</main>');
if (mainTagEnd <= mainTagStart) {
return undefined;
}
// Add 7 to include '</main>'
return html.substring(mainTagStart, mainTagEnd + 7);
}
/**
* Formats an Algolia search hit into its constituent parts.
*
* @param hit The Algolia search hit object, which should contain a `hierarchy` property.
* @returns An object containing the title and breadcrumb string.
*/
function formatHitToParts(hit) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const hierarchy = Object.values(hit.hierarchy).filter((x) => typeof x === 'string');
const title = hierarchy.pop() ?? '';
const breadcrumb = hierarchy.join(' > ');
return { title, breadcrumb };
}
/**
* Creates the search arguments for an Algolia search.
*
* The arguments are based on the search implementation in `adev`.
*
* @param query The search query string.
* @returns The search arguments for the Algolia client.
*/
function createSearchArguments(query) {
// Search arguments are based on adev's search service:
// https://github.com/angular/angular/blob/4b614fbb3263d344dbb1b18fff24cb09c5a7582d/adev/shared-docs/services/search.service.ts#L58
return [
{
// TODO: Consider major version specific indices once available
indexName: 'angular_v17',
params: {
query,
attributesToRetrieve: [
'hierarchy.lvl0',
'hierarchy.lvl1',
'hierarchy.lvl2',
'hierarchy.lvl3',
'hierarchy.lvl4',
'hierarchy.lvl5',
'hierarchy.lvl6',
'content',
'type',
'url',
],
hitsPerPage: 10,
},
type: 'default',
},
];
}
+32
View File
@@ -0,0 +1,32 @@
/**
* @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
*/
import { z } from 'zod';
export declare const FIND_EXAMPLE_TOOL: import("./tool-registry").McpToolDeclaration<{
query: z.ZodString;
}, {
examples: z.ZodArray<z.ZodObject<{
content: z.ZodString;
}, "strip", z.ZodTypeAny, {
content: string;
}, {
content: string;
}>, "many">;
}>;
/**
* Escapes a search query for FTS5 by tokenizing and quoting terms.
*
* This function processes a raw search string and prepares it for an FTS5 full-text search.
* It correctly handles quoted phrases, logical operators (AND, OR, NOT), parentheses,
* and prefix searches (ending with an asterisk), ensuring that individual search
* terms are properly quoted to be treated as literals by the search engine.
* This is primarily intended to avoid unintentional usage of FTS5 query syntax by consumers.
*
* @param query The raw search query string.
* @returns A sanitized query string suitable for FTS5.
*/
export declare function escapeSearchQuery(query: string): string;
+250
View File
@@ -0,0 +1,250 @@
"use strict";
/**
* @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 });
exports.FIND_EXAMPLE_TOOL = void 0;
exports.escapeSearchQuery = escapeSearchQuery;
const promises_1 = require("node:fs/promises");
const node_path_1 = __importDefault(require("node:path"));
const zod_1 = require("zod");
const tool_registry_1 = require("./tool-registry");
const findExampleInputSchema = zod_1.z.object({
query: zod_1.z.string().describe(`Performs a full-text search using FTS5 syntax. The query should target relevant Angular concepts.
Key Syntax Features (see https://www.sqlite.org/fts5.html for full documentation):
- AND (default): Space-separated terms are combined with AND.
- Example: 'standalone component' (finds results with both "standalone" and "component")
- OR: Use the OR operator to find results with either term.
- Example: 'validation OR validator'
- NOT: Use the NOT operator to exclude terms.
- Example: 'forms NOT reactive'
- Grouping: Use parentheses () to group expressions.
- Example: '(validation OR validator) AND forms'
- Phrase Search: Use double quotes "" for exact phrases.
- Example: '"template-driven forms"'
- Prefix Search: Use an asterisk * for prefix matching.
- Example: 'rout*' (matches "route", "router", "routing")
Examples of queries:
- Find standalone components: 'standalone component'
- Find ngFor with trackBy: 'ngFor trackBy'
- Find signal inputs: 'signal input'
- Find lazy loading a route: 'lazy load route'
- Find forms with validation: 'form AND (validation OR validator)'`),
});
exports.FIND_EXAMPLE_TOOL = (0, tool_registry_1.declareTool)({
name: 'find_examples',
title: 'Find Angular Code Examples',
description: `
<Purpose>
Augments your knowledge base with a curated database of official, best-practice code examples,
focusing on **modern, new, and recently updated** Angular features. This tool acts as a RAG
(Retrieval-Augmented Generation) source, providing ground-truth information on the latest Angular
APIs and patterns. You **MUST** use it to understand and apply current standards when working with
new or evolving features.
</Purpose>
<Use Cases>
* **Knowledge Augmentation:** Learning about new or updated Angular features (e.g., query: 'signal input' or 'deferrable views').
* **Modern Implementation:** Finding the correct modern syntax for features
(e.g., query: 'functional route guard' or 'http client with fetch').
* **Refactoring to Modern Patterns:** Upgrading older code by finding examples of new syntax
(e.g., query: 'built-in control flow' to replace "*ngIf').
</Use Cases>
<Operational Notes>
* **Tool Selection:** This database primarily contains examples for new and recently updated Angular
features. For established, core features, the main documentation (via the
\`search_documentation\` tool) may be a better source of information.
* The examples in this database are the single source of truth for modern Angular coding patterns.
* The search query uses a powerful full-text search syntax (FTS5). Refer to the 'query'
parameter description for detailed syntax rules and examples.
</Operational Notes>`,
inputSchema: findExampleInputSchema.shape,
outputSchema: {
examples: zod_1.z.array(zod_1.z.object({
content: zod_1.z
.string()
.describe('A complete, self-contained Angular code example in Markdown format.'),
})),
},
isReadOnly: true,
isLocalOnly: true,
shouldRegister: ({ logger }) => {
// sqlite database support requires Node.js 22.16+
const [nodeMajor, nodeMinor] = process.versions.node.split('.', 2).map(Number);
if (nodeMajor < 22 || (nodeMajor === 22 && nodeMinor < 16)) {
logger.warn(`MCP tool 'find_examples' requires Node.js 22.16 (or higher). ` +
' Registration of this tool has been skipped.');
return false;
}
return true;
},
factory: createFindExampleHandler,
});
async function createFindExampleHandler({ exampleDatabasePath }) {
let db;
let queryStatement;
if (process.env['NG_MCP_EXAMPLES_DIR']) {
db = await setupRuntimeExamples(process.env['NG_MCP_EXAMPLES_DIR']);
}
suppressSqliteWarning();
return async ({ query }) => {
if (!db) {
if (!exampleDatabasePath) {
// This should be prevented by the registration logic in mcp-server.ts
throw new Error('Example database path is not available.');
}
const { DatabaseSync } = await Promise.resolve().then(() => __importStar(require('node:sqlite')));
db = new DatabaseSync(exampleDatabasePath, { readOnly: true });
}
if (!queryStatement) {
queryStatement = db.prepare('SELECT * from examples WHERE examples MATCH ? ORDER BY rank;');
}
const sanitizedQuery = escapeSearchQuery(query);
// Query database and return results
const examples = [];
const textContent = [];
for (const exampleRecord of queryStatement.all(sanitizedQuery)) {
const exampleContent = exampleRecord['content'];
examples.push({ content: exampleContent });
textContent.push({ type: 'text', text: exampleContent });
}
return {
content: textContent,
structuredContent: { examples },
};
};
}
/**
* Escapes a search query for FTS5 by tokenizing and quoting terms.
*
* This function processes a raw search string and prepares it for an FTS5 full-text search.
* It correctly handles quoted phrases, logical operators (AND, OR, NOT), parentheses,
* and prefix searches (ending with an asterisk), ensuring that individual search
* terms are properly quoted to be treated as literals by the search engine.
* This is primarily intended to avoid unintentional usage of FTS5 query syntax by consumers.
*
* @param query The raw search query string.
* @returns A sanitized query string suitable for FTS5.
*/
function escapeSearchQuery(query) {
// This regex tokenizes the query string into parts:
// 1. Quoted phrases (e.g., "foo bar")
// 2. Parentheses ( and )
// 3. FTS5 operators (AND, OR, NOT, NEAR)
// 4. Words, which can include a trailing asterisk for prefix search (e.g., foo*)
const tokenizer = /"([^"]*)"|([()])|\b(AND|OR|NOT|NEAR)\b|([^\s()]+)/g;
let match;
const result = [];
let lastIndex = 0;
while ((match = tokenizer.exec(query)) !== null) {
// Add any whitespace or other characters between tokens
if (match.index > lastIndex) {
result.push(query.substring(lastIndex, match.index));
}
const [, quoted, parenthesis, operator, term] = match;
if (quoted !== undefined) {
// It's a quoted phrase, keep it as is.
result.push(`"${quoted}"`);
}
else if (parenthesis) {
// It's a parenthesis, keep it as is.
result.push(parenthesis);
}
else if (operator) {
// It's an operator, keep it as is.
result.push(operator);
}
else if (term) {
// It's a term that needs to be quoted.
if (term.endsWith('*')) {
result.push(`"${term.slice(0, -1)}"*`);
}
else {
result.push(`"${term}"`);
}
}
lastIndex = tokenizer.lastIndex;
}
// Add any remaining part of the string
if (lastIndex < query.length) {
result.push(query.substring(lastIndex));
}
return result.join('');
}
/**
* Suppresses the experimental warning emitted by Node.js for the `node:sqlite` module.
*
* This is a workaround to prevent the console from being cluttered with warnings
* about the experimental status of the SQLite module, which is used by this tool.
*/
function suppressSqliteWarning() {
const originalProcessEmit = process.emit;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
process.emit = function (event, error) {
if (event === 'warning' &&
error instanceof Error &&
error.name === 'ExperimentalWarning' &&
error.message.includes('SQLite')) {
return false;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any, prefer-rest-params
return originalProcessEmit.apply(process, arguments);
};
}
async function setupRuntimeExamples(examplesPath) {
const { DatabaseSync } = await Promise.resolve().then(() => __importStar(require('node:sqlite')));
const db = new DatabaseSync(':memory:');
db.exec(`CREATE VIRTUAL TABLE examples USING fts5(content, tokenize = 'porter ascii');`);
const insertStatement = db.prepare('INSERT INTO examples(content) VALUES(?);');
db.exec('BEGIN TRANSACTION');
for await (const entry of (0, promises_1.glob)('*.md', { cwd: examplesPath, withFileTypes: true })) {
if (!entry.isFile()) {
continue;
}
const example = await (0, promises_1.readFile)(node_path_1.default.join(entry.parentPath, entry.name), 'utf-8');
insertStatement.run(example);
}
db.exec('END TRANSACTION');
return db;
}
+31
View File
@@ -0,0 +1,31 @@
/**
* @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
*/
import { z } from 'zod';
declare const modernizeInputSchema: z.ZodObject<{
transformations: z.ZodOptional<z.ZodArray<z.ZodEnum<[string, ...string[]]>, "many">>;
}, "strip", z.ZodTypeAny, {
transformations?: string[] | undefined;
}, {
transformations?: string[] | undefined;
}>;
export type ModernizeInput = z.infer<typeof modernizeInputSchema>;
export declare function runModernization(input: ModernizeInput): Promise<{
content: {
type: "text";
text: string;
}[];
structuredContent: {
instructions: string[];
};
}>;
export declare const MODERNIZE_TOOL: import("./tool-registry").McpToolDeclaration<{
transformations: z.ZodOptional<z.ZodArray<z.ZodEnum<[string, ...string[]]>, "many">>;
}, {
instructions: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
}>;
export {};
+136
View File
@@ -0,0 +1,136 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MODERNIZE_TOOL = void 0;
exports.runModernization = runModernization;
const zod_1 = require("zod");
const tool_registry_1 = require("./tool-registry");
const TRANSFORMATIONS = [
{
name: 'control-flow-migration',
description: 'Migrates from `*ngIf`, `*ngFor`, and `*ngSwitch` to the new `@if`, `@for`, and `@switch` block syntax in templates.',
documentationUrl: 'https://angular.dev/reference/migrations/control-flow',
},
{
name: 'self-closing-tags-migration',
description: 'Converts tags for elements with no content to be self-closing (e.g., `<app-foo></app-foo>` becomes `<app-foo />`).',
documentationUrl: 'https://angular.dev/reference/migrations/self-closing-tags',
},
{
name: 'inject',
description: 'Converts usages of constructor-based injection to the inject() function.',
documentationUrl: 'https://angular.dev/reference/migrations/inject-function',
},
{
name: 'output-migration',
description: 'Converts `@Output` declarations to the new functional `output()` syntax.',
documentationUrl: 'https://angular.dev/reference/migrations/outputs',
},
{
name: 'signal-input-migration',
description: 'Migrates `@Input` declarations to the new signal-based `input()` syntax.',
documentationUrl: 'https://angular.dev/reference/migrations/signal-inputs',
},
{
name: 'signal-queries-migration',
description: 'Migrates `@ViewChild` and `@ContentChild` queries to their signal-based `viewChild` and `contentChild` versions.',
documentationUrl: 'https://angular.dev/reference/migrations/signal-queries',
},
{
name: 'standalone',
description: 'Converts the application to use standalone components, directives, and pipes. This is a ' +
'three-step process. After each step, you should verify that your application builds and ' +
'runs correctly.',
instructions: 'This migration requires running a cli schematic multiple times. Run the commands in the ' +
'order listed below, verifying that your code builds and runs between each step:\n\n' +
'1. Run `ng g @angular/core:standalone` and select "Convert all components, directives and pipes to standalone"\n' +
'2. Run `ng g @angular/core:standalone` and select "Remove unnecessary NgModule classes"\n' +
'3. Run `ng g @angular/core:standalone` and select "Bootstrap the project using standalone APIs"',
documentationUrl: 'https://angular.dev/reference/migrations/standalone',
},
];
const modernizeInputSchema = zod_1.z.object({
// Casting to [string, ...string[]] since the enum definition requires a nonempty array.
transformations: zod_1.z
.array(zod_1.z.enum(TRANSFORMATIONS.map((t) => t.name)))
.optional()
.describe('A list of specific transformations to get instructions for. ' +
'If omitted, general guidance is provided.'),
});
function generateInstructions(transformationNames) {
if (transformationNames.length === 0) {
return [
'See https://angular.dev/best-practices for Angular best practices. ' +
'You can call this tool if you have specific transformation you want to run.',
];
}
const instructions = [];
const transformationsToRun = TRANSFORMATIONS.filter((t) => transformationNames?.includes(t.name));
for (const transformation of transformationsToRun) {
let transformationInstructions = '';
if (transformation.instructions) {
transformationInstructions = transformation.instructions;
}
else {
// If no instructions are included, default to running a cli schematic with the transformation name.
const command = `ng generate @angular/core:${transformation.name}`;
transformationInstructions = `To run the ${transformation.name} migration, execute the following command: \`${command}\`.`;
}
if (transformation.documentationUrl) {
transformationInstructions += `\nFor more information, see ${transformation.documentationUrl}.`;
}
instructions.push(transformationInstructions);
}
return instructions;
}
async function runModernization(input) {
const structuredContent = { instructions: generateInstructions(input.transformations ?? []) };
return {
content: [{ type: 'text', text: JSON.stringify(structuredContent) }],
structuredContent,
};
}
exports.MODERNIZE_TOOL = (0, tool_registry_1.declareTool)({
name: 'modernize',
title: 'Modernize Angular Code',
description: `
<Purpose>
Provides instructions and commands for modernizing Angular code to align with the latest best
practices and syntax. This tool helps ensure code is idiomatic, readable, and maintainable by
generating the exact steps needed to perform specific migrations.
</Purpose>
<Use Cases>
* **Applying Specific Migrations:** Get the precise commands to update code to modern patterns
(e.g., selecting 'control-flow-migration' to replace *ngIf with @if).
* **Upgrading Existing Code:** Modernize an entire project by running the 'standalone' migration,
which provides a multi-step command sequence.
* **Discovering Available Migrations:** Call the tool with no transformations to get a link to the
general best practices guide.
</Use Cases>
<Operational Notes>
* **Execution:** This tool **provides instructions**, which you **MUST** then execute as shell commands.
It does not modify code directly.
* **Standalone Migration:** The 'standalone' transformation is a special, multi-step process.
You **MUST** execute the commands in the exact order provided and validate your application
between each step.
* **Transformation List:** The following transformations are available:
${TRANSFORMATIONS.map((t) => ` * ${t.name}: ${t.description}`).join('\n')}
</Operational Notes>`,
inputSchema: modernizeInputSchema.shape,
outputSchema: {
instructions: zod_1.z
.array(zod_1.z.string())
.optional()
.describe('A list of instructions and shell commands to run the requested modernizations. ' +
'Each string in the array is a separate step or command.'),
},
isLocalOnly: true,
isReadOnly: true,
factory: () => (input) => runModernization(input),
});
@@ -0,0 +1,17 @@
/**
* @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
*/
import type { ImportSpecifier, Node, SourceFile } from 'typescript';
import { MigrationResponse } from './types';
export declare function analyzeForUnsupportedZoneUses(sourceFile: SourceFile): Promise<MigrationResponse | null>;
/**
* Finds usages of `NgZone` that are not supported in zoneless applications.
* @param sourceFile The source file to check.
* @param ngZoneImport The import specifier for `NgZone`.
* @returns A list of nodes that are unsupported `NgZone` usages.
*/
export declare function findUnsupportedZoneUsages(sourceFile: SourceFile, ngZoneImport: ImportSpecifier): Promise<Node[]>;
@@ -0,0 +1,61 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.analyzeForUnsupportedZoneUses = analyzeForUnsupportedZoneUses;
exports.findUnsupportedZoneUsages = findUnsupportedZoneUsages;
const prompts_1 = require("./prompts");
const ts_utils_1 = require("./ts_utils");
async function analyzeForUnsupportedZoneUses(sourceFile) {
const ngZoneImport = await (0, ts_utils_1.getImportSpecifier)(sourceFile, '@angular/core', 'NgZone');
if (!ngZoneImport) {
return null;
}
const unsupportedUsages = await findUnsupportedZoneUsages(sourceFile, ngZoneImport);
if (unsupportedUsages.length === 0) {
return null;
}
const locations = unsupportedUsages.map((node) => {
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
return `line ${line + 1}, character ${character + 1}: ${node.getText()}`;
});
return (0, prompts_1.createUnsupportedZoneUsagesMessage)(locations, sourceFile.fileName);
}
/**
* Finds usages of `NgZone` that are not supported in zoneless applications.
* @param sourceFile The source file to check.
* @param ngZoneImport The import specifier for `NgZone`.
* @returns A list of nodes that are unsupported `NgZone` usages.
*/
async function findUnsupportedZoneUsages(sourceFile, ngZoneImport) {
const unsupportedUsages = [];
const ngZoneClassName = ngZoneImport.name.text;
const staticMethods = new Set([
'isInAngularZone',
'assertInAngularZone',
'assertNotInAngularZone',
]);
const instanceMethods = new Set(['onMicrotaskEmpty', 'onStable']);
const ts = await (0, ts_utils_1.loadTypescript)();
ts.forEachChild(sourceFile, function visit(node) {
if (ts.isPropertyAccessExpression(node)) {
const propertyName = node.name.text;
const expressionText = node.expression.getText(sourceFile);
// Static: NgZone.method()
if (expressionText === ngZoneClassName && staticMethods.has(propertyName)) {
unsupportedUsages.push(node);
}
// Instance: zone.method() or this.zone.method()
if (instanceMethods.has(propertyName)) {
unsupportedUsages.push(node);
}
}
ts.forEachChild(node, visit);
});
return unsupportedUsages;
}
@@ -0,0 +1,12 @@
/**
* @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
*/
import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol';
import { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types';
import type { SourceFile } from 'typescript';
import { MigrationResponse } from './types';
export declare function migrateSingleFile(sourceFile: SourceFile, extras: RequestHandlerExtra<ServerRequest, ServerNotification>): Promise<MigrationResponse | null>;
@@ -0,0 +1,72 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.migrateSingleFile = migrateSingleFile;
const analyze_for_unsupported_zone_uses_1 = require("./analyze_for_unsupported_zone_uses");
const migrate_test_file_1 = require("./migrate_test_file");
const prompts_1 = require("./prompts");
const send_debug_message_1 = require("./send_debug_message");
const ts_utils_1 = require("./ts_utils");
async function migrateSingleFile(sourceFile, extras) {
const testBedSpecifier = await (0, ts_utils_1.getImportSpecifier)(sourceFile, '@angular/core/testing', 'TestBed');
const isTestFile = sourceFile.fileName.endsWith('.spec.ts') || !!testBedSpecifier;
if (isTestFile) {
return (0, migrate_test_file_1.migrateTestFile)(sourceFile);
}
const unsupportedZoneUseResponse = await (0, analyze_for_unsupported_zone_uses_1.analyzeForUnsupportedZoneUses)(sourceFile);
if (unsupportedZoneUseResponse) {
return unsupportedZoneUseResponse;
}
let detectedStrategy;
let hasComponentDecorator = false;
const componentSpecifier = await (0, ts_utils_1.getImportSpecifier)(sourceFile, '@angular/core', 'Component');
if (!componentSpecifier) {
(0, send_debug_message_1.sendDebugMessage)(`No component decorator found in file: ${sourceFile.fileName}`, extras);
return null;
}
const ts = await (0, ts_utils_1.loadTypescript)();
ts.forEachChild(sourceFile, function visit(node) {
if (detectedStrategy) {
return; // Already found, no need to traverse further
}
if (ts.isDecorator(node) && ts.isCallExpression(node.expression)) {
const callExpr = node.expression;
if (callExpr.expression.getText(sourceFile) === 'Component') {
hasComponentDecorator = true;
if (callExpr.arguments.length > 0 && ts.isObjectLiteralExpression(callExpr.arguments[0])) {
const componentMetadata = callExpr.arguments[0];
for (const prop of componentMetadata.properties) {
if (ts.isPropertyAssignment(prop) &&
prop.name.getText(sourceFile) === 'changeDetection') {
if (ts.isPropertyAccessExpression(prop.initializer) &&
prop.initializer.expression.getText(sourceFile) === 'ChangeDetectionStrategy') {
const strategy = prop.initializer.name.text;
if (strategy === 'OnPush' || strategy === 'Default') {
detectedStrategy = strategy;
return;
}
}
}
}
}
}
}
ts.forEachChild(node, visit);
});
if (!hasComponentDecorator ||
// component uses OnPush. We don't have anything more to do here.
detectedStrategy === 'OnPush' ||
// Explicit default strategy, assume there's a reason for it (already migrated, or is a library that hosts Default components) and skip.
detectedStrategy === 'Default') {
(0, send_debug_message_1.sendDebugMessage)(`Component decorator found with strategy: ${detectedStrategy} in file: ${sourceFile.fileName}. Skipping migration for file.`, extras);
return null;
}
// Component decorator found, but no change detection strategy.
return (0, prompts_1.generateZonelessMigrationInstructionsForComponent)(sourceFile.fileName);
}
@@ -0,0 +1,11 @@
/**
* @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
*/
import type { SourceFile } from 'typescript';
import { MigrationResponse } from './types';
export declare function migrateTestFile(sourceFile: SourceFile): Promise<MigrationResponse | null>;
export declare function searchForGlobalZoneless(startPath: string): Promise<boolean>;
@@ -0,0 +1,105 @@
"use strict";
/**
* @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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.migrateTestFile = migrateTestFile;
exports.searchForGlobalZoneless = searchForGlobalZoneless;
const fs = __importStar(require("node:fs"));
const promises_1 = require("node:fs/promises");
const node_path_1 = require("node:path");
const prompts_1 = require("./prompts");
const ts_utils_1 = require("./ts_utils");
async function migrateTestFile(sourceFile) {
const ts = await (0, ts_utils_1.loadTypescript)();
// Check if tests use zoneless either by default through `initTestEnvironment` or by explicitly calling `provideZonelessChangeDetection`.
let testsUseZonelessChangeDetection = await searchForGlobalZoneless(sourceFile.fileName);
if (!testsUseZonelessChangeDetection) {
ts.forEachChild(sourceFile, function visit(node) {
if (ts.isCallExpression(node) &&
node.expression.getText(sourceFile) === 'provideZonelessChangeDetection') {
testsUseZonelessChangeDetection = true;
return;
}
ts.forEachChild(node, visit);
});
}
if (!testsUseZonelessChangeDetection) {
// Tests do not use zoneless, so we provide instructions to set it up.
return (0, prompts_1.createProvideZonelessForTestsSetupPrompt)(sourceFile.fileName);
}
// At this point, tests are using zoneless, so we look for any explicit uses of `provideZoneChangeDetection` that need to be fixed.
return (0, prompts_1.createFixResponseForZoneTests)(sourceFile);
}
async function searchForGlobalZoneless(startPath) {
const angularJsonDir = findAngularJsonDir(startPath);
if (!angularJsonDir) {
// Cannot determine project root, fallback to original behavior or assume false.
// For now, let's assume no global setup if angular.json is not found.
return false;
}
try {
const files = (0, promises_1.glob)(`${angularJsonDir}/**/*.ts`);
for await (const file of files) {
const content = fs.readFileSync(file, 'utf-8');
if (content.includes('initTestEnvironment') &&
content.includes('provideZonelessChangeDetection')) {
return true;
}
}
}
catch (e) {
return false;
}
return false;
}
function findAngularJsonDir(startDir) {
let currentDir = startDir;
while (true) {
if (fs.existsSync((0, node_path_1.join)(currentDir, 'angular.json'))) {
return currentDir;
}
const parentDir = (0, node_path_1.dirname)(currentDir);
if (parentDir === currentDir) {
return null;
}
currentDir = parentDir;
}
}
@@ -0,0 +1,15 @@
/**
* @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
*/
import type { SourceFile } from 'typescript';
import { MigrationResponse } from './types';
export declare function createProvideZonelessForTestsSetupPrompt(testFilePath: string): MigrationResponse;
export declare function createUnsupportedZoneUsagesMessage(usages: string[], filePath: string): MigrationResponse;
export declare function generateZonelessMigrationInstructionsForComponent(filePath: string): MigrationResponse;
export declare function createTestDebuggingGuideForNonActionableInput(fileOrDirPath: string): MigrationResponse;
export declare function createFixResponseForZoneTests(sourceFile: SourceFile): Promise<MigrationResponse | null>;
export declare function createResponse(text: string): MigrationResponse;
+237
View File
@@ -0,0 +1,237 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.createProvideZonelessForTestsSetupPrompt = createProvideZonelessForTestsSetupPrompt;
exports.createUnsupportedZoneUsagesMessage = createUnsupportedZoneUsagesMessage;
exports.generateZonelessMigrationInstructionsForComponent = generateZonelessMigrationInstructionsForComponent;
exports.createTestDebuggingGuideForNonActionableInput = createTestDebuggingGuideForNonActionableInput;
exports.createFixResponseForZoneTests = createFixResponseForZoneTests;
exports.createResponse = createResponse;
const ts_utils_1 = require("./ts_utils");
/* eslint-disable max-len */
function createProvideZonelessForTestsSetupPrompt(testFilePath) {
const text = `You are an expert Angular developer assisting with a migration to zoneless. Your task is to update the test file at \`${testFilePath}\` to enable zoneless change detection and identify tests that are not yet compatible.
Follow these instructions precisely.
### Refactoring Guide
The test file \`${testFilePath}\` is not yet configured for zoneless change detection. You need to enable it for the entire test suite and then identify which specific tests fail.
#### Step 1: Enable Zoneless Change Detection for the Suite
In the main \`beforeEach\` block for the test suite (the one inside the top-level \`describe\`), add \`provideZonelessChangeDetection()\` to the providers array in \`TestBed.configureTestingModule\`.
* If there is already an import from \`@angular/core\`, add \`provideZonelessChangeDetection\` to the existing import.
* Otherwise, add a new import statement for \`provideZonelessChangeDetection\` from \`@angular/core\`.
\`\`\`diff
- import {{ SomeImport }} from '@angular/core';
+ import {{ SomeImport, provideZonelessChangeDetection }} from '@angular/core';
describe('MyComponent', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({providers: [provideZonelessChangeDetection()]});
+ });
});
\`\`\`
#### Step 2: Identify and fix Failing Tests
After enabling zoneless detection for the suite, some tests will likely fail. Your next task is to identify these failing tests and fix them.
${testDebuggingGuideText(testFilePath)}
8. **DO** add \`provideZonelessChangeDetection()\` _once_ to the top-most \`describe\` in a \`beforeEach\` block as instructed in Step 1.
9. **DO** run the tests after adding \`provideZonelessChangeDetection\` to see which ones fail. **DO NOT** make assumptions about which tests will might fail.
### Final Step
After you have applied all the required changes and followed all the rules, consult this tool again for the next steps in the migration process.`;
return createResponse(text);
}
function createUnsupportedZoneUsagesMessage(usages, filePath) {
const text = `You are an expert Angular developer assisting with a migration to zoneless. Your task is to refactor the component in ${filePath} to remove unsupported NgZone APIs.
The component uses NgZone APIs that are incompatible with zoneless applications. The only permitted NgZone APIs are \`NgZone.run\` and \`NgZone.runOutsideAngular\`.
The following usages are unsupported and must be fixed:
${usages.map((usage) => `- ${usage}`).join('\n')}
Follow these instructions precisely to refactor the code.
### Refactoring Guide
#### 1. APIs to Remove (No Replacement)
The following methods have no replacement in a zoneless context and must be removed entirely:
- \`NgZone.assertInAngularZone\`
- \`NgZone.assertNotInAngularZone\`
- \`NgZone.isInAngularZone\`
#### 2. APIs to Replace
The \`onMicrotaskEmpty\` and \`onStable\` observables must be replaced with modern Angular APIs.
- **For single-event subscriptions** (e.g., using \`.pipe(take(1))\` or \`.pipe(first())\`), use \`afterNextRender\` from \`@angular/core\`.
\`\`\`diff
- this.zone.onMicrotaskEmpty.pipe(take(1)).subscribe(() => {});
- this.zone.onStable.pipe(take(1)).subscribe(() => {});
+ import { afterNextRender, Injector } from '@angular/core';
+ afterNextRender(() => {}, {injector: this.injector});
\`\`\`
- **For continuous subscriptions**, use \`afterEveryRender\` from \`@angular/core\`.
\`\`\`diff
- this.zone.onMicrotaskEmpty.subscribe(() => {});
- this.zone.onStable.subscribe(() => {});
+ import { afterEveryRender, Injector } from '@angular/core';
+ afterEveryRender(() => {}, {injector: this.injector});
\`\`\`
- If the code checks \`this.zone.isStable\` before subscribing, you can remove the \`isStable\` check. \`afterNextRender\` handles this case correctly.
### IMPORTANT: Rules and Constraints
You must follow these rules without exception:
1. **DO NOT** make any changes to the component that are unrelated to removing the unsupported NgZone APIs listed above.
2. **DO NOT** remove or modify usages of \`NgZone.run\` or \`NgZone.runOutsideAngular\`. These are still required.
3. **DO** ensure that you replace \`onMicrotaskEmpty\` and \`onStable\` with the correct replacements (\`afterNextRender\` or \`afterEveryRender\`) as described in the guide.
4. **DO** add the necessary imports for \`afterNextRender\`, \`afterEveryRender\`, and \`Injector\` when you use them.
### Final Step
After you have applied all the required changes and followed all the rules, consult this tool again for the next steps in the migration process.
`;
return createResponse(text);
}
function generateZonelessMigrationInstructionsForComponent(filePath) {
const text = `You are an expert Angular developer assisting with a migration to zoneless. Your task is to refactor the component in \`${filePath}\` to be compatible with zoneless change detection by ensuring Angular is notified of all state changes that affect the view.
The component does not currently use a change detection strategy, which means it may rely on Zone.js. To prepare it for zoneless, you must manually trigger change detection when its state changes.
Follow these instructions precisely.
### Refactoring Guide
#### Step 1: Identify and Refactor State
Your primary goal is to ensure that every time a component property used in the template is updated, Angular knows it needs to run change detection.
1. **Identify Properties**: Find all component properties that are read by the template.
2. **Choose a Strategy**: For each property identified, choose one of the following refactoring strategies:
* **(Preferred) Convert to Signal**: The best approach is to convert the property to an Angular Signal. This is the most idiomatic and future-proof way to handle state in zoneless applications.
* **(Alternative) Use \`markForCheck()\`**: If converting to a signal is too complex or would require extensive refactoring, you can instead inject \`ChangeDetectorRef\` and call \`this.cdr.markForCheck()\` immediately after the property is updated.
#### Step 2: Add \`ChangeDetectionStrategy.Default\`
After you have refactored all necessary properties, you must update the component's decorator to explicitly set the change detection strategy.
1. Add \`ChangeDetectionStrategy\` to the import from \`@angular/core\`.
2. In the \`@Component\` decorator, add the property \`changeDetection: ChangeDetectionStrategy.Default\`.
3. Add a \`// TODO\` comment above this line explaining that the component should be fully migrated to \`OnPush\` after the application has been tested with these changes.
Example:
\`\`\`typescript
@Component({
...
// TODO: This component has been partially migrated to be zoneless-compatible.
// After testing, this should be updated to ChangeDetectionStrategy.OnPush.
changeDetection: ChangeDetectionStrategy.Default,
})
\`\`\`
### IMPORTANT: Rules and Constraints
You must follow these rules without exception:
1. **DO** apply one of the two refactoring strategies (signals or \`markForCheck()\`) for all relevant component properties.
2. **DO** add \`changeDetection: ChangeDetectionStrategy.Default\` with the specified TODO comment as the final code change.
3. **DO NOT** use \`ChangeDetectionStrategy.OnPush\`. This will be the next step in the migration, but it is not part of this task.
4. **DO NOT** modify properties that are already signals or are used with the \`async\` pipe in the template, as they are already zoneless-compatible.
5. **DO NOT** make any changes to files other than the component file at \`${filePath}\` and its direct template/style files if necessary.
6. **DO NOT** remove or modify usages of \`NgZone.run\` or \`NgZone.runOutsideAngular\`. These are still required.
### Final Step
After you have applied all the required changes and followed all the rules, consult this tool again for the next steps in the migration process.`;
return createResponse(text);
}
function createTestDebuggingGuideForNonActionableInput(fileOrDirPath) {
const text = `You are an expert Angular developer assisting with a migration to zoneless.
No actionable migration steps were found in the application code for \`${fileOrDirPath}\`. However, if the tests for this code are failing with zoneless enabled, the tests themselves likely need to be updated.
Your task is to investigate and fix any failing tests related to the code in \`${fileOrDirPath}\`.
${testDebuggingGuideText(fileOrDirPath)}
`;
return createResponse(text);
}
async function createFixResponseForZoneTests(sourceFile) {
const ts = await (0, ts_utils_1.loadTypescript)();
const usages = [];
ts.forEachChild(sourceFile, function visit(node) {
if (ts.isCallExpression(node) &&
node.expression.getText(sourceFile) === 'provideZoneChangeDetection') {
usages.push(node);
}
ts.forEachChild(node, visit);
});
if (usages.length === 0) {
// No usages of provideZoneChangeDetection found, so no fix needed.
return null;
}
const locations = usages.map((node) => {
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
return `line ${line + 1}, character ${character + 1}`;
});
const text = `You are an expert Angular developer assisting with a migration to zoneless. Your task is to update the test file at \`${sourceFile.fileName}\` to be fully zoneless-compatible.
The test suite has been partially migrated, but some tests were incompatible and are still using Zone.js-based change detection via \`provideZoneChangeDetection\`. You must refactor these tests to work in a zoneless environment and remove the \`provideZoneChangeDetection\` calls.
The following usages of \`provideZoneChangeDetection\` must be removed:
${locations.map((loc) => `- ${loc}`).join('\n')}
After removing \`provideZoneChangeDetection\`, the tests will likely fail. Use this guide to diagnose and fix the failures.
${testDebuggingGuideText(sourceFile.fileName)}
### Final Step
After you have applied all the required changes and followed all the rules, consult this tool again for the next steps in the migration process.`;
return createResponse(text);
}
function testDebuggingGuideText(fileName) {
return `
### Test Debugging Guide
1. **\`ExpressionChangedAfterItHasBeenCheckedError\`**:
* **Cause**: This error indicates that a value in a component's template was updated, but Angular was not notified to run change detection.
* **Solution**:
* If the value is in a test-only wrapper component, update the property to be a signal.
* For application components, either convert the property to a signal or call \`ChangeDetectorRef.markForCheck()\` immediately after the property is updated.
2. **Asynchronous Operations and Timing**:
* **Cause**: Without Zone.js, change detection is always scheduled asynchronously. Tests that previously relied on synchronous updates might now fail. The \`fixture.whenStable()\` utility also no longer waits for timers (like \`setTimeout\` or \`setInterval\`).
* **Solution**:
* Avoid relying on synchronous change detection.
* To wait for asynchronous operations to complete, you may need to poll for an expected state, use \`fakeAsync\` with \`tick()\`, or use a mock clock to flush timers.
3. **Indirect Dependencies**:
* **Cause**: The component itself might be zoneless-compatible, but it could be using a service or another dependency that is not.
* **Solution**: Investigate the services and dependencies used by the component and its tests. Run this tool on those dependencies to identify and fix any issues.
### IMPORTANT: Rules and Constraints
You must follow these rules without exception:
1. **DO** focus only on fixing the tests for the code in \`${fileName}\`.
2. **DO** remove all usages of \`provideZoneChangeDetection\` from the test file.
3. **DO** apply the solutions described in the debugging guide to fix any resulting test failures.
4. **DO** update properties of test components and directives to use signals. Tests often use plain objects and values and update the component state directly before calling \`fixture.detectChanges\`. This will not work and will result in \`ExpressionChangedAfterItHasBeenCheckedError\` because Angular was not notifed of the change.
5. **DO NOT** make changes to application code unless it is to fix a bug revealed by the zoneless migration (e.g., converting a property to a signal to fix an \`ExpressionChangedAfterItHasBeenCheckedError\`).
6. **DO NOT** make any changes unrelated to fixing the failing tests in \`${fileName}\`.
7. **DO NOT** re-introduce \`provideZoneChangeDetection()\` into tests that are already using \`provideZonelessChangeDetection()\`.`;
}
/* eslint-enable max-len */
function createResponse(text) {
return {
content: [{ type: 'text', text }],
};
}
@@ -0,0 +1,10 @@
/**
* @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
*/
import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol';
import { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types';
export declare function sendDebugMessage(message: string, { sendNotification }: RequestHandlerExtra<ServerRequest, ServerNotification>): void;
@@ -0,0 +1,19 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.sendDebugMessage = sendDebugMessage;
function sendDebugMessage(message, { sendNotification }) {
void sendNotification({
method: 'notifications/message',
params: {
level: 'debug',
data: message,
},
});
}
@@ -0,0 +1,36 @@
/**
* @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
*/
import type { ImportSpecifier, NodeArray, SourceFile } from 'typescript';
import type ts from 'typescript';
export declare function loadTypescript(): Promise<typeof ts>;
/**
* Gets a top-level import specifier with a specific name that is imported from a particular module.
* E.g. given a file that looks like:
*
* ```ts
* import { Component, Directive } from '@angular/core';
* import { Foo } from './foo';
* ```
*
* Calling `getImportSpecifier(sourceFile, '@angular/core', 'Directive')` will yield the node
* referring to `Directive` in the top import.
*
* @param sourceFile File in which to look for imports.
* @param moduleName Name of the import's module.
* @param specifierName Original name of the specifier to look for. Aliases will be resolved to
* their original name.
*/
export declare function getImportSpecifier(sourceFile: SourceFile, moduleName: string | RegExp, specifierName: string): Promise<ImportSpecifier | null>;
/**
* Finds an import specifier with a particular name.
* @param nodes Array of import specifiers to search through.
* @param specifierName Name of the specifier to look for.
*/
export declare function findImportSpecifier(nodes: NodeArray<ImportSpecifier>, specifierName: string): ImportSpecifier | undefined;
/** Creates a TypeScript source file from a file path. */
export declare function createSourceFile(file: string): Promise<SourceFile>;
+135
View File
@@ -0,0 +1,135 @@
"use strict";
/**
* @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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadTypescript = loadTypescript;
exports.getImportSpecifier = getImportSpecifier;
exports.findImportSpecifier = findImportSpecifier;
exports.createSourceFile = createSourceFile;
const fs = __importStar(require("node:fs"));
let typescriptModule;
async function loadTypescript() {
return (typescriptModule ??= await Promise.resolve().then(() => __importStar(require('typescript'))));
}
/**
* Gets a top-level import specifier with a specific name that is imported from a particular module.
* E.g. given a file that looks like:
*
* ```ts
* import { Component, Directive } from '@angular/core';
* import { Foo } from './foo';
* ```
*
* Calling `getImportSpecifier(sourceFile, '@angular/core', 'Directive')` will yield the node
* referring to `Directive` in the top import.
*
* @param sourceFile File in which to look for imports.
* @param moduleName Name of the import's module.
* @param specifierName Original name of the specifier to look for. Aliases will be resolved to
* their original name.
*/
async function getImportSpecifier(sourceFile, moduleName, specifierName) {
return (getImportSpecifiers(sourceFile, moduleName, specifierName, await loadTypescript())[0] ?? null);
}
/**
* Gets top-level import specifiers with specific names that are imported from a particular module.
* E.g. given a file that looks like:
*
* ```ts
* import { Component, Directive } from '@angular/core';
* import { Foo } from './foo';
* ```
*
* Calling `getImportSpecifiers(sourceFile, '@angular/core', ['Directive', 'Component'])` will
* yield the nodes referring to `Directive` and `Component` in the top import.
*
* @param sourceFile File in which to look for imports.
* @param moduleName Name of the import's module.
* @param specifierOrSpecifiers Original name of the specifier to look for, or an array of such
* names. Aliases will be resolved to their original name.
*/
function getImportSpecifiers(sourceFile, moduleName, specifierOrSpecifiers, { isNamedImports, isImportDeclaration, isStringLiteral }) {
const matches = [];
for (const node of sourceFile.statements) {
if (!isImportDeclaration(node) || !isStringLiteral(node.moduleSpecifier)) {
continue;
}
const namedBindings = node.importClause?.namedBindings;
const isMatch = typeof moduleName === 'string'
? node.moduleSpecifier.text === moduleName
: moduleName.test(node.moduleSpecifier.text);
if (!isMatch || !namedBindings || !isNamedImports(namedBindings)) {
continue;
}
if (typeof specifierOrSpecifiers === 'string') {
const match = findImportSpecifier(namedBindings.elements, specifierOrSpecifiers);
if (match) {
matches.push(match);
}
}
else {
for (const specifierName of specifierOrSpecifiers) {
const match = findImportSpecifier(namedBindings.elements, specifierName);
if (match) {
matches.push(match);
}
}
}
}
return matches;
}
/**
* Finds an import specifier with a particular name.
* @param nodes Array of import specifiers to search through.
* @param specifierName Name of the specifier to look for.
*/
function findImportSpecifier(nodes, specifierName) {
return nodes.find((element) => {
const { name, propertyName } = element;
return propertyName ? propertyName.text === specifierName : name.text === specifierName;
});
}
/** Creates a TypeScript source file from a file path. */
async function createSourceFile(file) {
const content = fs.readFileSync(file, 'utf8');
const ts = await loadTypescript();
return ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
}
+13
View File
@@ -0,0 +1,13 @@
/**
* @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
*/
export type MigrationResponse = {
content: {
type: 'text';
text: string;
}[];
};
+9
View File
@@ -0,0 +1,9 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,14 @@
/**
* @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
*/
import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol';
import { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types';
import { z } from 'zod';
export declare const ZONELESS_MIGRATION_TOOL: import("../tool-registry").McpToolDeclaration<{
fileOrDirPath: z.ZodString;
}, z.ZodRawShape>;
export declare function registerZonelessMigrationTool(fileOrDirPath: string, extras: RequestHandlerExtra<ServerRequest, ServerNotification>): Promise<import("./types").MigrationResponse>;
@@ -0,0 +1,205 @@
"use strict";
/**
* @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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZONELESS_MIGRATION_TOOL = void 0;
exports.registerZonelessMigrationTool = registerZonelessMigrationTool;
const fs = __importStar(require("node:fs"));
const promises_1 = require("node:fs/promises");
const zod_1 = require("zod");
const tool_registry_1 = require("../tool-registry");
const analyze_for_unsupported_zone_uses_1 = require("./analyze_for_unsupported_zone_uses");
const migrate_single_file_1 = require("./migrate_single_file");
const migrate_test_file_1 = require("./migrate_test_file");
const prompts_1 = require("./prompts");
const send_debug_message_1 = require("./send_debug_message");
const ts_utils_1 = require("./ts_utils");
exports.ZONELESS_MIGRATION_TOOL = (0, tool_registry_1.declareTool)({
name: 'onpush-zoneless-migration',
title: 'Plan migration to OnPush and/or zoneless',
description: `
<Purpose>
Analyzes Angular code and provides a step-by-step, iterative plan to migrate it to \`OnPush\`
change detection, a prerequisite for a zoneless application. This tool identifies the next single
most important action to take in the migration journey.
</Purpose>
<Use Cases>
* **Step-by-Step Migration:** Running the tool repeatedly to get the next instruction for a full
migration to \`OnPush\`.
* **Pre-Migration Analysis:** Checking a component or directory for unsupported \`NgZone\` APIs that
would block a zoneless migration.
* **Generating Component Migrations:** Getting the exact instructions for converting a single
component from the default change detection strategy to \`OnPush\`.
</Use Cases>
<Operational Notes>
* **Execution Model:** This tool **DOES NOT** modify code. It **PROVIDES INSTRUCTIONS** for a
single action at a time. You **MUST** apply the changes it suggests, and then run the tool
again to get the next step.
* **Iterative Process:** The migration process is iterative. You must call this tool repeatedly,
applying the suggested fix after each call, until the tool indicates that no more actions are
needed.
* **Relationship to \`modernize\`:** This tool is the specialized starting point for the zoneless/OnPush
migration. For other migrations (like signal inputs), you should use the \`modernize\` tool first,
as the zoneless migration may depend on them as prerequisites.
* **Input:** The tool can operate on either a single file or an entire directory. Provide the
absolute path.
</Operational Notes>`,
isReadOnly: true,
isLocalOnly: true,
inputSchema: {
fileOrDirPath: zod_1.z
.string()
.describe('The absolute path of the directory or file with the component(s), directive(s), or service(s) to migrate.' +
' The contents are read with fs.readFileSync.'),
},
factory: () => ({ fileOrDirPath }, requestHandlerExtra) => registerZonelessMigrationTool(fileOrDirPath, requestHandlerExtra),
});
async function registerZonelessMigrationTool(fileOrDirPath, extras) {
let files = [];
const componentTestFiles = new Set();
const filesWithComponents = new Set();
const zoneFiles = new Set();
if (fs.statSync(fileOrDirPath).isDirectory()) {
const allFiles = (0, promises_1.glob)(`${fileOrDirPath}/**/*.ts`);
for await (const file of allFiles) {
files.push(await (0, ts_utils_1.createSourceFile)(file));
}
}
else {
files = [await (0, ts_utils_1.createSourceFile)(fileOrDirPath)];
const maybeTestFile = await getTestFilePath(fileOrDirPath);
if (maybeTestFile) {
componentTestFiles.add(await (0, ts_utils_1.createSourceFile)(maybeTestFile));
}
}
for (const sourceFile of files) {
const content = sourceFile.getFullText();
const componentSpecifier = await (0, ts_utils_1.getImportSpecifier)(sourceFile, '@angular/core', 'Component');
const zoneSpecifier = await (0, ts_utils_1.getImportSpecifier)(sourceFile, '@angular/core', 'NgZone');
const testBedSpecifier = await (0, ts_utils_1.getImportSpecifier)(sourceFile, /(@angular\/core)?\/testing/, 'TestBed');
if (testBedSpecifier) {
componentTestFiles.add(sourceFile);
}
else if (componentSpecifier) {
if (!content.includes('changeDetectionStrategy: ChangeDetectionStrategy.OnPush') &&
!content.includes('changeDetectionStrategy: ChangeDetectionStrategy.Default')) {
filesWithComponents.add(sourceFile);
}
else {
(0, send_debug_message_1.sendDebugMessage)(`Component file already has change detection strategy: ${sourceFile.fileName}. Skipping migration.`, extras);
}
const testFilePath = await getTestFilePath(sourceFile.fileName);
if (testFilePath) {
componentTestFiles.add(await (0, ts_utils_1.createSourceFile)(testFilePath));
}
}
else if (zoneSpecifier) {
zoneFiles.add(sourceFile);
}
}
if (zoneFiles.size > 0) {
for (const file of zoneFiles) {
const result = await (0, analyze_for_unsupported_zone_uses_1.analyzeForUnsupportedZoneUses)(file);
if (result !== null) {
return result;
}
}
}
if (filesWithComponents.size > 0) {
const rankedFiles = filesWithComponents.size > 1
? await rankComponentFilesForMigration(extras, Array.from(filesWithComponents))
: Array.from(filesWithComponents);
for (const file of rankedFiles) {
const result = await (0, migrate_single_file_1.migrateSingleFile)(file, extras);
if (result !== null) {
return result;
}
}
}
for (const file of componentTestFiles) {
const result = await (0, migrate_test_file_1.migrateTestFile)(file);
if (result !== null) {
return result;
}
}
return (0, prompts_1.createTestDebuggingGuideForNonActionableInput)(fileOrDirPath);
}
async function rankComponentFilesForMigration({ sendRequest }, componentFiles) {
try {
const response = await sendRequest({
method: 'sampling/createMessage',
params: {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `The following files are components that need to be migrated to OnPush change detection.` +
` Please rank them based on which ones are most likely to be shared or common components.` +
` The most likely shared component should be first.
${componentFiles.map((f) => f.fileName).join('\n ')}
Respond ONLY with the ranked list of files, one file per line.`,
},
},
],
systemPrompt: 'You are a helpful assistant that helps migrate identify shared Angular components.',
maxTokens: 2000,
},
}, zod_1.z.object({ sortedFiles: zod_1.z.array(zod_1.z.string()) }));
const rankedFiles = response.sortedFiles
.map((line) => line.trim())
.map((fileName) => componentFiles.find((f) => f.fileName === fileName))
.filter((f) => !!f);
// Ensure the ranking didn't mess up the list of files
if (rankedFiles.length === componentFiles.length) {
return rankedFiles;
}
}
catch { }
return componentFiles; // Fallback to original order if the response fails
}
async function getTestFilePath(filePath) {
const testFilePath = filePath.replace(/\.ts$/, '.spec.ts');
if (fs.existsSync(testFilePath)) {
return testFilePath;
}
return undefined;
}
+29
View File
@@ -0,0 +1,29 @@
/**
* @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
*/
import z from 'zod';
export declare const LIST_PROJECTS_TOOL: import("./tool-registry").McpToolDeclaration<z.ZodRawShape, {
projects: z.ZodArray<z.ZodObject<{
name: z.ZodString;
type: z.ZodOptional<z.ZodEnum<["application", "library"]>>;
root: z.ZodString;
sourceRoot: z.ZodString;
selectorPrefix: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
name: string;
root: string;
sourceRoot: string;
type?: "application" | "library" | undefined;
selectorPrefix?: string | undefined;
}, {
name: string;
root: string;
sourceRoot: string;
type?: "application" | "library" | undefined;
selectorPrefix?: string | undefined;
}>, "many">;
}>;
+87
View File
@@ -0,0 +1,87 @@
"use strict";
/**
* @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 __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LIST_PROJECTS_TOOL = void 0;
const node_path_1 = __importDefault(require("node:path"));
const zod_1 = __importDefault(require("zod"));
const tool_registry_1 = require("./tool-registry");
exports.LIST_PROJECTS_TOOL = (0, tool_registry_1.declareTool)({
name: 'list_projects',
title: 'List Angular Projects',
description: 'Lists the names of all applications and libraries defined within an Angular workspace. ' +
'It reads the `angular.json` configuration file to identify the projects. ',
outputSchema: {
projects: zod_1.default.array(zod_1.default.object({
name: zod_1.default
.string()
.describe('The name of the project, as defined in the `angular.json` file.'),
type: zod_1.default
.enum(['application', 'library'])
.optional()
.describe(`The type of the project, either 'application' or 'library'.`),
root: zod_1.default
.string()
.describe('The root directory of the project, relative to the workspace root.'),
sourceRoot: zod_1.default
.string()
.describe(`The root directory of the project's source files, relative to the workspace root.`),
selectorPrefix: zod_1.default
.string()
.optional()
.describe('The prefix to use for component selectors.' +
` For example, a prefix of 'app' would result in selectors like '<app-my-component>'.`),
})),
},
isReadOnly: true,
isLocalOnly: true,
shouldRegister: (context) => !!context.workspace,
factory: createListProjectsHandler,
});
function createListProjectsHandler({ workspace }) {
return async () => {
if (!workspace) {
return {
content: [
{
type: 'text',
text: 'No Angular workspace found.' +
' An `angular.json` file, which marks the root of a workspace,' +
' could not be located in the current directory or any of its parent directories.',
},
],
structuredContent: { projects: [] },
};
}
const projects = [];
// Convert to output format
for (const [name, project] of workspace.projects.entries()) {
projects.push({
name,
type: project.extensions['projectType'],
root: project.root,
sourceRoot: project.sourceRoot ?? node_path_1.default.posix.join(project.root, 'src'),
selectorPrefix: project.extensions['prefix'],
});
}
// The structuredContent field is newer and may not be supported by all hosts.
// A text representation of the content is also provided for compatibility.
return {
content: [
{
type: 'text',
text: `Projects in the Angular workspace:\n${JSON.stringify(projects)}`,
},
],
structuredContent: { projects },
};
};
}
+35
View File
@@ -0,0 +1,35 @@
/**
* @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
*/
import type { McpServer, ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ZodRawShape } from 'zod';
import type { AngularWorkspace } from '../../../utilities/config';
type ToolConfig = Parameters<McpServer['registerTool']>[1];
export interface McpToolContext {
workspace?: AngularWorkspace;
logger: {
warn(text: string): void;
};
exampleDatabasePath?: string;
}
export type McpToolFactory<TInput extends ZodRawShape> = (context: McpToolContext) => ToolCallback<TInput> | Promise<ToolCallback<TInput>>;
export interface McpToolDeclaration<TInput extends ZodRawShape, TOutput extends ZodRawShape> {
name: string;
title?: string;
description: string;
annotations?: ToolConfig['annotations'];
inputSchema?: TInput;
outputSchema?: TOutput;
factory: McpToolFactory<TInput>;
shouldRegister?: (context: McpToolContext) => boolean | Promise<boolean>;
isReadOnly?: boolean;
isLocalOnly?: boolean;
}
export type AnyMcpToolDeclaration = McpToolDeclaration<any, any>;
export declare function declareTool<TInput extends ZodRawShape, TOutput extends ZodRawShape>(declaration: McpToolDeclaration<TInput, TOutput>): McpToolDeclaration<TInput, TOutput>;
export declare function registerTools(server: McpServer, context: McpToolContext, declarations: AnyMcpToolDeclaration[]): Promise<void>;
export {};
+33
View File
@@ -0,0 +1,33 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.declareTool = declareTool;
exports.registerTools = registerTools;
function declareTool(declaration) {
return declaration;
}
async function registerTools(server, context, declarations) {
for (const declaration of declarations) {
if (declaration.shouldRegister && !(await declaration.shouldRegister(context))) {
continue;
}
const { name, factory, shouldRegister, isReadOnly, isLocalOnly, ...config } = declaration;
const handler = await factory(context);
// Add declarative characteristics to annotations
config.annotations ??= {};
if (isReadOnly !== undefined) {
config.annotations.readOnlyHint = isReadOnly;
}
if (isLocalOnly !== undefined) {
// openWorldHint: false means local only
config.annotations.openWorldHint = !isLocalOnly;
}
server.registerTool(name, config, handler);
}
}
+27
View File
@@ -0,0 +1,27 @@
/**
* @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
*/
import { Argv } from 'yargs';
import { CommandModuleImplementation, CommandScope, Options, OtherOptions } from '../../command-builder/command-module';
import { SchematicsCommandArgs, SchematicsCommandModule } from '../../command-builder/schematics-command-module';
interface NewCommandArgs extends SchematicsCommandArgs {
collection?: string;
}
export default class NewCommandModule extends SchematicsCommandModule implements CommandModuleImplementation<NewCommandArgs> {
private readonly schematicName;
scope: CommandScope;
protected allowPrivateSchematics: boolean;
command: string;
aliases: string[] | undefined;
describe: string;
longDescriptionPath: string;
builder(argv: Argv): Promise<Argv<NewCommandArgs>>;
run(options: Options<NewCommandArgs> & OtherOptions): Promise<number | void>;
/** Find a collection from config that has an `ng-new` schematic. */
private getCollectionFromConfig;
}
export {};
+74
View File
@@ -0,0 +1,74 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const node_path_1 = require("node:path");
const command_module_1 = require("../../command-builder/command-module");
const schematics_command_module_1 = require("../../command-builder/schematics-command-module");
const version_1 = require("../../utilities/version");
const command_config_1 = require("../command-config");
class NewCommandModule extends schematics_command_module_1.SchematicsCommandModule {
schematicName = 'ng-new';
scope = command_module_1.CommandScope.Out;
allowPrivateSchematics = true;
command = 'new [name]';
aliases = command_config_1.RootCommands['new'].aliases;
describe = 'Creates a new Angular workspace.';
longDescriptionPath = (0, node_path_1.join)(__dirname, 'long-description.md');
async builder(argv) {
const localYargs = (await super.builder(argv)).option('collection', {
alias: 'c',
describe: 'A collection of schematics to use in generating the initial application.',
type: 'string',
});
const { options: { collection: collectionNameFromArgs }, } = this.context.args;
const collectionName = typeof collectionNameFromArgs === 'string'
? collectionNameFromArgs
: await this.getCollectionFromConfig();
const workflow = this.getOrCreateWorkflowForBuilder(collectionName);
const collection = workflow.engine.createCollection(collectionName);
const options = await this.getSchematicOptions(collection, this.schematicName, workflow);
return this.addSchemaOptionsToCommand(localYargs, options);
}
async run(options) {
// Register the version of the CLI in the registry.
const collectionName = options.collection ?? (await this.getCollectionFromConfig());
const { dryRun, force, interactive, defaults, collection, ...schematicOptions } = options;
const workflow = await this.getOrCreateWorkflowForExecution(collectionName, {
dryRun,
force,
interactive,
defaults,
});
workflow.registry.addSmartDefaultProvider('ng-cli-version', () => version_1.VERSION.full);
return this.runSchematic({
collectionName,
schematicName: this.schematicName,
schematicOptions,
executionOptions: {
dryRun,
force,
interactive,
defaults,
},
});
}
/** Find a collection from config that has an `ng-new` schematic. */
async getCollectionFromConfig() {
for (const collectionName of await this.getSchematicCollections()) {
const workflow = this.getOrCreateWorkflowForBuilder(collectionName);
const collection = workflow.engine.createCollection(collectionName);
const schematicsInCollection = collection.description.schematics;
if (Object.keys(schematicsInCollection).includes(this.schematicName)) {
return collectionName;
}
}
return schematics_command_module_1.DEFAULT_SCHEMATICS_COLLECTION;
}
}
exports.default = NewCommandModule;
+15
View File
@@ -0,0 +1,15 @@
Creates and initializes a new Angular application that is the default project for a new workspace.
Provides interactive prompts for optional configuration, such as adding routing support.
All prompts can safely be allowed to default.
- The new workspace folder is given the specified project name, and contains configuration files at the top level.
- By default, the files for a new initial application (with the same name as the workspace) are placed in the `src/` subfolder.
- The new application's configuration appears in the `projects` section of the `angular.json` workspace configuration file, under its project name.
- Subsequent applications that you generate in the workspace reside in the `projects/` subfolder.
If you plan to have multiple applications in the workspace, you can create an empty workspace by using the `--no-create-application` option.
You can then use `ng generate application` to create an initial application.
This allows a workspace name different from the initial app name, and ensures that all applications reside in the `/projects` subfolder, matching the structure of the configuration file.
+25
View File
@@ -0,0 +1,25 @@
/**
* @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
*/
import { Target } from '@angular-devkit/architect';
import { Argv } from 'yargs';
import { ArchitectBaseCommandModule } from '../../command-builder/architect-base-command-module';
import { CommandModuleImplementation, CommandScope, Options, OtherOptions } from '../../command-builder/command-module';
export interface RunCommandArgs {
target: string;
}
export default class RunCommandModule extends ArchitectBaseCommandModule<RunCommandArgs> implements CommandModuleImplementation<RunCommandArgs> {
scope: CommandScope;
command: string;
describe: string;
longDescriptionPath: string;
builder(argv: Argv): Promise<Argv<RunCommandArgs>>;
run(options: Options<RunCommandArgs> & OtherOptions): Promise<number>;
protected makeTargetSpecifier(options?: Options<RunCommandArgs>): Target | undefined;
/** @returns a sorted list of target specifiers to be used for auto completion. */
private getTargetChoices;
}
+88
View File
@@ -0,0 +1,88 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const node_path_1 = require("node:path");
const architect_base_command_module_1 = require("../../command-builder/architect-base-command-module");
const command_module_1 = require("../../command-builder/command-module");
class RunCommandModule extends architect_base_command_module_1.ArchitectBaseCommandModule {
scope = command_module_1.CommandScope.In;
command = 'run <target>';
describe = 'Runs an Architect target with an optional custom builder configuration defined in your project.';
longDescriptionPath = (0, node_path_1.join)(__dirname, 'long-description.md');
async builder(argv) {
const { jsonHelp, getYargsCompletions, help } = this.context.args.options;
const localYargs = argv
.positional('target', {
describe: 'The Architect target to run provided in the following format `project:target[:configuration]`.',
type: 'string',
demandOption: true,
// Show only in when using --help and auto completion because otherwise comma seperated configuration values will be invalid.
// Also, hide choices from JSON help so that we don't display them in AIO.
choices: (getYargsCompletions || help) && !jsonHelp ? this.getTargetChoices() : undefined,
})
.middleware((args) => {
// TODO: remove in version 15.
const { configuration, target } = args;
if (typeof configuration === 'string' && target) {
const targetWithConfig = target.split(':', 2);
targetWithConfig.push(configuration);
throw new command_module_1.CommandModuleError('Unknown argument: configuration.\n' +
`Provide the configuration as part of the target 'ng run ${targetWithConfig.join(':')}'.`);
}
}, true)
.strict();
const target = this.makeTargetSpecifier();
if (!target) {
return localYargs;
}
const schemaOptions = await this.getArchitectTargetOptions(target);
return this.addSchemaOptionsToCommand(localYargs, schemaOptions);
}
async run(options) {
const target = this.makeTargetSpecifier(options);
const { target: _target, ...extraOptions } = options;
if (!target) {
throw new command_module_1.CommandModuleError('Cannot determine project or target.');
}
return this.runSingleTarget(target, extraOptions);
}
makeTargetSpecifier(options) {
const architectTarget = options?.target ?? this.context.args.positional[1];
if (!architectTarget) {
return undefined;
}
const [project = '', target = '', configuration] = architectTarget.split(':');
return {
project,
target,
configuration,
};
}
/** @returns a sorted list of target specifiers to be used for auto completion. */
getTargetChoices() {
if (!this.context.workspace) {
return;
}
const targets = [];
for (const [projectName, project] of this.context.workspace.projects) {
for (const [targetName, target] of project.targets) {
const currentTarget = `${projectName}:${targetName}`;
targets.push(currentTarget);
if (!target.configurations) {
continue;
}
for (const configName of Object.keys(target.configurations)) {
targets.push(`${currentTarget}:${configName}`);
}
}
}
return targets.sort();
}
}
exports.default = RunCommandModule;
+10
View File
@@ -0,0 +1,10 @@
Architect is the tool that the CLI uses to perform complex tasks such as compilation, according to provided configurations.
The CLI commands run Architect targets such as `build`, `serve`, `test`, and `lint`.
Each named target has a default configuration, specified by an `options` object,
and an optional set of named alternate configurations in the `configurations` object.
For example, the `serve` target for a newly generated app has a predefined
alternate configuration named `production`.
You can define new targets and their configuration options in the `architect` section
of the `angular.json` file which you can run them from the command line using the `ng run` command.
+16
View File
@@ -0,0 +1,16 @@
/**
* @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
*/
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
import { CommandModuleImplementation } from '../../command-builder/command-module';
export default class ServeCommandModule extends ArchitectCommandModule implements CommandModuleImplementation {
multiTarget: boolean;
command: string;
aliases: string[] | undefined;
describe: string;
longDescriptionPath?: string | undefined;
}
+19
View File
@@ -0,0 +1,19 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const architect_command_module_1 = require("../../command-builder/architect-command-module");
const command_config_1 = require("../command-config");
class ServeCommandModule extends architect_command_module_1.ArchitectCommandModule {
multiTarget = false;
command = 'serve [project]';
aliases = command_config_1.RootCommands['serve'].aliases;
describe = 'Builds and serves your application, rebuilding on file changes.';
longDescriptionPath;
}
exports.default = ServeCommandModule;
+16
View File
@@ -0,0 +1,16 @@
/**
* @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
*/
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
import { CommandModuleImplementation } from '../../command-builder/command-module';
export default class TestCommandModule extends ArchitectCommandModule implements CommandModuleImplementation {
multiTarget: boolean;
command: string;
aliases: string[] | undefined;
describe: string;
longDescriptionPath: string;
}
+20
View File
@@ -0,0 +1,20 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const node_path_1 = require("node:path");
const architect_command_module_1 = require("../../command-builder/architect-command-module");
const command_config_1 = require("../command-config");
class TestCommandModule extends architect_command_module_1.ArchitectCommandModule {
multiTarget = true;
command = 'test [project]';
aliases = command_config_1.RootCommands['test'].aliases;
describe = 'Runs unit tests in a project.';
longDescriptionPath = (0, node_path_1.join)(__dirname, 'long-description.md');
}
exports.default = TestCommandModule;
+2
View File
@@ -0,0 +1,2 @@
Takes the name of the project, as specified in the `projects` section of the `angular.json` workspace configuration file.
When a project name is not supplied, it will execute for all projects.
+58
View File
@@ -0,0 +1,58 @@
/**
* @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
*/
import { Argv } from 'yargs';
import { CommandModule, CommandScope, Options } from '../../command-builder/command-module';
interface UpdateCommandArgs {
packages?: string[];
force: boolean;
next: boolean;
'migrate-only'?: boolean;
name?: string;
from?: string;
to?: string;
'allow-dirty': boolean;
verbose: boolean;
'create-commits': boolean;
}
export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs> {
scope: CommandScope;
protected shouldReportAnalytics: boolean;
private readonly resolvePaths;
command: string;
describe: string;
longDescriptionPath: string;
builder(localYargs: Argv): Argv<UpdateCommandArgs>;
run(options: Options<UpdateCommandArgs>): Promise<number | void>;
private executeSchematic;
/**
* @return Whether or not the migration was performed successfully.
*/
private executeMigration;
/**
* @return Whether or not the migrations were performed successfully.
*/
private executeMigrations;
private executePackageMigrations;
private migrateOnly;
private updatePackagesAndMigrate;
/**
* @return Whether or not the commit was successful.
*/
private commit;
private checkCleanGit;
/**
* Checks if the current installed CLI version is older or newer than a compatible version.
* @returns the version to install or null when there is no update to install.
*/
private checkCLIVersion;
private getCLIUpdateRunnerVersion;
private runTempBinary;
private packageManagerForce;
private getOptionalMigrationsToRun;
}
export {};
+922
View File
@@ -0,0 +1,922 @@
"use strict";
/**
* @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 schematics_1 = require("@angular-devkit/schematics");
const tools_1 = require("@angular-devkit/schematics/tools");
const listr2_1 = require("listr2");
const node_child_process_1 = require("node:child_process");
const node_fs_1 = require("node:fs");
const node_module_1 = require("node:module");
const path = __importStar(require("node:path"));
const node_path_1 = require("node:path");
const npm_package_arg_1 = __importDefault(require("npm-package-arg"));
const semver = __importStar(require("semver"));
const workspace_schema_1 = require("../../../lib/config/workspace-schema");
const command_module_1 = require("../../command-builder/command-module");
const schematic_engine_host_1 = require("../../command-builder/utilities/schematic-engine-host");
const schematic_workflow_1 = require("../../command-builder/utilities/schematic-workflow");
const color_1 = require("../../utilities/color");
const environment_options_1 = require("../../utilities/environment-options");
const error_1 = require("../../utilities/error");
const log_file_1 = require("../../utilities/log-file");
const package_metadata_1 = require("../../utilities/package-metadata");
const package_tree_1 = require("../../utilities/package-tree");
const prompt_1 = require("../../utilities/prompt");
const tty_1 = require("../../utilities/tty");
const version_1 = require("../../utilities/version");
class CommandError extends Error {
}
const ANGULAR_PACKAGES_REGEXP = /^@(?:angular|nguniversal)\//;
const UPDATE_SCHEMATIC_COLLECTION = path.join(__dirname, 'schematic/collection.json');
class UpdateCommandModule extends command_module_1.CommandModule {
scope = command_module_1.CommandScope.In;
shouldReportAnalytics = false;
resolvePaths = [__dirname, this.context.root];
command = 'update [packages..]';
describe = 'Updates your workspace and its dependencies. See https://update.angular.dev/.';
longDescriptionPath = (0, node_path_1.join)(__dirname, 'long-description.md');
builder(localYargs) {
return localYargs
.positional('packages', {
description: 'The names of package(s) to update.',
type: 'string',
array: true,
})
.option('force', {
description: 'Ignore peer dependency version mismatches.',
type: 'boolean',
default: false,
})
.option('next', {
description: 'Use the prerelease version, including beta and RCs.',
type: 'boolean',
default: false,
})
.option('migrate-only', {
description: 'Only perform a migration, do not update the installed version.',
type: 'boolean',
})
.option('name', {
description: 'The name of the migration to run. Only available when a single package is updated.',
type: 'string',
conflicts: ['to', 'from'],
})
.option('from', {
description: 'Version from which to migrate from. ' +
`Only available when a single package is updated, and only with 'migrate-only'.`,
type: 'string',
implies: ['migrate-only'],
conflicts: ['name'],
})
.option('to', {
describe: 'Version up to which to apply migrations. Only available when a single package is updated, ' +
`and only with 'migrate-only' option. Requires 'from' to be specified. Default to the installed version detected.`,
type: 'string',
implies: ['from', 'migrate-only'],
conflicts: ['name'],
})
.option('allow-dirty', {
describe: 'Whether to allow updating when the repository contains modified or untracked files.',
type: 'boolean',
default: false,
})
.option('verbose', {
describe: 'Display additional details about internal operations during execution.',
type: 'boolean',
default: false,
})
.option('create-commits', {
describe: 'Create source control commits for updates and migrations.',
type: 'boolean',
alias: ['C'],
default: false,
})
.middleware((argv) => {
if (argv.name) {
argv['migrate-only'] = true;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return argv;
})
.check(({ packages, 'allow-dirty': allowDirty, 'migrate-only': migrateOnly }) => {
const { logger } = this.context;
// This allows the user to easily reset any changes from the update.
if (packages?.length && !this.checkCleanGit()) {
if (allowDirty) {
logger.warn('Repository is not clean. Update changes will be mixed with pre-existing changes.');
}
else {
throw new command_module_1.CommandModuleError('Repository is not clean. Please commit or stash any changes before updating.');
}
}
if (migrateOnly) {
if (packages?.length !== 1) {
throw new command_module_1.CommandModuleError(`A single package must be specified when using the 'migrate-only' option.`);
}
}
return true;
})
.strict();
}
async run(options) {
const { logger, packageManager } = this.context;
// Check if the current installed CLI version is older than the latest compatible version.
// Skip when running `ng update` without a package name as this will not trigger an actual update.
if (!environment_options_1.disableVersionCheck && options.packages?.length) {
const cliVersionToInstall = await this.checkCLIVersion(options.packages, options.verbose, options.next);
if (cliVersionToInstall) {
logger.warn('The installed Angular CLI version is outdated.\n' +
`Installing a temporary Angular CLI versioned ${cliVersionToInstall} to perform the update.`);
return this.runTempBinary(`@angular/cli@${cliVersionToInstall}`, process.argv.slice(2));
}
}
const packages = [];
for (const request of options.packages ?? []) {
try {
const packageIdentifier = (0, npm_package_arg_1.default)(request);
// only registry identifiers are supported
if (!packageIdentifier.registry) {
logger.error(`Package '${request}' is not a registry package identifer.`);
return 1;
}
if (packages.some((v) => v.name === packageIdentifier.name)) {
logger.error(`Duplicate package '${packageIdentifier.name}' specified.`);
return 1;
}
if (options.migrateOnly && packageIdentifier.rawSpec !== '*') {
logger.warn('Package specifier has no effect when using "migrate-only" option.');
}
// Wildcard uses the next tag if next option is used otherwise use latest tag.
// Wildcard is present if no selector is provided on the command line.
if (packageIdentifier.rawSpec === '*') {
packageIdentifier.fetchSpec = options.next ? 'next' : 'latest';
packageIdentifier.type = 'tag';
}
packages.push(packageIdentifier);
}
catch (e) {
(0, error_1.assertIsError)(e);
logger.error(e.message);
return 1;
}
}
logger.info(`Using package manager: ${color_1.colors.gray(packageManager.name)}`);
logger.info('Collecting installed dependencies...');
const rootDependencies = await (0, package_tree_1.getProjectDependencies)(this.context.root);
logger.info(`Found ${rootDependencies.size} dependencies.`);
const workflow = new tools_1.NodeWorkflow(this.context.root, {
packageManager: packageManager.name,
packageManagerForce: this.packageManagerForce(options.verbose),
// __dirname -> favor @schematics/update from this package
// Otherwise, use packages from the active workspace (migrations)
resolvePaths: this.resolvePaths,
schemaValidation: true,
engineHostCreator: (options) => new schematic_engine_host_1.SchematicEngineHost(options.resolvePaths),
});
if (packages.length === 0) {
// Show status
const { success } = await this.executeSchematic(workflow, UPDATE_SCHEMATIC_COLLECTION, 'update', {
force: options.force,
next: options.next,
verbose: options.verbose,
packageManager: packageManager.name,
packages: [],
});
return success ? 0 : 1;
}
return options.migrateOnly
? this.migrateOnly(workflow, (options.packages ?? [])[0], rootDependencies, options)
: this.updatePackagesAndMigrate(workflow, rootDependencies, options, packages);
}
async executeSchematic(workflow, collection, schematic, options = {}) {
const { logger } = this.context;
const workflowSubscription = (0, schematic_workflow_1.subscribeToWorkflow)(workflow, logger);
// TODO: Allow passing a schematic instance directly
try {
await workflow
.execute({
collection,
schematic,
options,
logger,
})
.toPromise();
return { success: !workflowSubscription.error, files: workflowSubscription.files };
}
catch (e) {
if (e instanceof schematics_1.UnsuccessfulWorkflowExecution) {
logger.error(`${color_1.figures.cross} Migration failed. See above for further details.\n`);
}
else {
(0, error_1.assertIsError)(e);
const logPath = (0, log_file_1.writeErrorToLogFile)(e);
logger.fatal(`${color_1.figures.cross} Migration failed: ${e.message}\n` +
` See "${logPath}" for further details.\n`);
}
return { success: false, files: workflowSubscription.files };
}
finally {
workflowSubscription.unsubscribe();
}
}
/**
* @return Whether or not the migration was performed successfully.
*/
async executeMigration(workflow, packageName, collectionPath, migrationName, commit) {
const { logger } = this.context;
const collection = workflow.engine.createCollection(collectionPath);
const name = collection.listSchematicNames().find((name) => name === migrationName);
if (!name) {
logger.error(`Cannot find migration '${migrationName}' in '${packageName}'.`);
return 1;
}
logger.info(color_1.colors.cyan(`** Executing '${migrationName}' of package '${packageName}' **\n`));
const schematic = workflow.engine.createSchematic(name, collection);
return this.executePackageMigrations(workflow, [schematic.description], packageName, commit);
}
/**
* @return Whether or not the migrations were performed successfully.
*/
async executeMigrations(workflow, packageName, collectionPath, from, to, commit) {
const collection = workflow.engine.createCollection(collectionPath);
const migrationRange = new semver.Range('>' + (semver.prerelease(from) ? from.split('-')[0] + '-0' : from) + ' <=' + to.split('-')[0]);
const requiredMigrations = [];
const optionalMigrations = [];
for (const name of collection.listSchematicNames()) {
const schematic = workflow.engine.createSchematic(name, collection);
const description = schematic.description;
description.version = coerceVersionNumber(description.version);
if (!description.version) {
continue;
}
if (semver.satisfies(description.version, migrationRange, { includePrerelease: true })) {
(description.optional ? optionalMigrations : requiredMigrations).push(description);
}
}
if (requiredMigrations.length === 0 && optionalMigrations.length === 0) {
return 0;
}
// Required migrations
if (requiredMigrations.length) {
this.context.logger.info(color_1.colors.cyan(`** Executing migrations of package '${packageName}' **\n`));
requiredMigrations.sort((a, b) => semver.compare(a.version, b.version) || a.name.localeCompare(b.name));
const result = await this.executePackageMigrations(workflow, requiredMigrations, packageName, commit);
if (result === 1) {
return 1;
}
}
// Optional migrations
if (optionalMigrations.length) {
this.context.logger.info(color_1.colors.magenta(`** Optional migrations of package '${packageName}' **\n`));
optionalMigrations.sort((a, b) => semver.compare(a.version, b.version) || a.name.localeCompare(b.name));
const migrationsToRun = await this.getOptionalMigrationsToRun(optionalMigrations, packageName);
if (migrationsToRun?.length) {
return this.executePackageMigrations(workflow, migrationsToRun, packageName, commit);
}
}
return 0;
}
async executePackageMigrations(workflow, migrations, packageName, commit = false) {
const { logger } = this.context;
for (const migration of migrations) {
const { title, description } = getMigrationTitleAndDescription(migration);
logger.info(color_1.colors.cyan(color_1.figures.pointer) + ' ' + color_1.colors.bold(title));
if (description) {
logger.info(' ' + description);
}
const { success, files } = await this.executeSchematic(workflow, migration.collection.name, migration.name);
if (!success) {
return 1;
}
let modifiedFilesText;
switch (files.size) {
case 0:
modifiedFilesText = 'No changes made';
break;
case 1:
modifiedFilesText = '1 file modified';
break;
default:
modifiedFilesText = `${files.size} files modified`;
break;
}
logger.info(` Migration completed (${modifiedFilesText}).`);
// Commit migration
if (commit) {
const commitPrefix = `${packageName} migration - ${migration.name}`;
const commitMessage = migration.description
? `${commitPrefix}\n\n${migration.description}`
: commitPrefix;
const committed = this.commit(commitMessage);
if (!committed) {
// Failed to commit, something went wrong. Abort the update.
return 1;
}
}
logger.info(''); // Extra trailing newline.
}
return 0;
}
async migrateOnly(workflow, packageName, rootDependencies, options) {
const { logger } = this.context;
const packageDependency = rootDependencies.get(packageName);
let packagePath = packageDependency?.path;
let packageNode = packageDependency?.package;
if (packageDependency && !packageNode) {
logger.error('Package found in package.json but is not installed.');
return 1;
}
else if (!packageDependency) {
// Allow running migrations on transitively installed dependencies
// There can technically be nested multiple versions
// TODO: If multiple, this should find all versions and ask which one to use
const packageJson = (0, package_tree_1.findPackageJson)(this.context.root, packageName);
if (packageJson) {
packagePath = path.dirname(packageJson);
packageNode = await (0, package_tree_1.readPackageJson)(packageJson);
}
}
if (!packageNode || !packagePath) {
logger.error('Package is not installed.');
return 1;
}
const updateMetadata = packageNode['ng-update'];
let migrations = updateMetadata?.migrations;
if (migrations === undefined) {
logger.error('Package does not provide migrations.');
return 1;
}
else if (typeof migrations !== 'string') {
logger.error('Package contains a malformed migrations field.');
return 1;
}
else if (path.posix.isAbsolute(migrations) || path.win32.isAbsolute(migrations)) {
logger.error('Package contains an invalid migrations field. Absolute paths are not permitted.');
return 1;
}
// Normalize slashes
migrations = migrations.replace(/\\/g, '/');
if (migrations.startsWith('../')) {
logger.error('Package contains an invalid migrations field. Paths outside the package root are not permitted.');
return 1;
}
// Check if it is a package-local location
const localMigrations = path.join(packagePath, migrations);
if ((0, node_fs_1.existsSync)(localMigrations)) {
migrations = localMigrations;
}
else {
// Try to resolve from package location.
// This avoids issues with package hoisting.
try {
const packageRequire = (0, node_module_1.createRequire)(packagePath + '/');
migrations = packageRequire.resolve(migrations, { paths: this.resolvePaths });
}
catch (e) {
(0, error_1.assertIsError)(e);
if (e.code === 'MODULE_NOT_FOUND') {
logger.error('Migrations for package were not found.');
}
else {
logger.error(`Unable to resolve migrations for package. [${e.message}]`);
}
return 1;
}
}
if (options.name) {
return this.executeMigration(workflow, packageName, migrations, options.name, options.createCommits);
}
const from = coerceVersionNumber(options.from);
if (!from) {
logger.error(`"from" value [${options.from}] is not a valid version.`);
return 1;
}
return this.executeMigrations(workflow, packageName, migrations, from, options.to || packageNode.version, options.createCommits);
}
// eslint-disable-next-line max-lines-per-function
async updatePackagesAndMigrate(workflow, rootDependencies, options, packages) {
const { logger } = this.context;
const logVerbose = (message) => {
if (options.verbose) {
logger.info(message);
}
};
const requests = [];
// Validate packages actually are part of the workspace
for (const pkg of packages) {
const node = rootDependencies.get(pkg.name);
if (!node?.package) {
logger.error(`Package '${pkg.name}' is not a dependency.`);
return 1;
}
// If a specific version is requested and matches the installed version, skip.
if (pkg.type === 'version' && node.package.version === pkg.fetchSpec) {
logger.info(`Package '${pkg.name}' is already at '${pkg.fetchSpec}'.`);
continue;
}
requests.push({ identifier: pkg, node });
}
if (requests.length === 0) {
return 0;
}
logger.info('Fetching dependency metadata from registry...');
const packagesToUpdate = [];
for (const { identifier: requestIdentifier, node } of requests) {
const packageName = requestIdentifier.name;
let metadata;
try {
// Metadata requests are internally cached; multiple requests for same name
// does not result in additional network traffic
metadata = await (0, package_metadata_1.fetchPackageMetadata)(packageName, logger, {
verbose: options.verbose,
});
}
catch (e) {
(0, error_1.assertIsError)(e);
logger.error(`Error fetching metadata for '${packageName}': ` + e.message);
return 1;
}
// Try to find a package version based on the user requested package specifier
// registry specifier types are either version, range, or tag
let manifest;
switch (requestIdentifier.type) {
case 'tag':
manifest = metadata.tags[requestIdentifier.fetchSpec];
// If not found and next option was used and user did not provide a specifier, try latest.
// Package may not have a next tag.
if (!manifest &&
requestIdentifier.fetchSpec === 'next' &&
requestIdentifier.rawSpec === '*') {
manifest = metadata.tags['latest'];
}
break;
case 'version':
manifest = metadata.versions[requestIdentifier.fetchSpec];
break;
case 'range':
for (const potentialManifest of Object.values(metadata.versions)) {
// Ignore deprecated package versions
if (potentialManifest.deprecated) {
continue;
}
// Only consider versions that are within the range
if (!semver.satisfies(potentialManifest.version, requestIdentifier.fetchSpec, {
loose: true,
})) {
continue;
}
// Update the used manifest if current potential is newer than existing or there is not one yet
if (!manifest ||
semver.gt(potentialManifest.version, manifest.version, { loose: true })) {
manifest = potentialManifest;
}
}
break;
}
if (!manifest) {
logger.error(`Package specified by '${requestIdentifier.raw}' does not exist within the registry.`);
return 1;
}
if (manifest.version === node.package?.version) {
logger.info(`Package '${packageName}' is already up to date.`);
continue;
}
if (node.package && ANGULAR_PACKAGES_REGEXP.test(node.package.name)) {
const { name, version } = node.package;
const toBeInstalledMajorVersion = +manifest.version.split('.')[0];
const currentMajorVersion = +version.split('.')[0];
if (toBeInstalledMajorVersion - currentMajorVersion > 1) {
// Only allow updating a single version at a time.
if (currentMajorVersion < 6) {
// Before version 6, the major versions were not always sequential.
// Example @angular/core skipped version 3, @angular/cli skipped versions 2-5.
logger.error(`Updating multiple major versions of '${name}' at once is not supported. Please migrate each major version individually.\n` +
`For more information about the update process, see https://update.angular.dev/.`);
}
else {
const nextMajorVersionFromCurrent = currentMajorVersion + 1;
logger.error(`Updating multiple major versions of '${name}' at once is not supported. Please migrate each major version individually.\n` +
`Run 'ng update ${name}@${nextMajorVersionFromCurrent}' in your workspace directory ` +
`to update to latest '${nextMajorVersionFromCurrent}.x' version of '${name}'.\n\n` +
`For more information about the update process, see https://update.angular.dev/?v=${currentMajorVersion}.0-${nextMajorVersionFromCurrent}.0`);
}
return 1;
}
}
packagesToUpdate.push(requestIdentifier.toString());
}
if (packagesToUpdate.length === 0) {
return 0;
}
const { success } = await this.executeSchematic(workflow, UPDATE_SCHEMATIC_COLLECTION, 'update', {
verbose: options.verbose,
force: options.force,
next: options.next,
packageManager: this.context.packageManager.name,
packages: packagesToUpdate,
});
if (success) {
const { root: commandRoot, packageManager } = this.context;
const installArgs = this.packageManagerForce(options.verbose) ? ['--force'] : [];
const tasks = new listr2_1.Listr([
{
title: 'Cleaning node modules directory',
async task(_, task) {
try {
await node_fs_1.promises.rm(path.join(commandRoot, 'node_modules'), {
force: true,
recursive: true,
maxRetries: 3,
});
}
catch (e) {
(0, error_1.assertIsError)(e);
if (e.code === 'ENOENT') {
task.skip('Cleaning not required. Node modules directory not found.');
}
}
},
},
{
title: 'Installing packages',
async task() {
const installationSuccess = await packageManager.installAll(installArgs, commandRoot);
if (!installationSuccess) {
throw new CommandError('Unable to install packages');
}
},
},
]);
try {
await tasks.run();
}
catch (e) {
if (e instanceof CommandError) {
return 1;
}
throw e;
}
}
if (success && options.createCommits) {
if (!this.commit(`Angular CLI update for packages - ${packagesToUpdate.join(', ')}`)) {
return 1;
}
}
// This is a temporary workaround to allow data to be passed back from the update schematic
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const migrations = global.externalMigrations;
if (success && migrations) {
const rootRequire = (0, node_module_1.createRequire)(this.context.root + '/');
for (const migration of migrations) {
// Resolve the package from the workspace root, as otherwise it will be resolved from the temp
// installed CLI version.
let packagePath;
logVerbose(`Resolving migration package '${migration.package}' from '${this.context.root}'...`);
try {
try {
packagePath = path.dirname(
// This may fail if the `package.json` is not exported as an entry point
rootRequire.resolve(path.join(migration.package, 'package.json')));
}
catch (e) {
(0, error_1.assertIsError)(e);
if (e.code === 'MODULE_NOT_FOUND') {
// Fallback to trying to resolve the package's main entry point
packagePath = rootRequire.resolve(migration.package);
}
else {
throw e;
}
}
}
catch (e) {
(0, error_1.assertIsError)(e);
if (e.code === 'MODULE_NOT_FOUND') {
logVerbose(e.toString());
logger.error(`Migrations for package (${migration.package}) were not found.` +
' The package could not be found in the workspace.');
}
else {
logger.error(`Unable to resolve migrations for package (${migration.package}). [${e.message}]`);
}
return 1;
}
let migrations;
// Check if it is a package-local location
const localMigrations = path.join(packagePath, migration.collection);
if ((0, node_fs_1.existsSync)(localMigrations)) {
migrations = localMigrations;
}
else {
// Try to resolve from package location.
// This avoids issues with package hoisting.
try {
const packageRequire = (0, node_module_1.createRequire)(packagePath + '/');
migrations = packageRequire.resolve(migration.collection);
}
catch (e) {
(0, error_1.assertIsError)(e);
if (e.code === 'MODULE_NOT_FOUND') {
logger.error(`Migrations for package (${migration.package}) were not found.`);
}
else {
logger.error(`Unable to resolve migrations for package (${migration.package}). [${e.message}]`);
}
return 1;
}
}
const result = await this.executeMigrations(workflow, migration.package, migrations, migration.from, migration.to, options.createCommits);
// A non-zero value is a failure for the package's migrations
if (result !== 0) {
return result;
}
}
}
return success ? 0 : 1;
}
/**
* @return Whether or not the commit was successful.
*/
commit(message) {
const { logger } = this.context;
// Check if a commit is needed.
let commitNeeded;
try {
commitNeeded = hasChangesToCommit();
}
catch (err) {
logger.error(` Failed to read Git tree:\n${err.stderr}`);
return false;
}
if (!commitNeeded) {
logger.info(' No changes to commit after migration.');
return true;
}
// Commit changes and abort on error.
try {
createCommit(message);
}
catch (err) {
logger.error(`Failed to commit update (${message}):\n${err.stderr}`);
return false;
}
// Notify user of the commit.
const hash = findCurrentGitSha();
const shortMessage = message.split('\n')[0];
if (hash) {
logger.info(` Committed migration step (${getShortHash(hash)}): ${shortMessage}.`);
}
else {
// Commit was successful, but reading the hash was not. Something weird happened,
// but nothing that would stop the update. Just log the weirdness and continue.
logger.info(` Committed migration step: ${shortMessage}.`);
logger.warn(' Failed to look up hash of most recent commit, continuing anyways.');
}
return true;
}
checkCleanGit() {
try {
const topLevel = (0, node_child_process_1.execSync)('git rev-parse --show-toplevel', {
encoding: 'utf8',
stdio: 'pipe',
});
const result = (0, node_child_process_1.execSync)('git status --porcelain', { encoding: 'utf8', stdio: 'pipe' });
if (result.trim().length === 0) {
return true;
}
// Only files inside the workspace root are relevant
for (const entry of result.split('\n')) {
const relativeEntry = path.relative(path.resolve(this.context.root), path.resolve(topLevel.trim(), entry.slice(3).trim()));
if (!relativeEntry.startsWith('..') && !path.isAbsolute(relativeEntry)) {
return false;
}
}
}
catch { }
return true;
}
/**
* Checks if the current installed CLI version is older or newer than a compatible version.
* @returns the version to install or null when there is no update to install.
*/
async checkCLIVersion(packagesToUpdate, verbose = false, next = false) {
const { version } = await (0, package_metadata_1.fetchPackageManifest)(`@angular/cli@${this.getCLIUpdateRunnerVersion(packagesToUpdate, next)}`, this.context.logger, {
verbose,
usingYarn: this.context.packageManager.name === workspace_schema_1.PackageManager.Yarn,
});
return version_1.VERSION.full === version ? null : version;
}
getCLIUpdateRunnerVersion(packagesToUpdate, next) {
if (next) {
return 'next';
}
const updatingAngularPackage = packagesToUpdate?.find((r) => ANGULAR_PACKAGES_REGEXP.test(r));
if (updatingAngularPackage) {
// If we are updating any Angular package we can update the CLI to the target version because
// migrations for @angular/core@13 can be executed using Angular/cli@13.
// This is same behaviour as `npx @angular/cli@13 update @angular/core@13`.
// `@angular/cli@13` -> ['', 'angular/cli', '13']
// `@angular/cli` -> ['', 'angular/cli']
const tempVersion = coerceVersionNumber(updatingAngularPackage.split('@')[2]);
return semver.parse(tempVersion)?.major ?? 'latest';
}
// When not updating an Angular package we cannot determine which schematic runtime the migration should to be executed in.
// Typically, we can assume that the `@angular/cli` was updated previously.
// Example: Angular official packages are typically updated prior to NGRX etc...
// Therefore, we only update to the latest patch version of the installed major version of the Angular CLI.
// This is important because we might end up in a scenario where locally Angular v12 is installed, updating NGRX from 11 to 12.
// We end up using Angular ClI v13 to run the migrations if we run the migrations using the CLI installed major version + 1 logic.
return version_1.VERSION.major;
}
async runTempBinary(packageName, args = []) {
const { success, tempNodeModules } = await this.context.packageManager.installTemp(packageName);
if (!success) {
return 1;
}
// Remove version/tag etc... from package name
// Ex: @angular/cli@latest -> @angular/cli
const packageNameNoVersion = packageName.substring(0, packageName.lastIndexOf('@'));
const pkgLocation = (0, node_path_1.join)(tempNodeModules, packageNameNoVersion);
const packageJsonPath = (0, node_path_1.join)(pkgLocation, 'package.json');
// Get a binary location for this package
let binPath;
if ((0, node_fs_1.existsSync)(packageJsonPath)) {
const content = await node_fs_1.promises.readFile(packageJsonPath, 'utf-8');
if (content) {
const { bin = {} } = JSON.parse(content);
const binKeys = Object.keys(bin);
if (binKeys.length) {
binPath = (0, node_path_1.resolve)(pkgLocation, bin[binKeys[0]]);
}
}
}
if (!binPath) {
throw new Error(`Cannot locate bin for temporary package: ${packageNameNoVersion}.`);
}
const { status, error } = (0, node_child_process_1.spawnSync)(process.execPath, [binPath, ...args], {
stdio: 'inherit',
env: {
...process.env,
NG_DISABLE_VERSION_CHECK: 'true',
NG_CLI_ANALYTICS: 'false',
},
});
if (status === null && error) {
throw error;
}
return status ?? 0;
}
packageManagerForce(verbose) {
// npm 7+ can fail due to it incorrectly resolving peer dependencies that have valid SemVer
// ranges during an update. Update will set correct versions of dependencies within the
// package.json file. The force option is set to workaround these errors.
// Example error:
// npm ERR! Conflicting peer dependency: @angular/compiler-cli@14.0.0-rc.0
// npm ERR! node_modules/@angular/compiler-cli
// npm ERR! peer @angular/compiler-cli@"^14.0.0 || ^14.0.0-rc" from @angular-devkit/build-angular@14.0.0-rc.0
// npm ERR! node_modules/@angular-devkit/build-angular
// npm ERR! dev @angular-devkit/build-angular@"~14.0.0-rc.0" from the root project
if (this.context.packageManager.name === workspace_schema_1.PackageManager.Npm &&
this.context.packageManager.version &&
semver.gte(this.context.packageManager.version, '7.0.0')) {
if (verbose) {
this.context.logger.info('NPM 7+ detected -- enabling force option for package installation');
}
return true;
}
return false;
}
async getOptionalMigrationsToRun(optionalMigrations, packageName) {
const { logger } = this.context;
const numberOfMigrations = optionalMigrations.length;
logger.info(`This package has ${numberOfMigrations} optional migration${numberOfMigrations > 1 ? 's' : ''} that can be executed.`);
if (!(0, tty_1.isTTY)()) {
for (const migration of optionalMigrations) {
const { title } = getMigrationTitleAndDescription(migration);
logger.info(color_1.colors.cyan(color_1.figures.pointer) + ' ' + color_1.colors.bold(title));
logger.info(color_1.colors.gray(` ng update ${packageName} --name ${migration.name}`));
logger.info(''); // Extra trailing newline.
}
return undefined;
}
logger.info('Optional migrations may be skipped and executed after the update process, if preferred.');
logger.info(''); // Extra trailing newline.
const answer = await (0, prompt_1.askChoices)(`Select the migrations that you'd like to run`, optionalMigrations.map((migration) => {
const { title, documentation } = getMigrationTitleAndDescription(migration);
return {
name: `[${color_1.colors.white(migration.name)}] ${title}${documentation ? ` (${documentation})` : ''}`,
value: migration.name,
checked: migration.recommended,
};
}), null);
logger.info(''); // Extra trailing newline.
return optionalMigrations.filter(({ name }) => answer?.includes(name));
}
}
exports.default = UpdateCommandModule;
/**
* @return Whether or not the working directory has Git changes to commit.
*/
function hasChangesToCommit() {
// List all modified files not covered by .gitignore.
// If any files are returned, then there must be something to commit.
return (0, node_child_process_1.execSync)('git ls-files -m -d -o --exclude-standard').toString() !== '';
}
/**
* Precondition: Must have pending changes to commit, they do not need to be staged.
* Postcondition: The Git working tree is committed and the repo is clean.
* @param message The commit message to use.
*/
function createCommit(message) {
// Stage entire working tree for commit.
(0, node_child_process_1.execSync)('git add -A', { encoding: 'utf8', stdio: 'pipe' });
// Commit with the message passed via stdin to avoid bash escaping issues.
(0, node_child_process_1.execSync)('git commit --no-verify -F -', { encoding: 'utf8', stdio: 'pipe', input: message });
}
/**
* @return The Git SHA hash of the HEAD commit. Returns null if unable to retrieve the hash.
*/
function findCurrentGitSha() {
try {
return (0, node_child_process_1.execSync)('git rev-parse HEAD', { encoding: 'utf8', stdio: 'pipe' }).trim();
}
catch {
return null;
}
}
function getShortHash(commitHash) {
return commitHash.slice(0, 9);
}
function coerceVersionNumber(version) {
if (!version) {
return undefined;
}
if (!/^\d{1,30}\.\d{1,30}\.\d{1,30}/.test(version)) {
const match = version.match(/^\d{1,30}(\.\d{1,30})*/);
if (!match) {
return undefined;
}
if (!match[1]) {
version = version.substring(0, match[0].length) + '.0.0' + version.substring(match[0].length);
}
else if (!match[2]) {
version = version.substring(0, match[0].length) + '.0' + version.substring(match[0].length);
}
else {
return undefined;
}
}
return semver.valid(version) ?? undefined;
}
function getMigrationTitleAndDescription(migration) {
const [title, ...description] = migration.description.split('. ');
return {
title: title.endsWith('.') ? title : title + '.',
description: description.join('.\n '),
documentation: migration.documentation
? new URL(migration.documentation, 'https://angular.dev').href
: undefined,
};
}
+22
View File
@@ -0,0 +1,22 @@
Perform a basic update to the current stable release of the core framework and CLI by running the following command.
```
ng update @angular/cli @angular/core
```
To update to the next beta or pre-release version, use the `--next` option.
To update from one major version to another, use the format
```
ng update @angular/cli@^<major_version> @angular/core@^<major_version>
```
We recommend that you always update to the latest patch version, as it contains fixes we released since the initial major release.
For example, use the following command to take the latest 10.x.x version and use that to update.
```
ng update @angular/cli@^10 @angular/core@^10
```
For detailed information and guidance on updating your application, see the interactive [Angular Update Guide](https://update.angular.dev/).
+9
View File
@@ -0,0 +1,9 @@
{
"schematics": {
"update": {
"factory": "./index",
"schema": "./schema.json",
"description": "Update one or multiple packages to versions, updating peer dependencies along the way."
}
}
}
+11
View File
@@ -0,0 +1,11 @@
/**
* @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
*/
import { Rule } from '@angular-devkit/schematics';
import { Schema as UpdateSchema } from './schema';
export declare function angularMajorCompatGuarantee(range: string): string;
export default function (options: UpdateSchema): Rule;
+720
View File
@@ -0,0 +1,720 @@
"use strict";
/**
* @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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.angularMajorCompatGuarantee = angularMajorCompatGuarantee;
exports.default = default_1;
const core_1 = require("@angular-devkit/core");
const schematics_1 = require("@angular-devkit/schematics");
const npa = __importStar(require("npm-package-arg"));
const semver = __importStar(require("semver"));
const package_metadata_1 = require("../../../utilities/package-metadata");
// Angular guarantees that a major is compatible with its following major (so packages that depend
// on Angular 5 are also compatible with Angular 6). This is, in code, represented by verifying
// that all other packages that have a peer dependency of `"@angular/core": "^5.0.0"` actually
// supports 6.0, by adding that compatibility to the range, so it is `^5.0.0 || ^6.0.0`.
// We export it to allow for testing.
function angularMajorCompatGuarantee(range) {
let newRange = semver.validRange(range);
if (!newRange) {
return range;
}
let major = 1;
while (!semver.gtr(major + '.0.0', newRange)) {
major++;
if (major >= 99) {
// Use original range if it supports a major this high
// Range is most likely unbounded (e.g., >=5.0.0)
return newRange;
}
}
// Add the major version as compatible with the angular compatible, with all minors. This is
// already one major above the greatest supported, because we increment `major` before checking.
// We add minors like this because a minor beta is still compatible with a minor non-beta.
newRange = range;
for (let minor = 0; minor < 20; minor++) {
newRange += ` || ^${major}.${minor}.0-alpha.0 `;
}
return semver.validRange(newRange) || range;
}
// This is a map of packageGroupName to range extending function. If it isn't found, the range is
// kept the same.
const knownPeerCompatibleList = {
'@angular/core': angularMajorCompatGuarantee,
};
function _updatePeerVersion(infoMap, name, range) {
// Resolve packageGroupName.
const maybePackageInfo = infoMap.get(name);
if (!maybePackageInfo) {
return range;
}
if (maybePackageInfo.target) {
name = maybePackageInfo.target.updateMetadata.packageGroupName || name;
}
else {
name = maybePackageInfo.installed.updateMetadata.packageGroupName || name;
}
const maybeTransform = knownPeerCompatibleList[name];
if (maybeTransform) {
if (typeof maybeTransform == 'function') {
return maybeTransform(range);
}
else {
return maybeTransform;
}
}
return range;
}
function _validateForwardPeerDependencies(name, infoMap, peers, peersMeta, logger, next) {
let validationFailed = false;
for (const [peer, range] of Object.entries(peers)) {
logger.debug(`Checking forward peer ${peer}...`);
const maybePeerInfo = infoMap.get(peer);
const isOptional = peersMeta[peer] && !!peersMeta[peer].optional;
if (!maybePeerInfo) {
if (!isOptional) {
logger.warn([
`Package ${JSON.stringify(name)} has a missing peer dependency of`,
`${JSON.stringify(peer)} @ ${JSON.stringify(range)}.`,
].join(' '));
}
continue;
}
const peerVersion = maybePeerInfo.target && maybePeerInfo.target.packageJson.version
? maybePeerInfo.target.packageJson.version
: maybePeerInfo.installed.version;
logger.debug(` Range intersects(${range}, ${peerVersion})...`);
if (!semver.satisfies(peerVersion, range, { includePrerelease: next || undefined })) {
logger.error([
`Package ${JSON.stringify(name)} has an incompatible peer dependency to`,
`${JSON.stringify(peer)} (requires ${JSON.stringify(range)},`,
`would install ${JSON.stringify(peerVersion)})`,
].join(' '));
validationFailed = true;
continue;
}
}
return validationFailed;
}
function _validateReversePeerDependencies(name, version, infoMap, logger, next) {
for (const [installed, installedInfo] of infoMap.entries()) {
const installedLogger = logger.createChild(installed);
installedLogger.debug(`${installed}...`);
const peers = (installedInfo.target || installedInfo.installed).packageJson.peerDependencies;
for (const [peer, range] of Object.entries(peers || {})) {
if (peer != name) {
// Only check peers to the packages we're updating. We don't care about peers
// that are unmet but we have no effect on.
continue;
}
// Ignore peerDependency mismatches for these packages.
// They are deprecated and removed via a migration.
const ignoredPackages = [
'codelyzer',
'@schematics/update',
'@angular-devkit/build-ng-packagr',
'tsickle',
'@nguniversal/builders',
];
if (ignoredPackages.includes(installed)) {
continue;
}
// Override the peer version range if it's known as a compatible.
const extendedRange = _updatePeerVersion(infoMap, peer, range);
if (!semver.satisfies(version, extendedRange, { includePrerelease: next || undefined })) {
logger.error([
`Package ${JSON.stringify(installed)} has an incompatible peer dependency to`,
`${JSON.stringify(name)} (requires`,
`${JSON.stringify(range)}${extendedRange == range ? '' : ' (extended)'},`,
`would install ${JSON.stringify(version)}).`,
].join(' '));
return true;
}
}
}
return false;
}
function _validateUpdatePackages(infoMap, force, next, logger) {
logger.debug('Updating the following packages:');
infoMap.forEach((info) => {
if (info.target) {
logger.debug(` ${info.name} => ${info.target.version}`);
}
});
let peerErrors = false;
infoMap.forEach((info) => {
const { name, target } = info;
if (!target) {
return;
}
const pkgLogger = logger.createChild(name);
logger.debug(`${name}...`);
const { peerDependencies = {}, peerDependenciesMeta = {} } = target.packageJson;
peerErrors =
_validateForwardPeerDependencies(name, infoMap, peerDependencies, peerDependenciesMeta, pkgLogger, next) || peerErrors;
peerErrors =
_validateReversePeerDependencies(name, target.version, infoMap, pkgLogger, next) ||
peerErrors;
});
if (!force && peerErrors) {
throw new schematics_1.SchematicsException('Incompatible peer dependencies found.\n' +
'Peer dependency warnings when installing dependencies means that those dependencies might not work correctly together.\n' +
`You can use the '--force' option to ignore incompatible peer dependencies and instead address these warnings later.`);
}
}
function _performUpdate(tree, context, infoMap, logger, migrateOnly) {
const packageJsonContent = tree.read('/package.json')?.toString();
if (!packageJsonContent) {
throw new schematics_1.SchematicsException('Could not find a package.json. Are you in a Node project?');
}
const packageJson = tree.readJson('/package.json');
const updateDependency = (deps, name, newVersion) => {
const oldVersion = deps[name];
// We only respect caret and tilde ranges on update.
const execResult = /^[\^~]/.exec(oldVersion);
deps[name] = `${execResult ? execResult[0] : ''}${newVersion}`;
};
const toInstall = [...infoMap.values()]
.map((x) => [x.name, x.target, x.installed])
.filter(([name, target, installed]) => {
return !!name && !!target && !!installed;
});
toInstall.forEach(([name, target, installed]) => {
logger.info(`Updating package.json with dependency ${name} ` +
`@ ${JSON.stringify(target.version)} (was ${JSON.stringify(installed.version)})...`);
if (packageJson.dependencies && packageJson.dependencies[name]) {
updateDependency(packageJson.dependencies, name, target.version);
if (packageJson.devDependencies && packageJson.devDependencies[name]) {
delete packageJson.devDependencies[name];
}
if (packageJson.peerDependencies && packageJson.peerDependencies[name]) {
delete packageJson.peerDependencies[name];
}
}
else if (packageJson.devDependencies && packageJson.devDependencies[name]) {
updateDependency(packageJson.devDependencies, name, target.version);
if (packageJson.peerDependencies && packageJson.peerDependencies[name]) {
delete packageJson.peerDependencies[name];
}
}
else if (packageJson.peerDependencies && packageJson.peerDependencies[name]) {
updateDependency(packageJson.peerDependencies, name, target.version);
}
else {
logger.warn(`Package ${name} was not found in dependencies.`);
}
});
const eofMatches = packageJsonContent.match(/\r?\n$/);
const eof = eofMatches?.[0] ?? '';
const newContent = JSON.stringify(packageJson, null, 2) + eof;
if (packageJsonContent != newContent || migrateOnly) {
if (!migrateOnly) {
tree.overwrite('/package.json', newContent);
}
const externalMigrations = [];
// Run the migrate schematics with the list of packages to use. The collection contains
// version information and we need to do this post installation. Please note that the
// migration COULD fail and leave side effects on disk.
// Run the schematics task of those packages.
toInstall.forEach(([name, target, installed]) => {
if (!target.updateMetadata.migrations) {
return;
}
externalMigrations.push({
package: name,
collection: target.updateMetadata.migrations,
from: installed.version,
to: target.version,
});
return;
});
if (externalMigrations.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
global.externalMigrations = externalMigrations;
}
}
}
function _getUpdateMetadata(packageJson, logger) {
const metadata = packageJson['ng-update'];
const result = {
packageGroup: {},
requirements: {},
};
if (!metadata || typeof metadata != 'object' || Array.isArray(metadata)) {
return result;
}
if (metadata['packageGroup']) {
const packageGroup = metadata['packageGroup'];
// Verify that packageGroup is an array of strings or an map of versions. This is not an error
// but we still warn the user and ignore the packageGroup keys.
if (Array.isArray(packageGroup) && packageGroup.every((x) => typeof x == 'string')) {
result.packageGroup = packageGroup.reduce((group, name) => {
group[name] = packageJson.version;
return group;
}, result.packageGroup);
}
else if (typeof packageGroup == 'object' &&
packageGroup &&
!Array.isArray(packageGroup) &&
Object.values(packageGroup).every((x) => typeof x == 'string')) {
result.packageGroup = packageGroup;
}
else {
logger.warn(`packageGroup metadata of package ${packageJson.name} is malformed. Ignoring.`);
}
result.packageGroupName = Object.keys(result.packageGroup)[0];
}
if (typeof metadata['packageGroupName'] == 'string') {
result.packageGroupName = metadata['packageGroupName'];
}
if (metadata['requirements']) {
const requirements = metadata['requirements'];
// Verify that requirements are
if (typeof requirements != 'object' ||
Array.isArray(requirements) ||
Object.keys(requirements).some((name) => typeof requirements[name] != 'string')) {
logger.warn(`requirements metadata of package ${packageJson.name} is malformed. Ignoring.`);
}
else {
result.requirements = requirements;
}
}
if (metadata['migrations']) {
const migrations = metadata['migrations'];
if (typeof migrations != 'string') {
logger.warn(`migrations metadata of package ${packageJson.name} is malformed. Ignoring.`);
}
else {
result.migrations = migrations;
}
}
return result;
}
function _usageMessage(options, infoMap, logger) {
const packageGroups = new Map();
const packagesToUpdate = [...infoMap.entries()]
.map(([name, info]) => {
let tag = options.next
? info.npmPackageJson['dist-tags']['next']
? 'next'
: 'latest'
: 'latest';
let version = info.npmPackageJson['dist-tags'][tag];
let target = info.npmPackageJson.versions[version];
const versionDiff = semver.diff(info.installed.version, version);
if (versionDiff !== 'patch' &&
versionDiff !== 'minor' &&
/^@(?:angular|nguniversal)\//.test(name)) {
const installedMajorVersion = semver.parse(info.installed.version)?.major;
const toInstallMajorVersion = semver.parse(version)?.major;
if (installedMajorVersion !== undefined &&
toInstallMajorVersion !== undefined &&
installedMajorVersion < toInstallMajorVersion - 1) {
const nextMajorVersion = `${installedMajorVersion + 1}.`;
const nextMajorVersions = Object.keys(info.npmPackageJson.versions)
.filter((v) => v.startsWith(nextMajorVersion))
.sort((a, b) => (a > b ? -1 : 1));
if (nextMajorVersions.length) {
version = nextMajorVersions[0];
target = info.npmPackageJson.versions[version];
tag = '';
}
}
}
return {
name,
info,
version,
tag,
target,
};
})
.filter(({ info, version, target }) => target?.['ng-update'] && semver.compare(info.installed.version, version) < 0)
.map(({ name, info, version, tag, target }) => {
// Look for packageGroup.
const ngUpdate = target['ng-update'];
const packageGroup = ngUpdate?.['packageGroup'];
if (packageGroup) {
const packageGroupNames = Array.isArray(packageGroup)
? packageGroup
: Object.keys(packageGroup);
const packageGroupName = ngUpdate?.['packageGroupName'] || packageGroupNames.find((n) => infoMap.has(n));
if (packageGroupName) {
if (packageGroups.has(name)) {
return null;
}
for (const groupName of packageGroupNames) {
packageGroups.set(groupName, packageGroupName);
}
packageGroups.set(packageGroupName, packageGroupName);
name = packageGroupName;
}
}
let command = `ng update ${name}`;
if (!tag) {
command += `@${semver.parse(version)?.major || version}`;
}
else if (tag == 'next') {
command += ' --next';
}
return [name, `${info.installed.version} -> ${version} `, command];
})
.filter((x) => x !== null)
.sort((a, b) => (a && b ? a[0].localeCompare(b[0]) : 0));
if (packagesToUpdate.length == 0) {
logger.info('We analyzed your package.json and everything seems to be in order. Good work!');
return;
}
logger.info('We analyzed your package.json, there are some packages to update:\n');
// Find the largest name to know the padding needed.
let namePad = Math.max(...[...infoMap.keys()].map((x) => x.length)) + 2;
if (!Number.isFinite(namePad)) {
namePad = 30;
}
const pads = [namePad, 25, 0];
logger.info(' ' + ['Name', 'Version', 'Command to update'].map((x, i) => x.padEnd(pads[i])).join(''));
logger.info(' ' + '-'.repeat(pads.reduce((s, x) => (s += x), 0) + 20));
packagesToUpdate.forEach((fields) => {
if (!fields) {
return;
}
logger.info(' ' + fields.map((x, i) => x.padEnd(pads[i])).join(''));
});
logger.info(`\nThere might be additional packages which don't provide 'ng update' capabilities that are outdated.\n` +
`You can update the additional packages by running the update command of your package manager.`);
return;
}
function _buildPackageInfo(tree, packages, allDependencies, npmPackageJson, logger) {
const name = npmPackageJson.name;
const packageJsonRange = allDependencies.get(name);
if (!packageJsonRange) {
throw new schematics_1.SchematicsException(`Package ${JSON.stringify(name)} was not found in package.json.`);
}
// Find out the currently installed version. Either from the package.json or the node_modules/
// TODO: figure out a way to read package-lock.json and/or yarn.lock.
const pkgJsonPath = `/node_modules/${name}/package.json`;
const pkgJsonExists = tree.exists(pkgJsonPath);
let installedVersion;
if (pkgJsonExists) {
const { version } = tree.readJson(pkgJsonPath);
installedVersion = version;
}
const packageVersionsNonDeprecated = [];
const packageVersionsDeprecated = [];
for (const [version, { deprecated }] of Object.entries(npmPackageJson.versions)) {
if (deprecated) {
packageVersionsDeprecated.push(version);
}
else {
packageVersionsNonDeprecated.push(version);
}
}
const findSatisfyingVersion = (targetVersion) => (semver.maxSatisfying(packageVersionsNonDeprecated, targetVersion) ??
semver.maxSatisfying(packageVersionsDeprecated, targetVersion)) ??
undefined;
if (!installedVersion) {
// Find the version from NPM that fits the range to max.
installedVersion = findSatisfyingVersion(packageJsonRange);
}
if (!installedVersion) {
throw new schematics_1.SchematicsException(`An unexpected error happened; could not determine version for package ${name}.`);
}
const installedPackageJson = npmPackageJson.versions[installedVersion] || pkgJsonExists;
if (!installedPackageJson) {
throw new schematics_1.SchematicsException(`An unexpected error happened; package ${name} has no version ${installedVersion}.`);
}
let targetVersion = packages.get(name);
if (targetVersion) {
if (npmPackageJson['dist-tags'][targetVersion]) {
targetVersion = npmPackageJson['dist-tags'][targetVersion];
}
else if (targetVersion == 'next') {
targetVersion = npmPackageJson['dist-tags']['latest'];
}
else {
targetVersion = findSatisfyingVersion(targetVersion);
}
}
if (targetVersion && semver.lte(targetVersion, installedVersion)) {
logger.debug(`Package ${name} already satisfied by package.json (${packageJsonRange}).`);
targetVersion = undefined;
}
const target = targetVersion
? {
version: targetVersion,
packageJson: npmPackageJson.versions[targetVersion],
updateMetadata: _getUpdateMetadata(npmPackageJson.versions[targetVersion], logger),
}
: undefined;
// Check if there's an installed version.
return {
name,
npmPackageJson,
installed: {
version: installedVersion,
packageJson: installedPackageJson,
updateMetadata: _getUpdateMetadata(installedPackageJson, logger),
},
target,
packageJsonRange,
};
}
function _buildPackageList(options, projectDeps, logger) {
// Parse the packages options to set the targeted version.
const packages = new Map();
const commandLinePackages = options.packages && options.packages.length > 0 ? options.packages : [];
for (const pkg of commandLinePackages) {
// Split the version asked on command line.
const m = pkg.match(/^((?:@[^/]{1,100}\/)?[^@]{1,100})(?:@(.{1,100}))?$/);
if (!m) {
logger.warn(`Invalid package argument: ${JSON.stringify(pkg)}. Skipping.`);
continue;
}
const [, npmName, maybeVersion] = m;
const version = projectDeps.get(npmName);
if (!version) {
logger.warn(`Package not installed: ${JSON.stringify(npmName)}. Skipping.`);
continue;
}
packages.set(npmName, (maybeVersion || (options.next ? 'next' : 'latest')));
}
return packages;
}
function _addPackageGroup(tree, packages, allDependencies, npmPackageJson, logger) {
const maybePackage = packages.get(npmPackageJson.name);
if (!maybePackage) {
return;
}
const info = _buildPackageInfo(tree, packages, allDependencies, npmPackageJson, logger);
const version = (info.target && info.target.version) ||
npmPackageJson['dist-tags'][maybePackage] ||
maybePackage;
if (!npmPackageJson.versions[version]) {
return;
}
const ngUpdateMetadata = npmPackageJson.versions[version]['ng-update'];
if (!ngUpdateMetadata) {
return;
}
const packageGroup = ngUpdateMetadata['packageGroup'];
if (!packageGroup) {
return;
}
let packageGroupNormalized = {};
if (Array.isArray(packageGroup) && !packageGroup.some((x) => typeof x != 'string')) {
packageGroupNormalized = packageGroup.reduce((acc, curr) => {
acc[curr] = maybePackage;
return acc;
}, {});
}
else if (typeof packageGroup == 'object' &&
packageGroup &&
!Array.isArray(packageGroup) &&
Object.values(packageGroup).every((x) => typeof x == 'string')) {
packageGroupNormalized = packageGroup;
}
else {
logger.warn(`packageGroup metadata of package ${npmPackageJson.name} is malformed. Ignoring.`);
return;
}
for (const [name, value] of Object.entries(packageGroupNormalized)) {
// Don't override names from the command line.
// Remove packages that aren't installed.
if (!packages.has(name) && allDependencies.has(name)) {
packages.set(name, value);
}
}
}
/**
* Add peer dependencies of packages on the command line to the list of packages to update.
* We don't do verification of the versions here as this will be done by a later step (and can
* be ignored by the --force flag).
* @private
*/
function _addPeerDependencies(tree, packages, allDependencies, npmPackageJson, npmPackageJsonMap, logger) {
const maybePackage = packages.get(npmPackageJson.name);
if (!maybePackage) {
return;
}
const info = _buildPackageInfo(tree, packages, allDependencies, npmPackageJson, logger);
const version = (info.target && info.target.version) ||
npmPackageJson['dist-tags'][maybePackage] ||
maybePackage;
if (!npmPackageJson.versions[version]) {
return;
}
const packageJson = npmPackageJson.versions[version];
const error = false;
for (const [peer, range] of Object.entries(packageJson.peerDependencies || {})) {
if (packages.has(peer)) {
continue;
}
const peerPackageJson = npmPackageJsonMap.get(peer);
if (peerPackageJson) {
const peerInfo = _buildPackageInfo(tree, packages, allDependencies, peerPackageJson, logger);
if (semver.satisfies(peerInfo.installed.version, range)) {
continue;
}
}
packages.set(peer, range);
}
if (error) {
throw new schematics_1.SchematicsException('An error occured, see above.');
}
}
function _getAllDependencies(tree) {
const { dependencies, devDependencies, peerDependencies } = tree.readJson('/package.json');
return [
...Object.entries(peerDependencies || {}),
...Object.entries(devDependencies || {}),
...Object.entries(dependencies || {}),
];
}
function _formatVersion(version) {
if (version === undefined) {
return undefined;
}
if (!version.match(/^\d{1,30}\.\d{1,30}\.\d{1,30}/)) {
version += '.0';
}
if (!version.match(/^\d{1,30}\.\d{1,30}\.\d{1,30}/)) {
version += '.0';
}
if (!semver.valid(version)) {
throw new schematics_1.SchematicsException(`Invalid migration version: ${JSON.stringify(version)}`);
}
return version;
}
/**
* Returns whether or not the given package specifier (the value string in a
* `package.json` dependency) is hosted in the NPM registry.
* @throws When the specifier cannot be parsed.
*/
function isPkgFromRegistry(name, specifier) {
const result = npa.resolve(name, specifier);
return !!result.registry;
}
function default_1(options) {
if (!options.packages) {
// We cannot just return this because we need to fetch the packages from NPM still for the
// help/guide to show.
options.packages = [];
}
else {
// We split every packages by commas to allow people to pass in multiple and make it an array.
options.packages = options.packages.reduce((acc, curr) => {
return acc.concat(curr.split(','));
}, []);
}
if (options.migrateOnly && options.from) {
if (options.packages.length !== 1) {
throw new schematics_1.SchematicsException('--from requires that only a single package be passed.');
}
}
options.from = _formatVersion(options.from);
options.to = _formatVersion(options.to);
const usingYarn = options.packageManager === 'yarn';
return async (tree, context) => {
const logger = context.logger;
const npmDeps = new Map(_getAllDependencies(tree).filter(([name, specifier]) => {
try {
return isPkgFromRegistry(name, specifier);
}
catch {
logger.warn(`Package ${name} was not found on the registry. Skipping.`);
return false;
}
}));
const packages = _buildPackageList(options, npmDeps, logger);
// Grab all package.json from the npm repository. This requires a lot of HTTP calls so we
// try to parallelize as many as possible.
const allPackageMetadata = await Promise.all(Array.from(npmDeps.keys()).map((depName) => (0, package_metadata_1.getNpmPackageJson)(depName, logger, {
registry: options.registry,
usingYarn,
verbose: options.verbose,
})));
// Build a map of all dependencies and their packageJson.
const npmPackageJsonMap = allPackageMetadata.reduce((acc, npmPackageJson) => {
// If the package was not found on the registry. It could be private, so we will just
// ignore. If the package was part of the list, we will error out, but will simply ignore
// if it's either not requested (so just part of package.json. silently).
if (!npmPackageJson.name) {
if (npmPackageJson.requestedName && packages.has(npmPackageJson.requestedName)) {
throw new schematics_1.SchematicsException(`Package ${JSON.stringify(npmPackageJson.requestedName)} was not found on the ` +
'registry. Cannot continue as this may be an error.');
}
}
else {
// If a name is present, it is assumed to be fully populated
acc.set(npmPackageJson.name, npmPackageJson);
}
return acc;
}, new Map());
// Augment the command line package list with packageGroups and forward peer dependencies.
// Each added package may uncover new package groups and peer dependencies, so we must
// repeat this process until the package list stabilizes.
let lastPackagesSize;
do {
lastPackagesSize = packages.size;
npmPackageJsonMap.forEach((npmPackageJson) => {
_addPackageGroup(tree, packages, npmDeps, npmPackageJson, logger);
_addPeerDependencies(tree, packages, npmDeps, npmPackageJson, npmPackageJsonMap, logger);
});
} while (packages.size > lastPackagesSize);
// Build the PackageInfo for each module.
const packageInfoMap = new Map();
npmPackageJsonMap.forEach((npmPackageJson) => {
packageInfoMap.set(npmPackageJson.name, _buildPackageInfo(tree, packages, npmDeps, npmPackageJson, logger));
});
// Now that we have all the information, check the flags.
if (packages.size > 0) {
if (options.migrateOnly && options.from && options.packages) {
return;
}
const sublog = new core_1.logging.LevelCapLogger('validation', logger.createChild(''), 'warn');
_validateUpdatePackages(packageInfoMap, !!options.force, !!options.next, sublog);
_performUpdate(tree, context, packageInfoMap, logger, !!options.migrateOnly);
}
else {
_usageMessage(options, packageInfoMap, logger);
}
};
}

Some files were not shown because too many files have changed in this diff Show More