mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-15 01:54:04 +08:00
Deprecates the standalone utilities that were added specifically for the Components project, but ended up being used externally in favor of the newly-introduced rules.
571 lines
18 KiB
TypeScript
571 lines
18 KiB
TypeScript
/**
|
|
* @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.io/license
|
|
*/
|
|
|
|
import { SchematicsException, Tree, UpdateRecorder } from '@angular-devkit/schematics';
|
|
import { dirname, join } from 'path';
|
|
import ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript';
|
|
import { insertImport } from '../utility/ast-utils';
|
|
import { InsertChange } from '../utility/change';
|
|
|
|
/** App config that was resolved to its source node. */
|
|
interface ResolvedAppConfig {
|
|
/** Tree-relative path of the file containing the app config. */
|
|
filePath: string;
|
|
|
|
/** Node defining the app config. */
|
|
node: ts.ObjectLiteralExpression;
|
|
}
|
|
|
|
/**
|
|
* Checks whether the providers from a module are being imported in a `bootstrapApplication` call.
|
|
* @param tree File tree of the project.
|
|
* @param filePath Path of the file in which to check.
|
|
* @param className Class name of the module to search for.
|
|
* @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from
|
|
* `@schematics/angular/utility` instead.
|
|
*/
|
|
export function importsProvidersFrom(tree: Tree, filePath: string, className: string): boolean {
|
|
const sourceFile = createSourceFile(tree, filePath);
|
|
const bootstrapCall = findBootstrapApplicationCall(sourceFile);
|
|
const appConfig = bootstrapCall ? findAppConfig(bootstrapCall, tree, filePath) : null;
|
|
const importProvidersFromCall = appConfig ? findImportProvidersFromCall(appConfig.node) : null;
|
|
|
|
return !!importProvidersFromCall?.arguments.some(
|
|
(arg) => ts.isIdentifier(arg) && arg.text === className,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Checks whether a providers function is being called in a `bootstrapApplication` call.
|
|
* @param tree File tree of the project.
|
|
* @param filePath Path of the file in which to check.
|
|
* @param functionName Name of the function to search for.
|
|
* @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from
|
|
* `@schematics/angular/utility` instead.
|
|
*/
|
|
export function callsProvidersFunction(
|
|
tree: Tree,
|
|
filePath: string,
|
|
functionName: string,
|
|
): boolean {
|
|
const sourceFile = createSourceFile(tree, filePath);
|
|
const bootstrapCall = findBootstrapApplicationCall(sourceFile);
|
|
const appConfig = bootstrapCall ? findAppConfig(bootstrapCall, tree, filePath) : null;
|
|
const providersLiteral = appConfig ? findProvidersLiteral(appConfig.node) : null;
|
|
|
|
return !!providersLiteral?.elements.some(
|
|
(el) =>
|
|
ts.isCallExpression(el) &&
|
|
ts.isIdentifier(el.expression) &&
|
|
el.expression.text === functionName,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Adds an `importProvidersFrom` call to the `bootstrapApplication` call.
|
|
* @param tree File tree of the project.
|
|
* @param filePath Path to the file that should be updated.
|
|
* @param moduleName Name of the module that should be imported.
|
|
* @param modulePath Path from which to import the module.
|
|
* @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from
|
|
* `@schematics/angular/utility` instead.
|
|
*/
|
|
export function addModuleImportToStandaloneBootstrap(
|
|
tree: Tree,
|
|
filePath: string,
|
|
moduleName: string,
|
|
modulePath: string,
|
|
) {
|
|
const sourceFile = createSourceFile(tree, filePath);
|
|
const bootstrapCall = findBootstrapApplicationCall(sourceFile);
|
|
const addImports = (file: ts.SourceFile, recorder: UpdateRecorder) => {
|
|
const sourceText = file.getText();
|
|
|
|
[
|
|
insertImport(file, sourceText, moduleName, modulePath),
|
|
insertImport(file, sourceText, 'importProvidersFrom', '@angular/core'),
|
|
].forEach((change) => {
|
|
if (change instanceof InsertChange) {
|
|
recorder.insertLeft(change.pos, change.toAdd);
|
|
}
|
|
});
|
|
};
|
|
|
|
if (!bootstrapCall) {
|
|
throw new SchematicsException(`Could not find bootstrapApplication call in ${filePath}`);
|
|
}
|
|
|
|
const importProvidersCall = ts.factory.createCallExpression(
|
|
ts.factory.createIdentifier('importProvidersFrom'),
|
|
[],
|
|
[ts.factory.createIdentifier(moduleName)],
|
|
);
|
|
|
|
// If there's only one argument, we have to create a new object literal.
|
|
if (bootstrapCall.arguments.length === 1) {
|
|
const recorder = tree.beginUpdate(filePath);
|
|
addNewAppConfigToCall(bootstrapCall, importProvidersCall, recorder);
|
|
addImports(sourceFile, recorder);
|
|
tree.commitUpdate(recorder);
|
|
|
|
return;
|
|
}
|
|
|
|
// If the config is a `mergeApplicationProviders` call, add another config to it.
|
|
if (isMergeAppConfigCall(bootstrapCall.arguments[1])) {
|
|
const recorder = tree.beginUpdate(filePath);
|
|
addNewAppConfigToCall(bootstrapCall.arguments[1], importProvidersCall, recorder);
|
|
addImports(sourceFile, recorder);
|
|
tree.commitUpdate(recorder);
|
|
|
|
return;
|
|
}
|
|
|
|
// Otherwise attempt to merge into the current config.
|
|
const appConfig = findAppConfig(bootstrapCall, tree, filePath);
|
|
|
|
if (!appConfig) {
|
|
throw new SchematicsException(
|
|
`Could not statically analyze config in bootstrapApplication call in ${filePath}`,
|
|
);
|
|
}
|
|
|
|
const { filePath: configFilePath, node: config } = appConfig;
|
|
const recorder = tree.beginUpdate(configFilePath);
|
|
const importCall = findImportProvidersFromCall(config);
|
|
|
|
addImports(config.getSourceFile(), recorder);
|
|
|
|
if (importCall) {
|
|
// If there's an `importProvidersFrom` call already, add the module to it.
|
|
recorder.insertRight(
|
|
importCall.arguments[importCall.arguments.length - 1].getEnd(),
|
|
`, ${moduleName}`,
|
|
);
|
|
} else {
|
|
const providersLiteral = findProvidersLiteral(config);
|
|
|
|
if (providersLiteral) {
|
|
// If there's a `providers` array, add the import to it.
|
|
addElementToArray(providersLiteral, importProvidersCall, recorder);
|
|
} else {
|
|
// Otherwise add a `providers` array to the existing object literal.
|
|
addProvidersToObjectLiteral(config, importProvidersCall, recorder);
|
|
}
|
|
}
|
|
|
|
tree.commitUpdate(recorder);
|
|
}
|
|
|
|
/**
|
|
* Adds a providers function call to the `bootstrapApplication` call.
|
|
* @param tree File tree of the project.
|
|
* @param filePath Path to the file that should be updated.
|
|
* @param functionName Name of the function that should be called.
|
|
* @param importPath Path from which to import the function.
|
|
* @param args Arguments to use when calling the function.
|
|
* @returns The file path that the provider was added to.
|
|
* @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from
|
|
* `@schematics/angular/utility` instead.
|
|
*/
|
|
export function addFunctionalProvidersToStandaloneBootstrap(
|
|
tree: Tree,
|
|
filePath: string,
|
|
functionName: string,
|
|
importPath: string,
|
|
args: ts.Expression[] = [],
|
|
): string {
|
|
const sourceFile = createSourceFile(tree, filePath);
|
|
const bootstrapCall = findBootstrapApplicationCall(sourceFile);
|
|
const addImports = (file: ts.SourceFile, recorder: UpdateRecorder) => {
|
|
const change = insertImport(file, file.getText(), functionName, importPath);
|
|
|
|
if (change instanceof InsertChange) {
|
|
recorder.insertLeft(change.pos, change.toAdd);
|
|
}
|
|
};
|
|
|
|
if (!bootstrapCall) {
|
|
throw new SchematicsException(`Could not find bootstrapApplication call in ${filePath}`);
|
|
}
|
|
|
|
const providersCall = ts.factory.createCallExpression(
|
|
ts.factory.createIdentifier(functionName),
|
|
undefined,
|
|
args,
|
|
);
|
|
|
|
// If there's only one argument, we have to create a new object literal.
|
|
if (bootstrapCall.arguments.length === 1) {
|
|
const recorder = tree.beginUpdate(filePath);
|
|
addNewAppConfigToCall(bootstrapCall, providersCall, recorder);
|
|
addImports(sourceFile, recorder);
|
|
tree.commitUpdate(recorder);
|
|
|
|
return filePath;
|
|
}
|
|
|
|
// If the config is a `mergeApplicationProviders` call, add another config to it.
|
|
if (isMergeAppConfigCall(bootstrapCall.arguments[1])) {
|
|
const recorder = tree.beginUpdate(filePath);
|
|
addNewAppConfigToCall(bootstrapCall.arguments[1], providersCall, recorder);
|
|
addImports(sourceFile, recorder);
|
|
tree.commitUpdate(recorder);
|
|
|
|
return filePath;
|
|
}
|
|
|
|
// Otherwise attempt to merge into the current config.
|
|
const appConfig = findAppConfig(bootstrapCall, tree, filePath);
|
|
|
|
if (!appConfig) {
|
|
throw new SchematicsException(
|
|
`Could not statically analyze config in bootstrapApplication call in ${filePath}`,
|
|
);
|
|
}
|
|
|
|
const { filePath: configFilePath, node: config } = appConfig;
|
|
const recorder = tree.beginUpdate(configFilePath);
|
|
const providersLiteral = findProvidersLiteral(config);
|
|
|
|
addImports(config.getSourceFile(), recorder);
|
|
|
|
if (providersLiteral) {
|
|
// If there's a `providers` array, add the import to it.
|
|
addElementToArray(providersLiteral, providersCall, recorder);
|
|
} else {
|
|
// Otherwise add a `providers` array to the existing object literal.
|
|
addProvidersToObjectLiteral(config, providersCall, recorder);
|
|
}
|
|
|
|
tree.commitUpdate(recorder);
|
|
|
|
return configFilePath;
|
|
}
|
|
|
|
/**
|
|
* Finds the call to `bootstrapApplication` within a file.
|
|
* @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from
|
|
* `@schematics/angular/utility` instead.
|
|
*/
|
|
export function findBootstrapApplicationCall(sourceFile: ts.SourceFile): ts.CallExpression | null {
|
|
const localName = findImportLocalName(
|
|
sourceFile,
|
|
'bootstrapApplication',
|
|
'@angular/platform-browser',
|
|
);
|
|
|
|
if (!localName) {
|
|
return null;
|
|
}
|
|
|
|
let result: ts.CallExpression | null = null;
|
|
|
|
sourceFile.forEachChild(function walk(node) {
|
|
if (
|
|
ts.isCallExpression(node) &&
|
|
ts.isIdentifier(node.expression) &&
|
|
node.expression.text === localName
|
|
) {
|
|
result = node;
|
|
}
|
|
|
|
if (!result) {
|
|
node.forEachChild(walk);
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
/** Find a call to `importProvidersFrom` within an application config. */
|
|
function findImportProvidersFromCall(config: ts.ObjectLiteralExpression): ts.CallExpression | null {
|
|
const importProvidersName = findImportLocalName(
|
|
config.getSourceFile(),
|
|
'importProvidersFrom',
|
|
'@angular/core',
|
|
);
|
|
const providersLiteral = findProvidersLiteral(config);
|
|
|
|
if (providersLiteral && importProvidersName) {
|
|
for (const element of providersLiteral.elements) {
|
|
// Look for an array element that calls the `importProvidersFrom` function.
|
|
if (
|
|
ts.isCallExpression(element) &&
|
|
ts.isIdentifier(element.expression) &&
|
|
element.expression.text === importProvidersName
|
|
) {
|
|
return element;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Finds the `providers` array literal within an application config. */
|
|
function findProvidersLiteral(
|
|
config: ts.ObjectLiteralExpression,
|
|
): ts.ArrayLiteralExpression | null {
|
|
for (const prop of config.properties) {
|
|
if (
|
|
ts.isPropertyAssignment(prop) &&
|
|
ts.isIdentifier(prop.name) &&
|
|
prop.name.text === 'providers' &&
|
|
ts.isArrayLiteralExpression(prop.initializer)
|
|
) {
|
|
return prop.initializer;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Resolves the node that defines the app config from a bootstrap call.
|
|
* @param bootstrapCall Call for which to resolve the config.
|
|
* @param tree File tree of the project.
|
|
* @param filePath File path of the bootstrap call.
|
|
*/
|
|
function findAppConfig(
|
|
bootstrapCall: ts.CallExpression,
|
|
tree: Tree,
|
|
filePath: string,
|
|
): ResolvedAppConfig | null {
|
|
if (bootstrapCall.arguments.length > 1) {
|
|
const config = bootstrapCall.arguments[1];
|
|
|
|
if (ts.isObjectLiteralExpression(config)) {
|
|
return { filePath, node: config };
|
|
}
|
|
|
|
if (ts.isIdentifier(config)) {
|
|
return resolveAppConfigFromIdentifier(config, tree, filePath);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Resolves the app config from an identifier referring to it.
|
|
* @param identifier Identifier referring to the app config.
|
|
* @param tree File tree of the project.
|
|
* @param bootstapFilePath Path of the bootstrap call.
|
|
*/
|
|
function resolveAppConfigFromIdentifier(
|
|
identifier: ts.Identifier,
|
|
tree: Tree,
|
|
bootstapFilePath: string,
|
|
): ResolvedAppConfig | null {
|
|
const sourceFile = identifier.getSourceFile();
|
|
|
|
for (const node of sourceFile.statements) {
|
|
// Only look at relative imports. This will break if the app uses a path
|
|
// mapping to refer to the import, but in order to resolve those, we would
|
|
// need knowledge about the entire program.
|
|
if (
|
|
!ts.isImportDeclaration(node) ||
|
|
!node.importClause?.namedBindings ||
|
|
!ts.isNamedImports(node.importClause.namedBindings) ||
|
|
!ts.isStringLiteralLike(node.moduleSpecifier) ||
|
|
!node.moduleSpecifier.text.startsWith('.')
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
for (const specifier of node.importClause.namedBindings.elements) {
|
|
if (specifier.name.text !== identifier.text) {
|
|
continue;
|
|
}
|
|
|
|
// Look for a variable with the imported name in the file. Note that ideally we would use
|
|
// the type checker to resolve this, but we can't because these utilities are set up to
|
|
// operate on individual files, not the entire program.
|
|
const filePath = join(dirname(bootstapFilePath), node.moduleSpecifier.text + '.ts');
|
|
const importedSourceFile = createSourceFile(tree, filePath);
|
|
const resolvedVariable = findAppConfigFromVariableName(
|
|
importedSourceFile,
|
|
(specifier.propertyName || specifier.name).text,
|
|
);
|
|
|
|
if (resolvedVariable) {
|
|
return { filePath, node: resolvedVariable };
|
|
}
|
|
}
|
|
}
|
|
|
|
const variableInSameFile = findAppConfigFromVariableName(sourceFile, identifier.text);
|
|
|
|
return variableInSameFile ? { filePath: bootstapFilePath, node: variableInSameFile } : null;
|
|
}
|
|
|
|
/**
|
|
* Finds an app config within the top-level variables of a file.
|
|
* @param sourceFile File in which to search for the config.
|
|
* @param variableName Name of the variable containing the config.
|
|
*/
|
|
function findAppConfigFromVariableName(
|
|
sourceFile: ts.SourceFile,
|
|
variableName: string,
|
|
): ts.ObjectLiteralExpression | null {
|
|
for (const node of sourceFile.statements) {
|
|
if (ts.isVariableStatement(node)) {
|
|
for (const decl of node.declarationList.declarations) {
|
|
if (
|
|
ts.isIdentifier(decl.name) &&
|
|
decl.name.text === variableName &&
|
|
decl.initializer &&
|
|
ts.isObjectLiteralExpression(decl.initializer)
|
|
) {
|
|
return decl.initializer;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Finds the local name of an imported symbol. Could be the symbol name itself or its alias.
|
|
* @param sourceFile File within which to search for the import.
|
|
* @param name Actual name of the import, not its local alias.
|
|
* @param moduleName Name of the module from which the symbol is imported.
|
|
*/
|
|
function findImportLocalName(
|
|
sourceFile: ts.SourceFile,
|
|
name: string,
|
|
moduleName: string,
|
|
): string | null {
|
|
for (const node of sourceFile.statements) {
|
|
// Only look for top-level imports.
|
|
if (
|
|
!ts.isImportDeclaration(node) ||
|
|
!ts.isStringLiteral(node.moduleSpecifier) ||
|
|
node.moduleSpecifier.text !== moduleName
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// Filter out imports that don't have the right shape.
|
|
if (
|
|
!node.importClause ||
|
|
!node.importClause.namedBindings ||
|
|
!ts.isNamedImports(node.importClause.namedBindings)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// Look through the elements of the declaration for the specific import.
|
|
for (const element of node.importClause.namedBindings.elements) {
|
|
if ((element.propertyName || element.name).text === name) {
|
|
// The local name is always in `name`.
|
|
return element.name.text;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Creates a source file from a file path within a project. */
|
|
function createSourceFile(tree: Tree, filePath: string): ts.SourceFile {
|
|
return ts.createSourceFile(filePath, tree.readText(filePath), ts.ScriptTarget.Latest, true);
|
|
}
|
|
|
|
/**
|
|
* Creates a new app config object literal and adds it to a call expression as an argument.
|
|
* @param call Call to which to add the config.
|
|
* @param expression Expression that should inserted into the new config.
|
|
* @param recorder Recorder to which to log the change.
|
|
*/
|
|
function addNewAppConfigToCall(
|
|
call: ts.CallExpression,
|
|
expression: ts.Expression,
|
|
recorder: UpdateRecorder,
|
|
): void {
|
|
const newCall = ts.factory.updateCallExpression(call, call.expression, call.typeArguments, [
|
|
...call.arguments,
|
|
ts.factory.createObjectLiteralExpression(
|
|
[
|
|
ts.factory.createPropertyAssignment(
|
|
'providers',
|
|
ts.factory.createArrayLiteralExpression([expression]),
|
|
),
|
|
],
|
|
true,
|
|
),
|
|
]);
|
|
|
|
recorder.remove(call.getStart(), call.getWidth());
|
|
recorder.insertRight(
|
|
call.getStart(),
|
|
ts.createPrinter().printNode(ts.EmitHint.Unspecified, newCall, call.getSourceFile()),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Adds an element to an array literal expression.
|
|
* @param node Array to which to add the element.
|
|
* @param element Element to be added.
|
|
* @param recorder Recorder to which to log the change.
|
|
*/
|
|
function addElementToArray(
|
|
node: ts.ArrayLiteralExpression,
|
|
element: ts.Expression,
|
|
recorder: UpdateRecorder,
|
|
): void {
|
|
const newLiteral = ts.factory.updateArrayLiteralExpression(node, [...node.elements, element]);
|
|
recorder.remove(node.getStart(), node.getWidth());
|
|
recorder.insertRight(
|
|
node.getStart(),
|
|
ts.createPrinter().printNode(ts.EmitHint.Unspecified, newLiteral, node.getSourceFile()),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Adds a `providers` property to an object literal.
|
|
* @param node Literal to which to add the `providers`.
|
|
* @param expression Provider that should be part of the generated `providers` array.
|
|
* @param recorder Recorder to which to log the change.
|
|
*/
|
|
function addProvidersToObjectLiteral(
|
|
node: ts.ObjectLiteralExpression,
|
|
expression: ts.Expression,
|
|
recorder: UpdateRecorder,
|
|
) {
|
|
const newOptionsLiteral = ts.factory.updateObjectLiteralExpression(node, [
|
|
...node.properties,
|
|
ts.factory.createPropertyAssignment(
|
|
'providers',
|
|
ts.factory.createArrayLiteralExpression([expression]),
|
|
),
|
|
]);
|
|
recorder.remove(node.getStart(), node.getWidth());
|
|
recorder.insertRight(
|
|
node.getStart(),
|
|
ts.createPrinter().printNode(ts.EmitHint.Unspecified, newOptionsLiteral, node.getSourceFile()),
|
|
);
|
|
}
|
|
|
|
/** Checks whether a node is a call to `mergeApplicationConfig`. */
|
|
function isMergeAppConfigCall(node: ts.Node): node is ts.CallExpression {
|
|
if (!ts.isCallExpression(node)) {
|
|
return false;
|
|
}
|
|
|
|
const localName = findImportLocalName(
|
|
node.getSourceFile(),
|
|
'mergeApplicationConfig',
|
|
'@angular/core',
|
|
);
|
|
|
|
return !!localName && ts.isIdentifier(node.expression) && node.expression.text === localName;
|
|
}
|