Browse Source

schematics standalone utilities

pull/22645/head
erdemcaygor 1 year ago
parent
commit
ebaf0be8f9
  1. 25
      npm/ng-packs/packages/schematics/src/utils/angular/eol.ts
  2. 148
      npm/ng-packs/packages/schematics/src/utils/angular/standalone/app_component.ts
  3. 127
      npm/ng-packs/packages/schematics/src/utils/angular/standalone/app_config.ts
  4. 115
      npm/ng-packs/packages/schematics/src/utils/angular/standalone/code_block.ts
  5. 10
      npm/ng-packs/packages/schematics/src/utils/angular/standalone/index.ts
  6. 258
      npm/ng-packs/packages/schematics/src/utils/angular/standalone/rules.ts
  7. 171
      npm/ng-packs/packages/schematics/src/utils/angular/standalone/util.ts

25
npm/ng-packs/packages/schematics/src/utils/angular/eol.ts

@ -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 { EOL } from 'node:os';
const CRLF = '\r\n';
const LF = '\n';
export function getEOL(content: string): string {
const newlines = content.match(/(?:\r?\n)/g);
if (newlines?.length) {
const crlf = newlines.filter(l => l === CRLF).length;
const lf = newlines.length - crlf;
return crlf > lf ? CRLF : LF;
}
return EOL;
}

148
npm/ng-packs/packages/schematics/src/utils/angular/standalone/app_component.ts

@ -0,0 +1,148 @@
/**
* @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 { SchematicsException, Tree } from '@angular-devkit/schematics';
import * as ts from 'typescript';
import { getDecoratorMetadata, getMetadataField } from '../ast-utils';
import { findBootstrapModuleCall, getAppModulePath } from '../ng-ast-utils';
import { findBootstrapApplicationCall, getSourceFile } from './util';
/** Data resolved for a bootstrapped component. */
interface BootstrappedComponentData {
/** Original name of the component class. */
componentName: string;
/** Path under which the component was imported in the main entrypoint. */
componentImportPathInSameFile: string;
/** Original name of the NgModule being bootstrapped, null if the app isn't module-based. */
moduleName: string | null;
/**
* Path under which the module was imported in the main entrypoint,
* null if the app isn't module-based.
*/
moduleImportPathInSameFile: string | null;
}
/**
* Finds the original name and path relative to the `main.ts` of the bootrstrapped app component.
* @param tree File tree in which to look for the component.
* @param mainFilePath Path of the `main` file.
*/
export function resolveBootstrappedComponentData(
tree: Tree,
mainFilePath: string,
): BootstrappedComponentData | null {
// First try to resolve for a standalone app.
try {
const call = findBootstrapApplicationCall(tree, mainFilePath);
if (call.arguments.length > 0 && ts.isIdentifier(call.arguments[0])) {
const resolved = resolveIdentifier(call.arguments[0]);
if (resolved) {
return {
componentName: resolved.name,
componentImportPathInSameFile: resolved.path,
moduleName: null,
moduleImportPathInSameFile: null,
};
}
}
} catch (e) {
// `findBootstrapApplicationCall` will throw if it can't find the `bootrstrapApplication` call.
// Catch so we can continue to the fallback logic.
if (!(e instanceof SchematicsException)) {
throw e;
}
}
// Otherwise fall back to resolving an NgModule-based app.
return resolveNgModuleBasedData(tree, mainFilePath);
}
/** Resolves the bootstrap data for a NgModule-based app. */
function resolveNgModuleBasedData(
tree: Tree,
mainFilePath: string,
): BootstrappedComponentData | null {
const appModulePath = getAppModulePath(tree, mainFilePath);
const appModuleFile = getSourceFile(tree, appModulePath);
const metadataNodes = getDecoratorMetadata(appModuleFile, 'NgModule', '@angular/core');
for (const node of metadataNodes) {
if (!ts.isObjectLiteralExpression(node)) {
continue;
}
const bootstrapProp = getMetadataField(node, 'bootstrap').find(prop => {
return (
ts.isArrayLiteralExpression(prop.initializer) &&
prop.initializer.elements.length > 0 &&
ts.isIdentifier(prop.initializer.elements[0])
);
});
const componentIdentifier = (bootstrapProp?.initializer as ts.ArrayLiteralExpression)
.elements[0] as ts.Identifier | undefined;
const componentResult = componentIdentifier ? resolveIdentifier(componentIdentifier) : null;
const bootstrapCall = findBootstrapModuleCall(tree, mainFilePath);
if (
componentResult &&
bootstrapCall &&
bootstrapCall.arguments.length > 0 &&
ts.isIdentifier(bootstrapCall.arguments[0])
) {
const moduleResult = resolveIdentifier(bootstrapCall.arguments[0]);
if (moduleResult) {
return {
componentName: componentResult.name,
componentImportPathInSameFile: componentResult.path,
moduleName: moduleResult.name,
moduleImportPathInSameFile: moduleResult.path,
};
}
}
}
return null;
}
/** Resolves an identifier to its original name and path that it was imported from. */
function resolveIdentifier(identifier: ts.Identifier): { name: string; path: string } | null {
const sourceFile = identifier.getSourceFile();
// Try to resolve the import path by looking at the top-level named imports of the file.
for (const node of sourceFile.statements) {
if (
!ts.isImportDeclaration(node) ||
!ts.isStringLiteral(node.moduleSpecifier) ||
!node.importClause ||
!node.importClause.namedBindings ||
!ts.isNamedImports(node.importClause.namedBindings)
) {
continue;
}
for (const element of node.importClause.namedBindings.elements) {
if (element.name.text === identifier.text) {
return {
// Note that we use `propertyName` if available, because it contains
// the real name in the case where the import is aliased.
name: (element.propertyName || element.name).text,
path: node.moduleSpecifier.text,
};
}
}
}
return null;
}

127
npm/ng-packs/packages/schematics/src/utils/angular/standalone/app_config.ts

@ -0,0 +1,127 @@
/**
* @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 { Tree } from '@angular-devkit/schematics';
import { dirname, join } from 'node:path';
import * as ts from 'typescript';
import { getSourceFile } from './util';
/** App config that was resolved to its source node. */
export interface ResolvedAppConfig {
/** Tree-relative path of the file containing the app config. */
filePath: string;
/** Node defining the app config. */
node: ts.ObjectLiteralExpression;
}
/**
* 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.
*/
export 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 = getSourceFile(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;
}

115
npm/ng-packs/packages/schematics/src/utils/angular/standalone/code_block.ts

@ -0,0 +1,115 @@
/**
* @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, Tree } from '@angular-devkit/schematics';
import * as ts from 'typescript';
import { hasTopLevelIdentifier, insertImport } from '../ast-utils';
import { applyToUpdateRecorder } from '../change';
/** Generated code that hasn't been interpolated yet. */
export interface PendingCode {
/** Code that will be inserted. */
expression: string;
/** Imports that need to be added to the file in which the code is inserted. */
imports: PendingImports;
}
/** Map keeping track of imports and aliases under which they're referred to in an expression. */
type PendingImports = Map<string, Map<string, string>>;
/** Counter used to generate unique IDs. */
let uniqueIdCounter = 0;
/**
* Callback invoked by a Rule that produces the code
* that needs to be inserted somewhere in the app.
*/
export type CodeBlockCallback = (block: CodeBlock) => PendingCode;
/**
* Utility class used to generate blocks of code that
* can be inserted by the devkit into a user's app.
*/
export class CodeBlock {
private _imports: PendingImports = new Map<string, Map<string, string>>();
// Note: the methods here are defined as arrow function so that they can be destructured by
// consumers without losing their context. This makes the API more concise.
/** Function used to tag a code block in order to produce a `PendingCode` object. */
code = (strings: TemplateStringsArray, ...params: unknown[]): PendingCode => {
return {
expression: strings.map((part, index) => part + (params[index] || '')).join(''),
imports: this._imports,
};
};
/**
* Used inside of a code block to mark external symbols and which module they should be imported
* from. When the code is inserted, the required import statements will be produced automatically.
* @param symbolName Name of the external symbol.
* @param moduleName Module from which the symbol should be imported.
*/
external = (symbolName: string, moduleName: string): string => {
if (!this._imports.has(moduleName)) {
this._imports.set(moduleName, new Map());
}
const symbolsPerModule = this._imports.get(moduleName) as Map<string, string>;
if (!symbolsPerModule.has(symbolName)) {
symbolsPerModule.set(symbolName, `@@__SCHEMATIC_PLACEHOLDER_${uniqueIdCounter++}__@@`);
}
return symbolsPerModule.get(symbolName) as string;
};
/**
* Produces the necessary rules to transform a `PendingCode` object into valid code.
* @param initialCode Code pending transformed.
* @param filePath Path of the file in which the code will be inserted.
*/
static transformPendingCode(initialCode: PendingCode, filePath: string) {
const code = { ...initialCode };
const rules: Rule[] = [];
code.imports.forEach((symbols, moduleName) => {
symbols.forEach((placeholder, symbolName) => {
rules.push((tree: Tree) => {
const recorder = tree.beginUpdate(filePath);
const sourceFile = ts.createSourceFile(
filePath,
tree.readText(filePath),
ts.ScriptTarget.Latest,
true,
);
// Note that this could still technically clash if there's a top-level symbol called
// `${symbolName}_alias`, however this is unlikely. We can revisit this if it becomes
// a problem.
const alias = hasTopLevelIdentifier(sourceFile, symbolName, moduleName)
? symbolName + '_alias'
: undefined;
code.expression = code.expression.replace(
new RegExp(placeholder, 'g'),
alias || symbolName,
);
applyToUpdateRecorder(recorder, [
insertImport(sourceFile, filePath, symbolName, moduleName, false, alias),
]);
tree.commitUpdate(recorder);
});
});
});
return { code, rules };
}
}

10
npm/ng-packs/packages/schematics/src/utils/angular/standalone/index.ts

@ -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 { addRootImport, addRootProvider } from './rules';
export type { PendingCode, CodeBlockCallback, CodeBlock } from './code_block';

258
npm/ng-packs/packages/schematics/src/utils/angular/standalone/rules.ts

@ -0,0 +1,258 @@
/**
* @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 { tags } from '@angular-devkit/core';
import { Rule, SchematicsException, Tree, chain } from '@angular-devkit/schematics';
import * as ts from 'typescript';
import { addSymbolToNgModuleMetadata, insertAfterLastOccurrence } from '../ast-utils';
import { InsertChange } from '../change';
import { getAppModulePath, isStandaloneApp } from '../ng-ast-utils';
import { ResolvedAppConfig, findAppConfig } from './app_config';
import { CodeBlock, CodeBlockCallback, PendingCode } from './code_block';
import {
applyChangesToFile,
findBootstrapApplicationCall,
findProvidersLiteral,
getMainFilePath,
getSourceFile,
isMergeAppConfigCall,
} from './util';
/**
* Adds an import to the root of the project.
* @param project Name of the project to which to add the import.
* @param callback Function that generates the code block which should be inserted.
* @example
*
* ```ts
* import { Rule } from '@angular-devkit/schematics';
* import { addRootImport } from '@schematics/angular/utility';
*
* export default function(): Rule {
* return addRootImport('default', ({code, external}) => {
* return code`${external('MyModule', '@my/module')}.forRoot({})`;
* });
* }
* ```
*/
export function addRootImport(project: string, callback: CodeBlockCallback): Rule {
return getRootInsertionRule(project, callback, 'imports', {
name: 'importProvidersFrom',
module: '@angular/core',
});
}
/**
* Adds a provider to the root of the project.
* @param project Name of the project to which to add the import.
* @param callback Function that generates the code block which should be inserted.
* @example
*
* ```ts
* import { Rule } from '@angular-devkit/schematics';
* import { addRootProvider } from '@schematics/angular/utility';
*
* export default function(): Rule {
* return addRootProvider('default', ({code, external}) => {
* return code`${external('provideLibrary', '@my/library')}({})`;
* });
* }
* ```
*/
export function addRootProvider(project: string, callback: CodeBlockCallback): Rule {
return getRootInsertionRule(project, callback, 'providers');
}
/**
* Creates a rule that inserts code at the root of either a standalone or NgModule-based project.
* @param project Name of the project into which to inser tthe code.
* @param callback Function that generates the code block which should be inserted.
* @param ngModuleField Field of the root NgModule into which the code should be inserted, if the
* app is based on NgModule
* @param standaloneWrapperFunction Function with which to wrap the code if the app is standalone.
*/
function getRootInsertionRule(
project: string,
callback: CodeBlockCallback,
ngModuleField: string,
standaloneWrapperFunction?: { name: string; module: string },
): Rule {
return async host => {
const mainFilePath = await getMainFilePath(host, project);
const codeBlock = new CodeBlock();
if (isStandaloneApp(host, mainFilePath)) {
return tree =>
addProviderToStandaloneBootstrap(
tree,
callback(codeBlock),
mainFilePath,
standaloneWrapperFunction,
);
}
const modulePath = getAppModulePath(host, mainFilePath);
const pendingCode = CodeBlock.transformPendingCode(callback(codeBlock), modulePath);
return chain([
...pendingCode.rules,
tree => {
const changes = addSymbolToNgModuleMetadata(
getSourceFile(tree, modulePath),
modulePath,
ngModuleField,
pendingCode.code.expression,
// Explicitly set the import path to null since we deal with imports here separately.
null,
);
applyChangesToFile(tree, modulePath, changes);
},
]);
};
}
/**
* Adds a provider to the root of a standalone project.
* @param host Tree of the root rule.
* @param pendingCode Code that should be inserted.
* @param mainFilePath Path to the project's main file.
* @param wrapperFunction Optional function with which to wrap the provider.
*/
function addProviderToStandaloneBootstrap(
host: Tree,
pendingCode: PendingCode,
mainFilePath: string,
wrapperFunction?: { name: string; module: string },
): Rule {
const bootstrapCall = findBootstrapApplicationCall(host, mainFilePath);
const fileToEdit = findAppConfig(bootstrapCall, host, mainFilePath)?.filePath || mainFilePath;
const { code, rules } = CodeBlock.transformPendingCode(pendingCode, fileToEdit);
return chain([
...rules,
() => {
let wrapped: PendingCode;
let additionalRules: Rule[];
if (wrapperFunction) {
const block = new CodeBlock();
const result = CodeBlock.transformPendingCode(
block.code`${block.external(wrapperFunction.name, wrapperFunction.module)}(${
code.expression
})`,
fileToEdit,
);
wrapped = result.code;
additionalRules = result.rules;
} else {
wrapped = code;
additionalRules = [];
}
return chain([
...additionalRules,
tree => insertStandaloneRootProvider(tree, mainFilePath, wrapped.expression),
]);
},
]);
}
/**
* Inserts a string expression into the root of a standalone project.
* @param tree File tree used to modify the project.
* @param mainFilePath Path to the main file of the project.
* @param expression Code expression to be inserted.
*/
function insertStandaloneRootProvider(tree: Tree, mainFilePath: string, expression: string): void {
const bootstrapCall = findBootstrapApplicationCall(tree, mainFilePath);
const appConfig = findAppConfig(bootstrapCall, tree, mainFilePath);
if (bootstrapCall.arguments.length === 0) {
throw new SchematicsException(
`Cannot add provider to invalid bootstrapApplication call in ${
bootstrapCall.getSourceFile().fileName
}`,
);
}
if (appConfig) {
addProvidersExpressionToAppConfig(tree, appConfig, expression);
return;
}
const newAppConfig = `, {\n${tags.indentBy(2)`providers: [${expression}]`}\n}`;
let targetCall: ts.CallExpression;
if (bootstrapCall.arguments.length === 1) {
targetCall = bootstrapCall;
} else if (isMergeAppConfigCall(bootstrapCall.arguments[1])) {
targetCall = bootstrapCall.arguments[1];
} else {
throw new SchematicsException(
`Cannot statically analyze bootstrapApplication call in ${
bootstrapCall.getSourceFile().fileName
}`,
);
}
applyChangesToFile(tree, mainFilePath, [
insertAfterLastOccurrence(
targetCall.arguments,
newAppConfig,
mainFilePath,
targetCall.getEnd() - 1,
),
]);
}
/**
* Adds a string expression to an app config object.
* @param tree File tree used to modify the project.
* @param appConfig Resolved configuration object of the project.
* @param expression Code expression to be inserted.
*/
function addProvidersExpressionToAppConfig(
tree: Tree,
appConfig: ResolvedAppConfig,
expression: string,
): void {
const { node, filePath } = appConfig;
const configProps = node.properties;
const providersLiteral = findProvidersLiteral(node);
// If there's a `providers` property, we can add the provider
// to it, otherwise we need to declare it ourselves.
if (providersLiteral) {
applyChangesToFile(tree, filePath, [
insertAfterLastOccurrence(
providersLiteral.elements,
(providersLiteral.elements.length === 0 ? '' : ', ') + expression,
filePath,
providersLiteral.getStart() + 1,
),
]);
} else {
const prop = tags.indentBy(2)`providers: [${expression}]`;
let toInsert: string;
let insertPosition: number;
if (configProps.length === 0) {
toInsert = '\n' + prop + '\n';
insertPosition = node.getEnd() - 1;
} else {
const hasTrailingComma = configProps.hasTrailingComma;
toInsert = (hasTrailingComma ? '' : ',') + '\n' + prop;
insertPosition = configProps[configProps.length - 1].getEnd() + (hasTrailingComma ? 1 : 0);
}
applyChangesToFile(tree, filePath, [new InsertChange(filePath, insertPosition, toInsert)]);
}
}

171
npm/ng-packs/packages/schematics/src/utils/angular/standalone/util.ts

@ -0,0 +1,171 @@
/**
* @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 { SchematicsException, Tree } from '@angular-devkit/schematics';
import * as ts from 'typescript';
import { Change, applyToUpdateRecorder } from '../change';
import { targetBuildNotFoundError } from '../project-targets';
import { getWorkspace } from '../workspace';
import { Builders } from '../workspace-models';
/**
* Finds the main file of a project.
* @param tree File tree for the project.
* @param projectName Name of the project in which to search.
*/
export async function getMainFilePath(tree: Tree, projectName: string): Promise<string> {
const workspace = await getWorkspace(tree);
const project = workspace.projects.get(projectName);
const buildTarget = project?.targets.get('build');
if (!buildTarget) {
throw targetBuildNotFoundError();
}
const options = buildTarget.options as Record<string, string>;
return buildTarget.builder === Builders.Application ||
buildTarget.builder === Builders.BuildApplication
? options.browser
: options.main;
}
/**
* Gets a TypeScript source file at a specific path.
* @param tree File tree of a project.
* @param path Path to the file.
*/
export function getSourceFile(tree: Tree, path: string): ts.SourceFile {
const content = tree.readText(path);
const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
return source;
}
/** Finds the call to `bootstrapApplication` within a file. */
export function findBootstrapApplicationCall(tree: Tree, mainFilePath: string): ts.CallExpression {
const sourceFile = getSourceFile(tree, mainFilePath);
const localName = findImportLocalName(
sourceFile,
'bootstrapApplication',
'@angular/platform-browser',
);
if (localName) {
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);
}
});
if (result) {
return result;
}
}
throw new SchematicsException(`Could not find bootstrapApplication call in ${mainFilePath}`);
}
/**
* 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;
}
/**
* Applies a set of changes to a file.
* @param tree File tree of the project.
* @param path Path to the file that is being changed.
* @param changes Changes that should be applied to the file.
*/
export function applyChangesToFile(tree: Tree, path: string, changes: Change[]) {
if (changes.length > 0) {
const recorder = tree.beginUpdate(path);
applyToUpdateRecorder(recorder, changes);
tree.commitUpdate(recorder);
}
}
/** Checks whether a node is a call to `mergeApplicationConfig`. */
export 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;
}
/** Finds the `providers` array literal within an application config. */
export 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;
}
Loading…
Cancel
Save