[!NOTE]
Target Audience: Ionic & Angular developers looking to modernize starter boilerplate, boost unit test speed by 5x, adopt pure Zoneless change detection, and upgrade to ESLint v9 Flat Config.[!TIP]
Looking Ahead to Ionic 9: The Ionic team is actively developing Ionic 9, which is expected to support Angular 21 and Angular 22 natively out of the box. While this guide provides immediate step-by-step instructions for modernizing applications today on Ionic 8, core architectural migrations—such as moving from Karma to Vitest, configuring ESLint v9 Flat Config, and enforcing Zoneless test setups—will remain essential practices regardless of the Ionic version.
Introduction: The CLI Boilerplate Gap
When generating a mobile application using the Ionic CLI:
ionic start my-app tabs --type=angular --standalone
The CLI scaffolds an Angular Standalone application with Ionic 8 components. However, newly generated starters launch with Angular 20 boilerplate.
Sequential Framework Upgrade (Angular 20 → 21 → 22)
Before applying modern architectural patterns, we must upgrade the framework. In Angular, major version upgrades should always be executed sequentially using Angular CLI's automated code refactoring schematics:
- Upgrade Angular 20 → Angular 21:
npx ng update @angular/cli@21 @angular/core@21
[!IMPORTANT]
Application Builder Prompt: During the Angular 21 upgrade, the CLI schematic will ask:
"Would you like to migrate to the new Application Builder (@angular/build:application)?"
Select YES. This migratesangular.jsonfrom the legacy@angular-devkit/build-angular:browserbuilder to the modern, lightning-fast esbuild/Vite bundler.
- Upgrade Angular 21 → Angular 22:
npx ng update @angular/cli@22 @angular/core@22
[!NOTE]
Karma to Vitest Schematic Note: During the Angular 22 upgrade, the CLI will ask:
"Would you like to migrate unit tests from Karma to Vitest?"
It is completely normal if the schematic leaves manual setup or if initial test runs fail. Ionic web components require specificinlineserver configurations invitest.config.tswhich automated CLI schematics cannot anticipate. The manual adjustments are detailed in Phase 1.
The Remaining Boilerplate Gap
Even after upgrading to Angular 22, the generated template starter retains legacy testing and configuration defaults:
- Karma & Jasmine: Browser-launcher test runners that take 10+ seconds to spin up headless Chrome.
- Zone.js: Legacy monkey-patching runtime for Angular change detection.
-
Legacy ESLint (
.eslintrc.json): ESLint v8 nested configuration format. -
Deprecated Dependencies: Unused legacy packages like
@angular/animationsand@angular/platform-browser-dynamic.
In this article, we will walk step-by-step through modernizing the upgraded Ionic 8 / Angular 22 application to modern web standards.
Phase 1: Replacing Karma & Jasmine with Vitest & Modern tsconfig.json
Angular 22 introduces native support for Vitest via the @angular/build:unit-test builder.
1. Update angular.json
Update the "test" target in angular.json to use the Vitest builder:
"test": {
"builder": "@angular/build:unit-test",
"options": {
"tsConfig": "tsconfig.spec.json",
"runner": "vitest",
"runnerConfig": "vitest.config.ts"
}
}
2. Configure vitest.config.ts
Create a minimal vitest.config.ts in your project root to handle Ionic web components:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
server: {
deps: {
inline: ['@ionic/angular', '@ionic/core']
}
}
}
});
3. Modernize tsconfig.json (baseUrl & Vitest Types)
In modern TypeScript with "moduleResolution": "bundler", the legacy "baseUrl": "./" property is no longer needed and is deprecated by TypeScript.
Remove "baseUrl": "./" and add "types": ["vitest/globals"] to enable global IDE autocompletion for describe, it, expect, and beforeEach:
{
"compilerOptions": {
"moduleResolution": "bundler",
"types": ["vitest/globals"]
}
}
[!TIP]
Performance Impact: Test execution time drops from ~15 seconds (Karma + Chrome) to ~1.08 seconds (Vitest in-memory).
Phase 2: Going 100% Zoneless (Signal Architecture)
Angular 22 natively supports Zoneless change detection powered by Signals.
1. Remove Zone Imports from polyfills.ts
Clean src/polyfills.ts by removing import 'zone.js';.
2. Delete Legacy Zone Flags
Delete src/zone-flags.ts and remove any commented-out Zone error imports in src/environments/environment.ts.
3. Fix the "Transitive Zone" Gotcha in angular.json
[!IMPORTANT]
The Hidden Gotcha: Even if you deletezone.jsfrompackage.json, third-party packages innode_modulesmay pullzone.jsback intonode_modulesas an optional peer dependency duringnpm update.
When@angular/builddetectsnode_modules/zone.json disk, it attempts to dynamically injectawait import('zone.js/testing'), causingesbuildtop-level await errors!
The Solution: Explicitly set "polyfills": [] under "testing" configuration in angular.json:
"testing": {
"aot": false,
"optimization": false,
"extractLicenses": false,
"polyfills": []
}
Setting "polyfills": [] explicitly tells Angular CLI that your testing target requires zero Zone polyfills, allowing tests to run in pure Zoneless mode 100% cleanly regardless of node_modules.
Phase 3: ESLint v8 (.eslintrc.json) → ESLint v9 (eslint.config.js)
ESLint v9 deprecated .eslintrc.json in favor of Flat Config (eslint.config.js).
1. Create eslint.config.js
Replace .eslintrc.json with eslint.config.js:
// @ts-check
const tsParser = require("@typescript-eslint/parser");
const tseslint = require("@typescript-eslint/eslint-plugin");
const angularEslint = require("@angular-eslint/eslint-plugin");
const angularTemplateParser = require("@angular-eslint/template-parser");
const angularTemplatePlugin = require("@angular-eslint/eslint-plugin-template");
module.exports = [
{
ignores: ["projects/**/*"]
},
{
files: ["**/*.ts"],
languageOptions: {
parser: tsParser,
parserOptions: {
project: ["./tsconfig.json"],
createDefaultProgram: true
}
},
plugins: {
"@typescript-eslint": tseslint,
"@angular-eslint": angularEslint,
"@angular-eslint/template": angularTemplatePlugin
},
rules: {
"@angular-eslint/component-class-suffix": [
"error",
{ suffixes: ["Page", "Component"] }
],
"@angular-eslint/component-selector": [
"error",
{ type: "element", prefix: "app", style: "kebab-case" }
],
"@angular-eslint/directive-selector": [
"error",
{ type: "attribute", prefix: "app", style: "camelCase" }
]
}
},
{
files: ["**/*.html"],
languageOptions: {
parser: angularTemplateParser
},
plugins: {
"@angular-eslint/template": angularTemplatePlugin
},
rules: {}
}
];
2. Remove .eslintrc.json
Delete .eslintrc.json and verify with npm run lint.
Phase 4: Pruning Deprecated Packages & Adding Carets
1. Remove Obsolete Angular Dependencies
In Angular 22:
-
@angular/platform-browser-dynamicis deprecated (Standalone AOT apps usebootstrapApplicationfrom@angular/platform-browser). -
@angular/animationsis deprecated (Ionic 8 & Angular 22 use native CSS View Transitions & Web Animations API).
Remove both packages if unused in your src/ codebase:
npm uninstall @angular/platform-browser-dynamic @angular/animations
2. Standardize Carets (^) in package.json
Ensure your dependencies use standard caret (^) SemVer ranges to automatically pick up non-breaking minor and patch updates:
"dependencies": {
"@angular/common": "^22.0.0",
"@angular/compiler": "^22.0.0",
"@angular/core": "^22.0.0",
"@angular/forms": "^22.0.0",
"@angular/platform-browser": "^22.0.0",
"@angular/router": "^22.0.0",
"@capacitor/app": "^8.0.0",
"@capacitor/core": "^8.0.0",
"@ionic/angular": "^8.0.0",
"ionicons": "^8.0.0"
}
Benchmark Results
| Metric | Before Modernization | After Modernization | Improvement |
|---|---|---|---|
| Unit Test Suite Time | ~15.2 seconds (Karma/Chrome) | 1.08 seconds (Vitest) | ~14x Faster ⚡️ |
| Test Runner Overhead | Headless Chrome Browser Launch | In-memory JSDOM Execution | Clean & Lightweight |
| Change Detection Mode | Zone.js Monkey-patching | Pure Zoneless Signals | Zero Zone Overhead |
| ESLint Format | Legacy .eslintrc.json
|
Modern eslint.config.js Flat Config |
Fully ESLint v9 Compliant |
| Unused Dependencies | 2 deprecated packages | 0 deprecated packages | Clean Lockfile |
Conclusion
By upgrading sequentially through Angular CLI schematics, modernizing test runners with Vitest, adopting Zoneless Signals, converting to ESLint Flat Config, and pruning legacy dependencies, your Ionic 8 & Angular 22 app becomes significantly faster, cleaner, and aligned with modern web standards.
Top comments (0)