From 6b7bc0fd253adf850324d872c7d11ba27c193cd7 Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Fri, 3 Nov 2023 14:02:31 -0400 Subject: [PATCH 1/2] feat: migrate angular app config --- .../0003-migrate-bootstrap-application.ts | 31 +--- .../0006-migrate-angular-app-config.test.ts | 44 ++++++ .../0006-migrate-angular-app-config.ts | 137 ++++++++++++++++++ .../angular/migrations/standalone/index.ts | 3 + packages/cli/src/angular/utils/ionic-utils.ts | 30 ++++ 5 files changed, 217 insertions(+), 28 deletions(-) create mode 100644 packages/cli/src/angular/migrations/standalone/0006-migrate-angular-app-config.test.ts create mode 100644 packages/cli/src/angular/migrations/standalone/0006-migrate-angular-app-config.ts diff --git a/packages/cli/src/angular/migrations/standalone/0003-migrate-bootstrap-application.ts b/packages/cli/src/angular/migrations/standalone/0003-migrate-bootstrap-application.ts index ee5cbdc..21c5ec3 100644 --- a/packages/cli/src/angular/migrations/standalone/0003-migrate-bootstrap-application.ts +++ b/packages/cli/src/angular/migrations/standalone/0003-migrate-bootstrap-application.ts @@ -1,7 +1,8 @@ -import type { ObjectLiteralExpression, Project, SourceFile } from "ts-morph"; +import type { ObjectLiteralExpression, Project } from "ts-morph"; import { SyntaxKind } from "ts-morph"; import type { CliOptions } from "../../../types/cli-options"; import { saveFileChanges } from "../../utils/log-utils"; +import { migrateProvideIonicAngularImportDeclarations } from "../../utils/ionic-utils"; export const migrateBootstrapApplication = async ( project: Project, @@ -136,35 +137,9 @@ export const migrateBootstrapApplication = async ( providersArray.formatText(); - migrateIonicAngularImportDeclarations(sourceFile); + migrateProvideIonicAngularImportDeclarations(sourceFile); return await saveFileChanges(sourceFile, cliOptions); } } }; - -function migrateIonicAngularImportDeclarations(sourceFile: SourceFile) { - const importDeclaration = sourceFile.getImportDeclaration("@ionic/angular"); - - if (!importDeclaration) { - // If the @ionic/angular import does not exist, then this is not an @ionic/angular application. - // This migration only applies to @ionic/angular applications. - return; - } - - // Update the import statement to import from @ionic/angular/standalone - importDeclaration.setModuleSpecifier("@ionic/angular/standalone"); - - const namedImports = importDeclaration.getNamedImports(); - const importSpecifier = namedImports.find( - (n) => n.getName() === "IonicModule", - ); - - if (importSpecifier) { - // Remove the IonicModule import specifier - importSpecifier.remove(); - } - - // Add the provideIonicAngular import specifier - importDeclaration.addNamedImport("provideIonicAngular"); -} diff --git a/packages/cli/src/angular/migrations/standalone/0006-migrate-angular-app-config.test.ts b/packages/cli/src/angular/migrations/standalone/0006-migrate-angular-app-config.test.ts new file mode 100644 index 0000000..55cf8ed --- /dev/null +++ b/packages/cli/src/angular/migrations/standalone/0006-migrate-angular-app-config.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from "vitest"; +import { Project } from "ts-morph"; +import { dedent } from "ts-dedent"; + +import { migrateAngularAppConfig } from "./0006-migrate-angular-app-config"; + +describe.only("migrateAngularAppConfig", () => { + it("should migrate app.config.ts", async () => { + const project = new Project({ useInMemoryFileSystem: true }); + + const appConfig = dedent(` + import { ApplicationConfig, importProvidersFrom } from '@angular/core'; + import { provideRouter } from '@angular/router'; + import { IonicModule } from '@ionic/angular'; + + import { routes } from './app.routes'; + + export const appConfig: ApplicationConfig = { + providers: [provideRouter(routes), importProvidersFrom(IonicModule.forRoot())] + }; + `); + + const configSourceFile = project.createSourceFile( + "src/app/app.config.ts", + appConfig, + ); + + await migrateAngularAppConfig(project, { dryRun: false }); + + expect(dedent(configSourceFile.getText())).toBe( + dedent(` + import { ApplicationConfig, importProvidersFrom } from '@angular/core'; + import { provideRouter } from '@angular/router'; + import { provideIonicAngular } from '@ionic/angular/standalone'; + + import { routes } from './app.routes'; + + export const appConfig: ApplicationConfig = { + providers: [provideRouter(routes), provideIonicAngular()] + }; + `), + ); + }); +}); diff --git a/packages/cli/src/angular/migrations/standalone/0006-migrate-angular-app-config.ts b/packages/cli/src/angular/migrations/standalone/0006-migrate-angular-app-config.ts new file mode 100644 index 0000000..81e744d --- /dev/null +++ b/packages/cli/src/angular/migrations/standalone/0006-migrate-angular-app-config.ts @@ -0,0 +1,137 @@ +import type { ObjectLiteralExpression, Project, SourceFile } from "ts-morph"; +import { SyntaxKind } from "ts-morph"; +import type { CliOptions } from "../../../types/cli-options"; +import { saveFileChanges } from "../../utils/log-utils"; +import { migrateProvideIonicAngularImportDeclarations } from "../../utils/ionic-utils"; + +export const migrateAngularAppConfig = async ( + project: Project, + cliOptions: CliOptions, +) => { + const sourceFile = project + .getSourceFiles() + .find((sourceFile) => sourceFile.getFilePath().endsWith("app.config.ts")); + + if (sourceFile === undefined) { + return; + } + + const appConfigVariableStatement = sourceFile + .getVariableStatements() + .find((variableStatement) => { + const declarationList = variableStatement.getDeclarationList(); + const variableDeclaration = declarationList.getDeclarations()[0]; + return variableDeclaration.getName() === "appConfig"; + }); + + if (appConfigVariableStatement === undefined) { + return; + } + + const appConfigVariableStatementDeclarationList = + appConfigVariableStatement.getDeclarationList(); + const appConfigVariableDeclaration = + appConfigVariableStatementDeclarationList.getDeclarations()[0]; + const appConfigInitializer = appConfigVariableDeclaration.getInitializer(); + if (appConfigInitializer === undefined) { + return; + } + + const appConfigObjectLiteralExpression = + appConfigInitializer as ObjectLiteralExpression; + const providersPropertyAssignment = + appConfigObjectLiteralExpression.getProperty("providers"); + if (providersPropertyAssignment === undefined) { + return; + } + + const providersArray = providersPropertyAssignment.getFirstChildByKind( + SyntaxKind.ArrayLiteralExpression, + ); + + if (providersArray === undefined) { + return; + } + + const importProvidersFromFunctionCall = providersArray + .getChildrenOfKind(SyntaxKind.CallExpression) + .find((callExpression) => { + const identifier = callExpression.getFirstChildByKind( + SyntaxKind.Identifier, + ); + return ( + identifier !== undefined && + identifier.getText() === "importProvidersFrom" + ); + }); + + if (importProvidersFromFunctionCall === undefined) { + return; + } + + const importProvidersFromFunctionCallIdentifier = + importProvidersFromFunctionCall.getFirstChildByKind(SyntaxKind.Identifier); + + if (importProvidersFromFunctionCallIdentifier === undefined) { + return; + } + + if ( + importProvidersFromFunctionCallIdentifier.getText() !== + "importProvidersFrom" + ) { + return; + } + + const importProvidersFromFunctionCallArguments = + importProvidersFromFunctionCall.getArguments(); + + if (importProvidersFromFunctionCallArguments.length === 0) { + return; + } + + const importProvidersFromFunctionCallIonicModuleForRootCallExpression = + importProvidersFromFunctionCallArguments.find((argument) => { + return argument.getText().includes("IonicModule.forRoot"); + }); + + if ( + importProvidersFromFunctionCallIonicModuleForRootCallExpression === + undefined + ) { + return; + } + + if ( + importProvidersFromFunctionCallIonicModuleForRootCallExpression.isKind( + SyntaxKind.CallExpression, + ) + ) { + const importProvidersFromFunctionCallIonicModuleForRootCallExpressionArguments = + importProvidersFromFunctionCallIonicModuleForRootCallExpression.getArguments(); + const ionicConfigObjectLiteralExpression = + importProvidersFromFunctionCallIonicModuleForRootCallExpressionArguments[0]; + + const ionicConfigValue = ionicConfigObjectLiteralExpression + ? ionicConfigObjectLiteralExpression.getText() + : ""; + + providersArray.addElement(`provideIonicAngular(${ionicConfigValue})`); + + // Remove the IonicModule.forRoot from the importProvidersFrom function call. + importProvidersFromFunctionCall.removeArgument( + importProvidersFromFunctionCallIonicModuleForRootCallExpression, + ); + + if (importProvidersFromFunctionCall.getArguments().length === 0) { + // If there are no remaining arguments, remove the importProvidersFrom function call. + providersArray.removeElement(importProvidersFromFunctionCall); + } + + providersArray.formatText(); + + migrateProvideIonicAngularImportDeclarations(sourceFile); + + return await saveFileChanges(sourceFile, cliOptions); + } +}; diff --git a/packages/cli/src/angular/migrations/standalone/index.ts b/packages/cli/src/angular/migrations/standalone/index.ts index 2364c57..c8be58f 100644 --- a/packages/cli/src/angular/migrations/standalone/index.ts +++ b/packages/cli/src/angular/migrations/standalone/index.ts @@ -8,6 +8,7 @@ import { migrateAngularJsonAssets } from "./0005-migrate-angular-json-assets"; import { group, confirm, log, spinner } from "@clack/prompts"; import { getActualPackageVersion } from "../../utils/package-utils"; +import { migrateAngularAppConfig } from "./0006-migrate-angular-app-config"; interface StandaloneMigrationOptions { /** @@ -51,6 +52,8 @@ export const runStandaloneMigration = async ({ await migrateImportStatements(project, cliOptions); // Migrate the assets array in angular.json await migrateAngularJsonAssets(project, cliOptions); + // Migrate angular projects with an app config + await migrateAngularAppConfig(project, cliOptions); spinner.stop(`Project migration at ${dir} completed successfully.`); diff --git a/packages/cli/src/angular/utils/ionic-utils.ts b/packages/cli/src/angular/utils/ionic-utils.ts index 71f9ac4..1f3e403 100644 --- a/packages/cli/src/angular/utils/ionic-utils.ts +++ b/packages/cli/src/angular/utils/ionic-utils.ts @@ -1,3 +1,5 @@ +import type { SourceFile } from "ts-morph"; + /** * List of Ionic components by tag name. */ @@ -91,3 +93,31 @@ export const IONIC_COMPONENTS = [ "ion-toggle", "ion-title", ]; // TODO can we generate this from @ionic/core and import it here? + +export const migrateProvideIonicAngularImportDeclarations = ( + sourceFile: SourceFile, +) => { + const importDeclaration = sourceFile.getImportDeclaration("@ionic/angular"); + + if (!importDeclaration) { + // If the @ionic/angular import does not exist, then this is not an @ionic/angular application. + // This migration only applies to @ionic/angular applications. + return; + } + + // Update the import statement to import from @ionic/angular/standalone + importDeclaration.setModuleSpecifier("@ionic/angular/standalone"); + + const namedImports = importDeclaration.getNamedImports(); + const importSpecifier = namedImports.find( + (n) => n.getName() === "IonicModule", + ); + + if (importSpecifier) { + // Remove the IonicModule import specifier + importSpecifier.remove(); + } + + // Add the provideIonicAngular import specifier + importDeclaration.addNamedImport("provideIonicAngular"); +}; From 985c0a44e137b585af81faa0e858cd2381fba5cd Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Fri, 3 Nov 2023 14:06:22 -0400 Subject: [PATCH 2/2] chore: remove .only from test --- .../standalone/0006-migrate-angular-app-config.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/angular/migrations/standalone/0006-migrate-angular-app-config.test.ts b/packages/cli/src/angular/migrations/standalone/0006-migrate-angular-app-config.test.ts index 55cf8ed..e7f324a 100644 --- a/packages/cli/src/angular/migrations/standalone/0006-migrate-angular-app-config.test.ts +++ b/packages/cli/src/angular/migrations/standalone/0006-migrate-angular-app-config.test.ts @@ -4,7 +4,7 @@ import { dedent } from "ts-dedent"; import { migrateAngularAppConfig } from "./0006-migrate-angular-app-config"; -describe.only("migrateAngularAppConfig", () => { +describe("migrateAngularAppConfig", () => { it("should migrate app.config.ts", async () => { const project = new Project({ useInMemoryFileSystem: true });