diff --git a/apps/electron-backend-e2e/src/performance/performance-build-config.spec.ts b/apps/electron-backend-e2e/src/performance/performance-build-config.spec.ts index 0ff7caa34..91a8701f4 100644 --- a/apps/electron-backend-e2e/src/performance/performance-build-config.spec.ts +++ b/apps/electron-backend-e2e/src/performance/performance-build-config.spec.ts @@ -10,6 +10,7 @@ import { JOURNEY_CD_TICK_COUNTER_KEY } from './journey-renderer-probe'; interface TargetConfiguration { configurations?: Record>; + defaultConfiguration?: string; dependsOn?: unknown; executor?: unknown; options?: Record; @@ -162,7 +163,12 @@ test('only the web performance build installs the tick counter the journeys read for (const [name, configuration] of Object.entries( webProject.targets['build'].configurations ?? {} )) { - if (name === 'electron-performance') continue; + if ( + name === 'electron-performance' || + name === 'electron-performance-zoneless' + ) { + continue; + } assert.doesNotMatch( JSON.stringify(configuration['fileReplacements'] ?? []), /environment\.performance/, @@ -171,6 +177,44 @@ test('only the web performance build installs the tick counter the journeys read } }); +// Plan item C6 measures zoneless change detection behind a build-time flag: +// each *-zoneless configuration is its base configuration plus one swap of +// the change-detection providers, and nothing else selects that swap. +test('the zoneless flag is opt-in through the *-zoneless web configurations only', () => { + const configurations = webProject.targets['build'].configurations ?? {}; + const zonelessReplacement = { + replace: 'apps/web/src/environments/change-detection.providers.ts', + with: 'apps/web/src/environments/change-detection.providers.zoneless.ts', + }; + + for (const base of ['electron-performance', 'electron-e2e']) { + const baseConfiguration = configurations[base]; + const zoneless = configurations[`${base}-zoneless`]; + assert.ok(zoneless, `web:build must define ${base}-zoneless`); + assert.ok(baseConfiguration, `web:build must define ${base}`); + const { fileReplacements: baseReplacements, ...baseRest } = + baseConfiguration; + const { fileReplacements, ...rest } = zoneless; + assert.deepEqual(rest, baseRest, base); + assert.deepEqual(fileReplacements, [ + ...((baseReplacements as unknown[] | undefined) ?? []), + zonelessReplacement, + ]); + } + for (const [name, configuration] of Object.entries(configurations)) { + if (name.endsWith('-zoneless')) continue; + assert.doesNotMatch( + JSON.stringify(configuration['fileReplacements'] ?? []), + /change-detection\.providers/, + name + ); + } + assert.equal( + webProject.targets['build'].defaultConfiguration, + 'production' + ); +}); + test('the resolved web build cache output is the renderer directory', () => { const task = readResolvedWebBuildTask(); diff --git a/apps/web/project.json b/apps/web/project.json index 7244c4ac5..74c415311 100644 --- a/apps/web/project.json +++ b/apps/web/project.json @@ -143,12 +143,50 @@ } ] }, + "electron-performance-zoneless": { + "baseHref": "./", + "serviceWorker": false, + "optimization": { + "scripts": true, + "styles": { + "minify": true, + "inlineCritical": false, + "removeSpecialComments": true + }, + "fonts": true + }, + "outputHashing": "all", + "sourceMap": true, + "fileReplacements": [ + { + "replace": "apps/web/src/environments/environment.ts", + "with": "apps/web/src/environments/environment.performance.ts" + }, + { + "replace": "apps/web/src/environments/change-detection.providers.ts", + "with": "apps/web/src/environments/change-detection.providers.zoneless.ts" + } + ] + }, "electron-e2e": { "baseHref": "./", "serviceWorker": false, "optimization": false, "extractLicenses": false, "sourceMap": true + }, + "electron-e2e-zoneless": { + "baseHref": "./", + "serviceWorker": false, + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "apps/web/src/environments/change-detection.providers.ts", + "with": "apps/web/src/environments/change-detection.providers.zoneless.ts" + } + ] } }, "defaultConfiguration": "production" diff --git a/apps/web/src/app/app.config.ts b/apps/web/src/app/app.config.ts index 1fc0d8950..edd657996 100644 --- a/apps/web/src/app/app.config.ts +++ b/apps/web/src/app/app.config.ts @@ -8,12 +8,7 @@ import { FullscreenOverlayContainer, OverlayContainer, } from '@angular/cdk/overlay'; -import { - ApplicationConfig, - inject, - importProvidersFrom, - provideZoneChangeDetection, -} from '@angular/core'; +import { ApplicationConfig, inject, importProvidersFrom } from '@angular/core'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; import { provideAnimations } from '@angular/platform-browser/animations'; import { provideRouter, withComponentInputBinding } from '@angular/router'; @@ -39,6 +34,7 @@ import { } from '@iptvnator/services'; import { dbConfig } from '@iptvnator/shared/interfaces'; import { AppConfig } from '../environments/environment'; +import { changeDetectionProviders } from '../environments/change-detection.providers'; import { routes } from './app.routes'; import { ElectronService } from './services/electron.service'; import { ExternalPlaybackService } from './services/external-playback.service'; @@ -110,7 +106,7 @@ export function DataFactory() { export const appConfig: ApplicationConfig = { providers: [ - provideZoneChangeDetection({ eventCoalescing: true }), + ...changeDetectionProviders, provideRouter(routes, withComponentInputBinding()), provideAnimations(), // CDK overlays (menus, tooltips, dialogs) live in a container under diff --git a/apps/web/src/environments/change-detection.providers.ts b/apps/web/src/environments/change-detection.providers.ts new file mode 100644 index 000000000..d0cf2caa0 --- /dev/null +++ b/apps/web/src/environments/change-detection.providers.ts @@ -0,0 +1,12 @@ +import { + EnvironmentProviders, + provideZoneChangeDetection, +} from '@angular/core'; + +// Change detection for every build: zone.js schedules the ticks. The +// *-zoneless build configurations replace this file with +// change-detection.providers.zoneless.ts while plan item C6 measures +// zoneless change detection; see docs/architecture/zoneless-migration.md. +export const changeDetectionProviders: EnvironmentProviders[] = [ + provideZoneChangeDetection({ eventCoalescing: true }), +]; diff --git a/apps/web/src/environments/change-detection.providers.zoneless.ts b/apps/web/src/environments/change-detection.providers.zoneless.ts new file mode 100644 index 000000000..30e71ff06 --- /dev/null +++ b/apps/web/src/environments/change-detection.providers.zoneless.ts @@ -0,0 +1,11 @@ +import { + EnvironmentProviders, + provideZonelessChangeDetection, +} from '@angular/core'; + +// Swapped in for change-detection.providers.ts by the *-zoneless build +// configurations only. zone.js stays in the polyfills until the flip, so +// Angular logs NG0914 in these builds; nothing patches through it. +export const changeDetectionProviders: EnvironmentProviders[] = [ + provideZonelessChangeDetection(), +];