DEV Community

Cover image for Modernizing Ionic 8 & Angular 22: Moving from Karma, Zone.js & ESLint v8 to Vitest, Zoneless & Flat Config
mehdi for The Modern Web

Posted on

Modernizing Ionic 8 & Angular 22: Moving from Karma, Zone.js & ESLint v8 to Vitest, Zoneless & Flat Config

[!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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Upgrade Angular 20 → Angular 21:
   npx ng update @angular/cli@21 @angular/core@21
Enter fullscreen mode Exit fullscreen mode

[!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 migrates angular.json from the legacy @angular-devkit/build-angular:browser builder to the modern, lightning-fast esbuild/Vite bundler.

  1. Upgrade Angular 21 → Angular 22:
   npx ng update @angular/cli@22 @angular/core@22
Enter fullscreen mode Exit fullscreen mode

[!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 specific inline server configurations in vitest.config.ts which 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/animations and @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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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']
      }
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

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"]
  }
}
Enter fullscreen mode Exit fullscreen mode

[!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 delete zone.js from package.json, third-party packages in node_modules may pull zone.js back into node_modules as an optional peer dependency during npm update.
When @angular/build detects node_modules/zone.js on disk, it attempts to dynamically inject await import('zone.js/testing'), causing esbuild top-level await errors!

The Solution: Explicitly set "polyfills": [] under "testing" configuration in angular.json:

"testing": {
  "aot": false,
  "optimization": false,
  "extractLicenses": false,
  "polyfills": []
}
Enter fullscreen mode Exit fullscreen mode

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: {}
  }
];
Enter fullscreen mode Exit fullscreen mode

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-dynamic is deprecated (Standalone AOT apps use bootstrapApplication from @angular/platform-browser).
  • @angular/animations is 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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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)