From 8073c7a98ea5f8aef559a13938caecdbb475a3d1 Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Wed, 21 Feb 2024 17:52:33 +0200 Subject: [PATCH 1/2] refactor(ts-update): update deprecated method calls --- packages/core/typescript/TypeScriptFileUpdate.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/typescript/TypeScriptFileUpdate.ts b/packages/core/typescript/TypeScriptFileUpdate.ts index 888702025..37df5624e 100644 --- a/packages/core/typescript/TypeScriptFileUpdate.ts +++ b/packages/core/typescript/TypeScriptFileUpdate.ts @@ -324,7 +324,7 @@ export class TypeScriptFileUpdate { const index = existingProperties.indexOf(childrenProperty); const childrenPropertyName = childrenProperty.name; childrenProperty = - ts.updatePropertyAssignment( + ts.factory.updatePropertyAssignment( childrenProperty, childrenPropertyName, ts.factory.createArrayLiteralExpression([...newArrayValues]) @@ -332,7 +332,7 @@ export class TypeScriptFileUpdate { existingProperties .splice(index, 1, childrenProperty); } - return ts.updateObjectLiteral(currentNode, existingProperties) as ts.Node; + return ts.factory.updateObjectLiteralExpression(currentNode, existingProperties) as ts.Node; } else { return ts.visitEachChild(node, conditionalVisitor, context); } @@ -488,7 +488,7 @@ export class TypeScriptFileUpdate { newProps.push(ts.factory.createPropertyAssignment(prop, arrayExpr)); } - return ts.updateObjectLiteral(obj, [ + return ts.factory.updateObjectLiteralExpression(obj, [ ...objProperties, ...newProps ]); From 2f476762ac772039228fe10565d49a6ced76b72d Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Wed, 21 Feb 2024 19:11:28 +0200 Subject: [PATCH 2/2] feat(ng-add): add setup config provideAnimations --- .../core/typescript/TypeScriptFileUpdate.ts | 49 +++++++++++++++++++ packages/core/typescript/TypeScriptUtils.ts | 13 +++-- .../ng-schematics/src/cli-config/index.ts | 7 +++ .../src/cli-config/index_spec.ts | 48 ++++++++++++++++++ 4 files changed, 113 insertions(+), 4 deletions(-) diff --git a/packages/core/typescript/TypeScriptFileUpdate.ts b/packages/core/typescript/TypeScriptFileUpdate.ts index 37df5624e..27290409a 100644 --- a/packages/core/typescript/TypeScriptFileUpdate.ts +++ b/packages/core/typescript/TypeScriptFileUpdate.ts @@ -6,6 +6,7 @@ import { Util } from "../util/Util"; import { TypeScriptUtils as TsUtils } from "./TypeScriptUtils"; const DEFAULT_ROUTES_VARIABLE = "routes"; +const DEFAULT_APPCONFIG_VARIABLE = "appConfig"; /** * Apply various updates to typescript files using AST */ @@ -170,6 +171,54 @@ export class TypeScriptFileUpdate { this.ngMetaEdits.imports.push(...imports); } + /** + * Create a CallExpression for dep and add it to the `ApplicationConfig` providers array. + * @param dep The dependency to provide. TODO: Use different type to describe CallExpression, possible parameters, etc + * @param configVariable The name of the app config variable to edit + */ + public addAppConfigProvider(dep: Pick, configVariable = DEFAULT_APPCONFIG_VARIABLE) { + let providers = this.asArray(dep.provide, {}); + + const transformer: ts.TransformerFactory = (context: ts.TransformationContext) => + (rootNode: T) => { + const conditionalVisitor: ts.Visitor = (node: ts.Node): ts.Node => { + if (node.kind === ts.SyntaxKind.ArrayLiteralExpression && + node.parent.kind === ts.SyntaxKind.PropertyAssignment && + (node.parent as ts.PropertyAssignment).name.getText() === "providers") { + const array = (node as ts.ArrayLiteralExpression); + const nodes = ts.visitNodes(array.elements, visitor); + const alreadyProvided = nodes.map(x => TsUtils.getIdentifierName(x)); + + providers = providers.filter(x => alreadyProvided.indexOf(x) === -1); + this.requestImport(providers, dep.from); + + const newProvides = providers + .map(x => ts.factory.createCallExpression(ts.factory.createIdentifier(x), undefined, undefined)); + const elements = ts.factory.createNodeArray([ + ...nodes, + ...newProvides + ]); + + return ts.factory.updateArrayLiteralExpression(array, elements); + } else { + return ts.visitEachChild(node, conditionalVisitor, context); + } + }; + + const visitCondition = (node: ts.Node): boolean => { + return node.kind === ts.SyntaxKind.VariableDeclaration && + (node as ts.VariableDeclaration).name.getText() === configVariable && + (node as ts.VariableDeclaration).type.getText() === "ApplicationConfig"; + }; + const visitor: ts.Visitor = this.createVisitor(conditionalVisitor, visitCondition, context); + context.enableSubstitution(ts.SyntaxKind.ArrayLiteralExpression); + return ts.visitNode(rootNode, visitor); + }; + this.targetSource = ts.transform(this.targetSource, [transformer], { + pretty: true // oh well.. + }).transformed[0] as ts.SourceFile; + } + //#region File state /** Initializes existing imports info, [re]sets import and `NgModule` edits */ diff --git a/packages/core/typescript/TypeScriptUtils.ts b/packages/core/typescript/TypeScriptUtils.ts index 11ef719c5..58772cc41 100644 --- a/packages/core/typescript/TypeScriptUtils.ts +++ b/packages/core/typescript/TypeScriptUtils.ts @@ -83,10 +83,15 @@ export class TypeScriptUtils { * @param x Node to extract identifier text from. */ public static getIdentifierName(x: ts.Node): string { - if (x.kind === ts.SyntaxKind.CallExpression) { - const prop = ((x as ts.CallExpression).expression as ts.PropertyAccessExpression); - //pluck identifier from expression.name - x = prop.expression; + if (ts.isCallExpression(x)) { + const expression = x.expression; + if (ts.isPropertyAccessExpression(expression)) { + //pluck identifier from expression.name + x = expression.expression; + } + if (ts.isIdentifier(expression)) { + x = expression; + } } return (x as ts.Identifier).text; } diff --git a/packages/ng-schematics/src/cli-config/index.ts b/packages/ng-schematics/src/cli-config/index.ts index c42022474..3727acac8 100644 --- a/packages/ng-schematics/src/cli-config/index.ts +++ b/packages/ng-schematics/src/cli-config/index.ts @@ -60,12 +60,19 @@ function importBrowserAnimations(): Rule { return async (tree: Tree) => { const projects = await getProjects(tree); projects.forEach(project => { + // TODO: Resolve hardcoded paths instead const moduleFile = `${project.sourceRoot}/app/app.module.ts`; if (tree.exists(moduleFile)) { const mainModule = new TypeScriptFileUpdate(moduleFile); mainModule.addNgModuleMeta({ import: "BrowserAnimationsModule", from: "@angular/platform-browser/animations" }); mainModule.finalize(); } + const appConfigFile = `${project.sourceRoot}/app/app.config.ts`; + if (tree.exists(appConfigFile)) { + const appConfig = new TypeScriptFileUpdate(appConfigFile); + appConfig.addAppConfigProvider({ provide: "provideAnimations", from: "@angular/platform-browser/animations" }); + appConfig.finalize(); + } }); }; } diff --git a/packages/ng-schematics/src/cli-config/index_spec.ts b/packages/ng-schematics/src/cli-config/index_spec.ts index 0cd02de2f..5d13bb228 100644 --- a/packages/ng-schematics/src/cli-config/index_spec.ts +++ b/packages/ng-schematics/src/cli-config/index_spec.ts @@ -241,6 +241,54 @@ export class AppModule { expect(content.replace(/\r\n/g, "\n")).toEqual(moduleContentAfterSchematic.replace(/\r\n/g, "\n")); }); + it("should add provideAnimations to app.config.ts", async () => { + const moduleContent = +`import { ApplicationConfig } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [provideRouter(routes)] +}; +`; + + const moduleContentAfterSchematic = +`import { ApplicationConfig } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { routes } from './app.routes'; +import { provideAnimations } from "@angular/platform-browser/animations"; + +export const appConfig: ApplicationConfig = { + providers: [provideRouter(routes), provideAnimations()] +}; +`; + const targetFile = "./src/app/app.config.ts"; + tree.create(targetFile, moduleContent); + + await runner.runSchematicAsync("cli-config", {}, tree).toPromise(); + let content = tree.readContent(targetFile); + expect(content.replace(/\r\n/g, "\n")).toEqual(moduleContentAfterSchematic.replace(/\r\n/g, "\n")); + }); + + it("should NOT add provideAnimations to app.config.ts if it already exists", async () => { + const moduleContent = +`import { ApplicationConfig } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { routes } from './app.routes'; +import { provideAnimations } from "@angular/platform-browser/animations"; + +export const appConfig: ApplicationConfig = { + providers: [provideRouter(routes), provideAnimations()] +}; +`; + const targetFile = "./src/app/app.config.ts"; + tree.create(targetFile, moduleContent); + + await runner.runSchematicAsync("cli-config", {}, tree).toPromise(); + let content = tree.readContent(targetFile); + expect(content.replace(/\r\n/g, "\n")).toEqual(moduleContent.replace(/\r\n/g, "\n")); + }); + it("should properly display the dependency mismatch warning", async () => { const warns: string[] = []; runner.logger.subscribe(entry => {