Skip to content

fix: migrate angular app config #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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");
}
Original file line number Diff line number Diff line change
@@ -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("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()]
};
`),
);
});
});
Original file line number Diff line number Diff line change
@@ -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);
}
};
3 changes: 3 additions & 0 deletions packages/cli/src/angular/migrations/standalone/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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.`);

Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/angular/utils/ionic-utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { SourceFile } from "ts-morph";

/**
* List of Ionic components by tag name.
*/
Expand Down Expand Up @@ -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");
};