Skip to content
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
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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`.

Expand Down
137 changes: 137 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
38 changes: 38 additions & 0 deletions src/migrations/standalone/index.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
39 changes: 16 additions & 23 deletions src/migrations/standalone/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ interface StandaloneMigrationOptions {
spinner: ReturnType<typeof spinner>;
}

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,
Expand Down Expand Up @@ -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({
Expand All @@ -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;
}
Expand Down
30 changes: 0 additions & 30 deletions src/utils/ionic-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { SourceFile } from "ts-morph";

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