diff --git a/angular.json b/angular.json index 32e8c46b..71c81340 100644 --- a/angular.json +++ b/angular.json @@ -24,8 +24,7 @@ "index": "src/index.html", "browser": "src/main.ts", "polyfills": [ - "@angular/localize/init", - "zone.js" + "@angular/localize/init" ], "tsConfig": "tsconfig.app.json", "allowedCommonJsDependencies": [ @@ -94,7 +93,6 @@ "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", - "polyfills": ["zone.js", "zone.js/testing"], "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "assets": [ diff --git a/docs/migration-doc.md b/docs/migration-doc.md index 5f774de0..53b59265 100644 --- a/docs/migration-doc.md +++ b/docs/migration-doc.md @@ -686,6 +686,19 @@ Each component toggle below documents a single commit. +
+ +Zoneless application and tests + +- The application now uses `provideZonelessChangeDetection()` instead of `provideZoneChangeDetection()`. +- Removed `zone.js` from the application dependencies and from the build/test polyfills, including the `zone.js/testing` test bootstrap import. +- Replaced the remaining Zone-dependent `fakeAsync` / `tick` test setup with native `async` / `await fixture.whenStable()`. +- Migrated the remaining loading and rendered-content state in `ActivityDescriptionPageComponent`, `ReportComponent`, `UsageComponent`, and `MarkdownViewerComponent` to signals so their templates update correctly without Zone.js. +- DSOMM is now completely zoneless at runtime and in its test configuration 🎉. +- **Files:** `angular.json`, `package.json`, `package-lock.json`, `src/main.ts`, `src/test.ts`, `src/app/pages/settings/settings.component.spec.ts`, `src/app/component/markdown-viewer/*`, `src/app/pages/activity-description/*`, `src/app/pages/report/*`, `src/app/pages/usage/*` + +
+ --- ## Backlog @@ -698,4 +711,4 @@ Each component toggle below documents a single commit. | 4 | Test Runner | • **Deprecated subdependencies:** Karma pulls in `glob@7.2.3`, `inflight@1.0.6`, and `rimraf@3.0.2` — all deprecated, cluttering `pnpm install` with warnings.
• **pnpm incompatibility:** Karma's Webpack-based builder (`@angular-devkit/build-angular:karma`) cannot resolve transitive dependencies (e.g. `@babel/runtime`) under pnpm's strict symlinked `node_modules`. Currently requires `node-linker=hoisted` in `.npmrc` as a workaround, defeating pnpm's strictness benefits.
• **Modern tooling alignment:** Build/serve already use esbuild/Vite via `@angular/build`. Tests are the last piece still on the legacy Webpack pipeline. Migrating would allow dropping `@angular-devkit/build-angular` entirely. | High | Karma is deprecated. Migrate to a Vite-based test runner (e.g. Vitest or `@angular/build` native test support). | > [!NOTE] -> Add new backlog items here as they are discovered during future upgrades. Remove items once resolved. \ No newline at end of file +> Add new backlog items here as they are discovered during future upgrades. Remove items once resolved. diff --git a/package-lock.json b/package-lock.json index c8f55681..f3436bff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,8 +25,7 @@ "tslib": "2.8.1", "xlsx": "0.18.5", "yaml": "2.9.0", - "yamljs": "0.3.0", - "zone.js": "0.15.1" + "yamljs": "0.3.0" }, "devDependencies": { "@angular-devkit/build-angular": "21.2.19", @@ -4676,9 +4675,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4696,9 +4692,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4716,9 +4709,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4736,9 +4726,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4756,9 +4743,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4776,9 +4760,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4796,9 +4777,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5286,9 +5264,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5310,9 +5285,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5334,9 +5306,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5358,9 +5327,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5382,9 +5348,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5406,9 +5369,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5747,9 +5707,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5767,9 +5724,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5787,9 +5741,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5807,9 +5758,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5986,9 +5934,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6003,9 +5948,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6020,9 +5962,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6037,9 +5976,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6054,9 +5990,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6071,9 +6004,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6088,9 +6018,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6105,9 +6032,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6122,9 +6046,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6139,9 +6060,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6156,9 +6074,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6173,9 +6088,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6190,9 +6102,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -17491,7 +17400,9 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true } } } diff --git a/package.json b/package.json index 0227918e..e64eab0b 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,7 @@ "tslib": "2.8.1", "xlsx": "0.18.5", "yaml": "2.9.0", - "yamljs": "0.3.0", - "zone.js": "0.15.1" + "yamljs": "0.3.0" }, "devDependencies": { "@angular-devkit/build-angular": "21.2.19", diff --git a/src/app/component/markdown-viewer/markdown-viewer.component.html b/src/app/component/markdown-viewer/markdown-viewer.component.html index 2aaa1992..8d242d92 100644 --- a/src/app/component/markdown-viewer/markdown-viewer.component.html +++ b/src/app/component/markdown-viewer/markdown-viewer.component.html @@ -1,3 +1,3 @@
-
+
diff --git a/src/app/component/markdown-viewer/markdown-viewer.component.ts b/src/app/component/markdown-viewer/markdown-viewer.component.ts index caaa3189..459a03af 100644 --- a/src/app/component/markdown-viewer/markdown-viewer.component.ts +++ b/src/app/component/markdown-viewer/markdown-viewer.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit, inject } from '@angular/core'; +import { Component, Input, OnInit, inject, signal } from '@angular/core'; import md from 'markdown-it'; import { HttpClient } from '@angular/common/http'; @@ -16,7 +16,7 @@ export class MarkdownViewerComponent implements OnInit { html: true, }); markdownURI: any; - toRender: string = ''; + readonly toRender = signal(''); ngOnInit(): void { this.loadMarkdownFiles(this.MDFile); @@ -25,10 +25,10 @@ export class MarkdownViewerComponent implements OnInit { async loadMarkdownFiles(MDFile: string): Promise { try { this.markdownURI = await this.http.get(MDFile, { responseType: 'text' }).toPromise(); - this.toRender = this.markdown.render(this.markdownURI); + this.toRender.set(this.markdown.render(this.markdownURI)); return true; } catch { - this.toRender = 'Markdown file could not be found'; + this.toRender.set('Markdown file could not be found'); return false; } } diff --git a/src/app/pages/activity-description/activity-description-page.component.html b/src/app/pages/activity-description/activity-description-page.component.html index 4a1a5cd2..8838aa57 100644 --- a/src/app/pages/activity-description/activity-description-page.component.html +++ b/src/app/pages/activity-description/activity-description-page.component.html @@ -1,11 +1,11 @@
- @if (isLoading) { + @if (isLoading()) {
} - @if (currentActivity && !isLoading) { + @if (currentActivity && !isLoading()) { { @@ -34,7 +34,7 @@ export class ActivityDescriptionPageComponent implements OnInit { } loadActivity(uuid?: string, name?: string) { - this.isLoading = true; + this.isLoading.set(true); this.loader .load() @@ -51,11 +51,11 @@ export class ActivityDescriptionPageComponent implements OnInit { } this.currentActivity = activity; - this.isLoading = false; + this.isLoading.set(false); }) .catch(err => { console.error('Error loading activity data:', err); - this.isLoading = false; + this.isLoading.set(false); this.displayMessage( new DialogInfo(err.message || 'Failed to load activity', 'An error occurred') ); diff --git a/src/app/pages/report/report.component.html b/src/app/pages/report/report.component.html index 8442193b..5cc130e9 100644 --- a/src/app/pages/report/report.component.html +++ b/src/app/pages/report/report.component.html @@ -10,7 +10,7 @@ print Print - @if (!isLoading) { + @if (!isLoading()) { {{ totalFilteredActivities() }} activities @if (reportConfig().selectedTeams.length > 0) { @@ -29,13 +29,13 @@
- @if (isLoading) { + @if (isLoading()) {
} - @if (!isLoading && filteredDimensions().length === 0) { + @if (!isLoading() && filteredDimensions().length === 0) {
filter_list_off

No activities match the current report configuration.

@@ -43,7 +43,7 @@
} - @if (!isLoading && filteredDimensions().length > 0) { + @if (!isLoading() && filteredDimensions().length > 0) {
diff --git a/src/app/pages/report/report.component.ts b/src/app/pages/report/report.component.ts index 4846386a..3af3d2e3 100644 --- a/src/app/pages/report/report.component.ts +++ b/src/app/pages/report/report.component.ts @@ -72,7 +72,7 @@ export class ReportComponent implements OnInit { reportConfig = signal(getReportConfig()); allActivities = signal([]); - isLoading: boolean = true; + readonly isLoading = signal(true); // For the config modal allDimensionNames: string[] = []; @@ -191,12 +191,12 @@ export class ReportComponent implements OnInit { } loadActivities(): void { - this.isLoading = true; + this.isLoading.set(true); this.loader .load() .then((dataStore: DataStore) => { if (!dataStore.activityStore) { - this.isLoading = false; + this.isLoading.set(false); return; } @@ -231,11 +231,11 @@ export class ReportComponent implements OnInit { } this.allActivities.set(activities); - this.isLoading = false; + this.isLoading.set(false); }) .catch(err => { console.error('Error loading activities for report:', err); - this.isLoading = false; + this.isLoading.set(false); }); } diff --git a/src/app/pages/settings/settings.component.spec.ts b/src/app/pages/settings/settings.component.spec.ts index 2694cc1d..948edd00 100644 --- a/src/app/pages/settings/settings.component.spec.ts +++ b/src/app/pages/settings/settings.component.spec.ts @@ -1,5 +1,5 @@ import { HttpClient, HttpHandler } from '@angular/common/http'; -import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatSelectModule } from '@angular/material/select'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -73,7 +73,7 @@ describe('SettingsComponent', () => { }).compileComponents(); }); - beforeEach(fakeAsync(() => { + beforeEach(async () => { fixture = TestBed.createComponent(SettingsComponent); component = fixture.componentInstance; component.meta.set({ @@ -84,27 +84,27 @@ describe('SettingsComponent', () => { } as any); fixture.detectChanges(); - tick(); + await fixture.whenStable(); fixture.detectChanges(); - })); + }); - it('should create', fakeAsync(() => { + it('should create', () => { expect(component).toBeTruthy(); - })); + }); - it('should update max level settings correctly', fakeAsync(() => { + it('should update max level settings correctly', () => { component.onMaxLevelChange(3); expect(component.selectedMaxLevel()).toBe(3); expect(settingsService.setMaxLevel).toHaveBeenCalledWith(3); - })); + }); - it('should handle max level reset to default', fakeAsync(() => { + it('should handle max level reset to default', () => { component.onMaxLevelChange(5); expect(component.selectedMaxLevel()).toBe(5); // Remove localStorage when settings' maxLevel is set to activity's maxLevel expect(settingsService.setMaxLevel).toHaveBeenCalledWith(null); - })); + }); }); diff --git a/src/app/pages/usage/usage.component.html b/src/app/pages/usage/usage.component.html index 939fd577..587d0058 100644 --- a/src/app/pages/usage/usage.component.html +++ b/src/app/pages/usage/usage.component.html @@ -1,3 +1,3 @@ - + diff --git a/src/app/pages/usage/usage.component.spec.ts b/src/app/pages/usage/usage.component.spec.ts index c487ef50..61880a5e 100644 --- a/src/app/pages/usage/usage.component.spec.ts +++ b/src/app/pages/usage/usage.component.spec.ts @@ -27,7 +27,7 @@ describe('UsageComponent', () => { fixture.detectChanges(); expect(component).toBeTruthy(); - expect(component.page).toBe('USAGE'); + expect(component.page()).toBe('USAGE'); }); it('should load page', () => { @@ -39,6 +39,6 @@ describe('UsageComponent', () => { component = fixture.componentInstance; fixture.detectChanges(); - expect(component.page).toBe('test-page'); + expect(component.page()).toBe('test-page'); }); }); diff --git a/src/app/pages/usage/usage.component.ts b/src/app/pages/usage/usage.component.ts index bb6bc4d1..fd132e90 100644 --- a/src/app/pages/usage/usage.component.ts +++ b/src/app/pages/usage/usage.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, inject } from '@angular/core'; +import { Component, OnInit, inject, signal } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { perfNow } from 'src/app/util/util'; import { MarkdownViewerComponent } from '../../component/markdown-viewer/markdown-viewer.component'; @@ -13,7 +13,7 @@ import { TopHeaderComponent } from '../../component/top-header/top-header.compon export class UsageComponent implements OnInit { private route = inject(ActivatedRoute); - page: string = 'USAGE'; + readonly page = signal('USAGE'); ngOnInit() { if (this.route && this.route.params) { @@ -21,7 +21,7 @@ export class UsageComponent implements OnInit { let page = params['page']; // CWE-79 - sanitize input if (page && page.match(/^[\w.-]+$/)) { - this.page = page; + this.page.set(page); } }); } diff --git a/src/main.ts b/src/main.ts index 0f20fb80..b475a9a6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,7 +3,7 @@ document.body.classList.remove('light-theme', 'dark-theme'); document.body.classList.add(`${savedTheme}-theme`); console.log('[main.ts] Theme set to:', savedTheme); // -import { enableProdMode, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; +import { enableProdMode, importProvidersFrom, provideZonelessChangeDetection } from '@angular/core'; import { environment } from './environments/environment'; import { AppComponent } from './app/app.component'; @@ -27,7 +27,7 @@ if (environment.production) { bootstrapApplication(AppComponent, { providers: [ - provideZoneChangeDetection(), + provideZonelessChangeDetection(), importProvidersFrom( BrowserModule, AppRoutingModule, diff --git a/src/test.ts b/src/test.ts index 864b2285..571499ce 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,6 +1,5 @@ // This file is required by karma.conf.js and loads recursively all the .spec and framework files -import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';