From aead2696d8983c4be13d2bc380d1d3b2856b24c9 Mon Sep 17 00:00:00 2001 From: rdlabo Date: Fri, 21 Aug 2026 20:18:30 +0900 Subject: [PATCH] feat: support Ionic Angular 9 --- README.md | 19 +++- docs/migration.md | 137 ++++++++++++++++++++++++ package-lock.json | 10 +- package.json | 2 +- src/index.ts | 4 +- src/migrations/standalone/index.test.ts | 38 +++++++ src/migrations/standalone/index.ts | 39 +++---- src/utils/ionic-utils.ts | 30 ------ 8 files changed, 217 insertions(+), 62 deletions(-) create mode 100644 docs/migration.md create mode 100644 src/migrations/standalone/index.test.ts diff --git a/README.md b/README.md index 471fa4d..1afa696 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,10 @@ This project is based [ionic-team/ionic-angular-standalone-codemods](https://git ## Requirements - Node.js >= 20 -- ionicons >= 6.0.0 +- Ionic Angular >= 9.0.0 +- Angular >= 18.0.0 +- TypeScript >= 5.4.0 +- ionicons >= 8.0.0 ## Quick start @@ -33,6 +36,19 @@ npx @rdlabo/ionic-angular-collect-icons --initialize true Details: [Initialize](./docs/initialize.md) and [Usage](./docs/usage.md). +## Migrating from Ionic Angular 8 + +Commit the consuming application's current changes, then run Ionic's official +migration tool from the application root: + +```bash +npx @ionic/migrate +``` + +It applies safe automatic changes and reports items that require manual review. +After it finishes, update this package and follow the +[Ionic Angular 9 migration guide](./docs/migration.md) for the remaining checks. + ## Installation ```bash @@ -45,6 +61,7 @@ Start with [Installation](#installation), then [Initialize](./docs/initialize.md - [Initialize](./docs/initialize.md) — wire `addIcons` automatically or by hand. - [Usage](./docs/usage.md) — run the collector before production builds. +- [Migration](./docs/migration.md) — migrate an existing project to Ionic Angular 9. - [CLI Options](./docs/options.md) — `--dry-run`, `--initialize`, paths. - [FAQ](./docs/faq.md) — tests, binding, and `main.ts`. diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000..3ee51b9 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,137 @@ +# Migration Guide + +## Migrating to Ionic Angular 9 + +This version targets Ionic Angular 9 and follows the +[Ionic Framework 9 breaking changes](https://github.com/ionic-team/ionic-framework/blob/main/BREAKING.md#version-9x). + +### Requirements + +- Ionic Angular 9 or later +- Angular 18 or later +- Capacitor 7 or later for native applications +- TypeScript 5.4 or later +- Ionicons 8 or later +- Node.js 20 or later + +### Run the official migrator + +Ionic recommends using its official migration tool. Commit the application's +current changes first: the migrator edits files in place and requires a clean +Git working tree so the commit can be used to review or undo its changes. + +Run it from the root of the Ionic application: + +```bash +npx @ionic/migrate +``` + +The migrator detects the installed Ionic major version, updates dependencies, +applies safe automatic fixes, formats changed files, reinstalls dependencies, +and prints a checklist of changes that require manual review. + +To preview the migration without writing files, run: + +```bash +npx @ionic/migrate --dry-run +``` + +After the official migration finishes, update this collector and confirm that +the resulting dependency versions meet the requirements above: + +```bash +npm install --save-dev @rdlabo/ionic-angular-collect-icons@latest +``` + +The remaining sections explain the important Ionic Angular 9 changes to verify +in the generated diff and in the migrator's manual-review checklist. + +### Update standalone imports + +Ionic 9 exports standalone Angular components from `@ionic/angular`. Replace +the Ionic 8 standalone entry point: + +```diff +- import { IonApp, IonIcon, provideIonicAngular } from '@ionic/angular/standalone'; ++ import { IonApp, IonIcon, provideIonicAngular } from '@ionic/angular'; +``` + +Lazy-loaded Ionic components now use `@ionic/angular/lazy` instead of the +package root: + +```diff +- import { IonModal } from '@ionic/angular'; ++ import { IonModal } from '@ionic/angular/lazy'; +``` + +Only use the lazy entry point when the application intentionally uses Ionic's +lazy-loaded component proxies. Regular standalone Angular components should be +imported from `@ionic/angular`. + +### Replace `IonicModule` + +`IonicModule` remains functional in Ionic 9 but is deprecated. Use +`provideIonicAngular()` for new and migrated applications. In an NgModule-based +application, move the Ionic configuration from `imports` to `providers`: + +```diff + @NgModule({ +- imports: [IonicModule.forRoot(config)], ++ providers: [provideIonicAngular(config)], + }) +``` + +### Use exports-aware module resolution + +Ionic 9 publishes package subpaths through `exports`. Applications should use +the Angular default bundler resolution: + +```json +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "bundler", + "target": "ES2022" + } +} +``` + +Replace webpack-style CSS imports that use `~`: + +```diff +- @import '~@ionic/angular/css/core.css'; ++ @import '@ionic/angular/css/core.css'; +``` + +### Run the icon collector + +Initialize the generated icon registration if the application has not already +done so: + +```bash +npx @rdlabo/ionic-angular-collect-icons --initialize true +``` + +Continue running the collector before production builds as described in the +[usage guide](./usage.md). + +### Review other Ionic 9 changes + +The collector finds `ion-icon` usage in Angular templates and updates its own +icon registration files. It does not depend on Ionic component behavior or +internal DOM, so those Ionic 9 changes do not require collector-specific code +changes. Consuming applications must still review the official migration notes, +particularly the new browser and mobile platform minimums and these changes: + +- Native applications require Capacitor 7+ and iOS 16+. +- Supported desktop browsers are Chrome 89+, Safari 16+, Edge 89+, and Firefox 75+. +- `ion-input` and `ion-searchbar` now use a boolean `autocorrect` property. +- Legacy picker components and `PickerController` were removed. +- Sheet modal handles now default to `handleBehavior="cycle"`. +- `ion-nav` no longer integrates with `ion-router`. +- `ion-select` emits `ionChange` only when its value changes. +- Input, select, and textarea internal DOM and styling hooks changed. +- Angular 21 applications use zoneless change detection by default. + +After migrating, run the application's lint, test, and production build commands +and verify any customized Ionic component styles visually. diff --git a/package-lock.json b/package-lock.json index 8cf6190..f907fe0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "np": "^10.2.0", "prettier": "^3.2.5", "tsup": "8.3.5", - "typescript": "^4.9.5", + "typescript": "^5.9.0", "vitest": "^0.34.6" }, "engines": { @@ -7642,16 +7642,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/ufo": { diff --git a/package.json b/package.json index d84ef90..56fac66 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "np": "^10.2.0", "prettier": "^3.2.5", "tsup": "8.3.5", - "typescript": "^4.9.5", + "typescript": "^5.9.0", "vitest": "^0.34.6" }, "engines": { diff --git a/src/index.ts b/src/index.ts index cf80d53..dee68da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -85,14 +85,14 @@ async function main() { project.addSourceFilesAtPaths([ `${cli.projectPath}/src/**/*.html`, `${cli.projectPath}/src/**/*.ts`, - `./angular.json`, + `${cli.projectPath}/angular.json`, ]); try { await runStandaloneMigration({ project, cliOptions: cli, - dir: cwd(), + dir: cli.projectPath, spinner: s, }); } catch (e: any) { diff --git a/src/migrations/standalone/index.test.ts b/src/migrations/standalone/index.test.ts new file mode 100644 index 0000000..82f337a --- /dev/null +++ b/src/migrations/standalone/index.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it, vi } from "vitest"; +import { checkInstalledIonicVersion, isSupportedIonicVersion } from "."; + +describe("isSupportedIonicVersion", () => { + it.each(["9.0.0", "9.1.0-dev.1", "10.0.0"])( + "accepts supported Ionic version %s", + (version) => { + expect(isSupportedIonicVersion(version)).toBe(true); + }, + ); + + it.each(["8.7.0", "7.5.0", "latest", "", "NaN.0.0"])( + "rejects unsupported or invalid Ionic version %s", + (version) => { + expect(isSupportedIonicVersion(version)).toBe(false); + }, + ); +}); + +describe("checkInstalledIonicVersion", () => { + it("checks the project directory and accepts Ionic 9", async () => { + const getVersion = vi.fn().mockResolvedValue("9.0.0"); + + await expect( + checkInstalledIonicVersion("/path/to/project", getVersion), + ).resolves.toBe(true); + expect(getVersion).toHaveBeenCalledWith( + "/path/to/project", + "@ionic/angular", + ); + }); + + it.each(["8.7.0", "invalid"])("rejects Ionic version %s", async (version) => { + await expect( + checkInstalledIonicVersion("/path/to/project", async () => version), + ).resolves.toBe(false); + }); +}); diff --git a/src/migrations/standalone/index.ts b/src/migrations/standalone/index.ts index 1cf3768..3376056 100644 --- a/src/migrations/standalone/index.ts +++ b/src/migrations/standalone/index.ts @@ -26,6 +26,11 @@ interface StandaloneMigrationOptions { spinner: ReturnType; } +export const isSupportedIonicVersion = (version: string): boolean => { + const match = /^(0|[1-9]\d*)\./.exec(version); + return match !== null && Number(match[1]) >= 9; +}; + export const runStandaloneMigration = async ({ project, cliOptions, @@ -57,25 +62,23 @@ export const runStandaloneMigration = async ({ }; /** - * Verifies that the installed version of @ionic/angular is at least 7.5.0. + * Verifies that the installed version of @ionic/angular is at least 9.0.0. * If the version cannot be detected, the user is prompted to continue. - * If the version is less than 7.5.0, the user is prompted to install the latest version. + * If the version is less than 9.0.0, the migration is canceled. * @param dir The directory of the project to be migrated. - * @returns True if the installed version of @ionic/angular is at least 7.5.0 or the user opted to continue, false otherwise. + * @returns True if the installed version of @ionic/angular is at least 9.0.0 or the user opted to continue, false otherwise. */ -async function checkInstalledIonicVersion(dir: string) { - const ionicAngularVersion = await getActualPackageVersion( - dir, - "@ionic/angular", - ); +export async function checkInstalledIonicVersion( + dir: string, + getVersion = getActualPackageVersion, +) { + const ionicAngularVersion = await getVersion(dir, "@ionic/angular"); if (!ionicAngularVersion) { log.warn( "We could not detect the version of @ionic/angular installed in your project.", ); - log.warn( - "This migration requires @ionic/angular version of 7.5.0 or later.", - ); + log.warn("This migration requires @ionic/angular version 9.0.0 or later."); log.warn("Do you want to proceed anyway?"); const { continue: shouldContinue } = await group({ @@ -91,25 +94,15 @@ async function checkInstalledIonicVersion(dir: string) { return false; } } else { - const [major, minor] = ionicAngularVersion.split("."); - const majorVersion = parseInt(major); - const minorVersion = parseInt(minor); - const logVersionError = () => { log.error( - "This migration requires an @ionic/angular version of v7.5.0 or greater.", + "This migration requires @ionic/angular version 9.0.0 or later.", ); log.error("Install the latest version of @ionic/angular and try again."); log.error("Migration canceled."); }; - if (majorVersion < 7) { - logVersionError(); - return false; - } - - // only need to add if is major v7 then compare with minor 5. - if (majorVersion == 7 && minorVersion < 5) { + if (!isSupportedIonicVersion(ionicAngularVersion)) { logVersionError(); return false; } diff --git a/src/utils/ionic-utils.ts b/src/utils/ionic-utils.ts index 1f3e403..71f9ac4 100644 --- a/src/utils/ionic-utils.ts +++ b/src/utils/ionic-utils.ts @@ -1,5 +1,3 @@ -import type { SourceFile } from "ts-morph"; - /** * List of Ionic components by tag name. */ @@ -93,31 +91,3 @@ 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"); -};