From 5dbad2383f294da9c1fb5e77aa346c3d823f85fa Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Sun, 27 Sep 2026 08:48:01 +0200 Subject: [PATCH] perf(web): keep channel lists, EPG views and the Stalker layer off the initial path (#1712) The root shell imported WindowControlsComponent and DialogService through the @iptvnator/ui/components barrel, and esbuild keeps every Angular component module a barrel re-exports, so channel lists, EPG views, @angular/forms, date-fns and the whole Stalker data layer sat in main.js. The shell now uses file-level entries, the Stalker connection editor is a lazy proxy, and the release-notes and external-player info dialogs load on demand with a handled failure path. renderer.initialBytes 2,714,336 -> 1,626,019 bytes (-40%); the baseline is lowered to the ubuntu ratchet measurement and the production/PWA initial budgets drop to 1.8/2 MB. J1: did-finish-load about -16 ms, first card within noise. Co-Authored-By: Claude Opus 5.5 --- .changes/web-initial-path-audit.md | 8 ++ apps/web/project.json | 8 +- ...pdate-notification-panel.component.spec.ts | 28 +++++- ...app-update-notification-panel.component.ts | 28 +++++- apps/web/src/app/app.component.ts | 4 +- apps/web/src/app/app.config.ts | 6 +- apps/web/src/app/services/electron.service.ts | 2 +- .../external-player-info-dialog.lazy.ts | 9 ++ ...stalker-playlist-connection-editor.spec.ts | 89 +++++++++++++++++++ ...lazy-stalker-playlist-connection-editor.ts | 56 ++++++++++++ .../src/app/services/player.service.spec.ts | 56 +++++++++++- apps/web/src/app/services/player.service.ts | 31 +++++-- ...lker-playlist-connection-editor.service.ts | 2 +- docs/architecture/nx-workspace-boundaries.md | 20 +++++ tools/performance/journey-baselines.json | 6 +- tsconfig.base.json | 9 ++ 16 files changed, 339 insertions(+), 23 deletions(-) create mode 100644 .changes/web-initial-path-audit.md create mode 100644 apps/web/src/app/services/external-player-info-dialog.lazy.ts create mode 100644 apps/web/src/app/services/lazy-stalker-playlist-connection-editor.spec.ts create mode 100644 apps/web/src/app/services/lazy-stalker-playlist-connection-editor.ts diff --git a/.changes/web-initial-path-audit.md b/.changes/web-initial-path-audit.md new file mode 100644 index 000000000..f1017a28c --- /dev/null +++ b/.changes/web-initial-path-audit.md @@ -0,0 +1,8 @@ +--- +type: perf +area: web +--- + +The app now loads about 40% less code before it shows its first screen: +channel lists, program guide views, Stalker portal tools and a few dialogs +are loaded when they are first needed instead of at startup. diff --git a/apps/web/project.json b/apps/web/project.json index 302ea2e37..72b30b14e 100644 --- a/apps/web/project.json +++ b/apps/web/project.json @@ -57,8 +57,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "4.5mb", - "maximumError": "5mb" + "maximumWarning": "1.8mb", + "maximumError": "2mb" }, { "type": "anyComponentStyle", @@ -88,8 +88,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "4.5mb", - "maximumError": "5mb" + "maximumWarning": "1.8mb", + "maximumError": "2mb" }, { "type": "anyComponentStyle", diff --git a/apps/web/src/app/app-update-notification-panel.component.spec.ts b/apps/web/src/app/app-update-notification-panel.component.spec.ts index f567a2573..d92cc47d9 100644 --- a/apps/web/src/app/app-update-notification-panel.component.spec.ts +++ b/apps/web/src/app/app-update-notification-panel.component.spec.ts @@ -73,7 +73,7 @@ describe('AppUpdateNotificationPanelComponent', () => { ).not.toBeNull(); }); - it('opens release notes without dismissing the notification', () => { + it('opens release notes without dismissing the notification', async () => { statusHandler?.(availableStatus); fixture.detectChanges(); @@ -82,6 +82,9 @@ describe('AppUpdateNotificationPanelComponent', () => { '[data-test-id="app-update-notification-release-notes"]' ) as HTMLButtonElement ).click(); + // The dialog component is imported on demand (it pulls in `marked`). + await fixture.whenStable(); + await new Promise((resolve) => setTimeout(resolve)); expect(TestBed.inject(MatDialog).open).toHaveBeenCalledWith( AppUpdateReleaseNotesDialogComponent, @@ -96,6 +99,29 @@ describe('AppUpdateNotificationPanelComponent', () => { ).not.toBeNull(); }); + it('falls back to the releases page when the release notes dialog fails to load', async () => { + const open = jest.spyOn(window, 'open').mockReturnValue(null); + const error = jest + .spyOn(console, 'error') + .mockImplementation(() => undefined); + fixture.componentInstance.loadReleaseNotesDialog = () => + Promise.reject(new Error('chunk failed')); + statusHandler?.(availableStatus); + fixture.detectChanges(); + + await fixture.componentInstance.openReleaseNotes(); + + expect(TestBed.inject(MatDialog).open).not.toHaveBeenCalled(); + expect(open).toHaveBeenCalledWith( + availableStatus.manualDownloadUrl, + '_blank', + 'noreferrer' + ); + expect(error).toHaveBeenCalledTimes(1); + open.mockRestore(); + error.mockRestore(); + }); + it('starts downloading the update from the notification action', async () => { statusHandler?.(availableStatus); fixture.detectChanges(); diff --git a/apps/web/src/app/app-update-notification-panel.component.ts b/apps/web/src/app/app-update-notification-panel.component.ts index d10b02117..a99e814ad 100644 --- a/apps/web/src/app/app-update-notification-panel.component.ts +++ b/apps/web/src/app/app-update-notification-panel.component.ts @@ -16,7 +16,6 @@ import { ElectronBridgeAppUpdateStatus, } from '@iptvnator/shared/interfaces'; import { AppUpdateInstallService } from './services/app-update-install.service'; -import { AppUpdateReleaseNotesDialogComponent } from './settings/app-update-release-notes-dialog.component'; @Component({ selector: 'app-update-notification-panel', @@ -239,8 +238,31 @@ export class AppUpdateNotificationPanelComponent implements OnInit, OnDestroy { this.unsubscribeStatus = null; } - openReleaseNotes(): void { - const latestVersion = this.status()?.latestVersion; + /** + * Loaded on demand: the dialog renders Markdown with `marked`, which this + * always-mounted panel would otherwise put on the initial path. A field so + * specs can substitute it. + */ + loadReleaseNotesDialog = () => + import('./settings/app-update-release-notes-dialog.component'); + + async openReleaseNotes(): Promise { + const status = this.status(); + const latestVersion = status?.latestVersion; + let dialogModule: Awaited< + ReturnType + >; + try { + dialogModule = await this.loadReleaseNotesDialog(); + } catch (error) { + // The same notes are on the releases page; the next click retries. + console.error('Could not load the release notes dialog:', error); + if (status?.manualDownloadUrl) { + window.open(status.manualDownloadUrl, '_blank', 'noreferrer'); + } + return; + } + const { AppUpdateReleaseNotesDialogComponent } = dialogModule; this.dialog.open(AppUpdateReleaseNotesDialogComponent, { autoFocus: false, diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 3e4dc7ad0..079fc8e9f 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -21,7 +21,9 @@ import { WORKSPACE_SHELL_ACTIONS, } from '@iptvnator/workspace/shell/util'; import { EpgProgressPanelComponent } from '@iptvnator/ui/epg/progress-panel'; -import { WindowControlsComponent } from '@iptvnator/ui/components'; +// File-level entry: the @iptvnator/ui/components barrel would put the whole +// library (channel lists, EPG, forms, date-fns) on the initial path. +import { WindowControlsComponent } from '@iptvnator/ui/components/window-controls'; import { PlaylistActions, selectAllPlaylistsMeta } from '@iptvnator/m3u-state'; import { filter, take } from 'rxjs'; import { diff --git a/apps/web/src/app/app.config.ts b/apps/web/src/app/app.config.ts index 0f08a66bf..1a4a956fe 100644 --- a/apps/web/src/app/app.config.ts +++ b/apps/web/src/app/app.config.ts @@ -31,7 +31,7 @@ import { PORTAL_EXTERNAL_PLAYBACK, PORTAL_PLAYER, } from '@iptvnator/portal/shared/util'; -import { STALKER_PLAYLIST_CONNECTION_EDITOR } from '@iptvnator/playlist/shared/ui'; +import { STALKER_PLAYLIST_CONNECTION_EDITOR } from '@iptvnator/playlist/shared/ui/stalker-connection-editor'; import { provideXtreamDataSource } from '@iptvnator/portal/xtream/data-access'; import { DataService } from '@iptvnator/services'; import { dbConfig } from '@iptvnator/shared/interfaces'; @@ -43,7 +43,7 @@ import { PlayerService } from './services/player.service'; import { providePortalPlaybackPositions } from './services/portal-playback-positions.service'; import { PwaService } from './services/pwa.service'; import { shouldEnableServiceWorker } from './services/runtime-config'; -import { AppStalkerPlaylistConnectionEditorService } from './services/stalker-playlist-connection-editor.service'; +import { LazyStalkerPlaylistConnectionEditor } from './services/lazy-stalker-playlist-connection-editor'; import { provideWorkspaceShellActions } from './services/workspace-shell-actions.service'; // AoT requires an exported function for factories @@ -157,7 +157,7 @@ export const appConfig: ApplicationConfig = { ...providePortalPlaybackPositions(), { provide: STALKER_PLAYLIST_CONNECTION_EDITOR, - useExisting: AppStalkerPlaylistConnectionEditorService, + useExisting: LazyStalkerPlaylistConnectionEditor, }, ...provideWorkspaceShellActions(), ...provideXtreamDataSource(), diff --git a/apps/web/src/app/services/electron.service.ts b/apps/web/src/app/services/electron.service.ts index 525ff5645..dd5be4f0b 100644 --- a/apps/web/src/app/services/electron.service.ts +++ b/apps/web/src/app/services/electron.service.ts @@ -3,7 +3,7 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { PlaylistActions } from '@iptvnator/m3u-state'; -import { DialogService } from '@iptvnator/ui/components'; +import { DialogService } from '@iptvnator/ui/components/confirm-dialog'; import { DataService, SettingsStore, diff --git a/apps/web/src/app/services/external-player-info-dialog.lazy.ts b/apps/web/src/app/services/external-player-info-dialog.lazy.ts new file mode 100644 index 000000000..b5158073e --- /dev/null +++ b/apps/web/src/app/services/external-player-info-dialog.lazy.ts @@ -0,0 +1,9 @@ +/** + * Lazy boundary for PlayerService: the service imports this file dynamically, + * so the dialog and the @angular/forms it brings (via the Material checkbox) + * stay off the initial path. The boundary is a local file rather than the + * library path because other web files, such as specs, import ui-playback + * statically, which @nx/enforce-module-boundaries forbids for a library the + * project also loads dynamically. + */ +export { ExternalPlayerInfoDialogComponent } from '@iptvnator/ui/playback/external-player-info-dialog'; diff --git a/apps/web/src/app/services/lazy-stalker-playlist-connection-editor.spec.ts b/apps/web/src/app/services/lazy-stalker-playlist-connection-editor.spec.ts new file mode 100644 index 000000000..4a35313b4 --- /dev/null +++ b/apps/web/src/app/services/lazy-stalker-playlist-connection-editor.spec.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import type { + StalkerPlaylistConnectionEditor, + StalkerPlaylistConnectionResult, +} from '@iptvnator/playlist/shared/ui/stalker-connection-editor'; +import type { + PlaylistMeta, + PlaylistMetaUpdate, +} from '@iptvnator/shared/interfaces'; +import { LazyStalkerPlaylistConnectionEditor } from './lazy-stalker-playlist-connection-editor'; + +const resolved: StalkerPlaylistConnectionResult = { + status: 'resolved', + playlist: { _id: 'p1' } as PlaylistMetaUpdate, +}; + +@Injectable({ providedIn: 'root' }) +class FakeEditor implements StalkerPlaylistConnectionEditor { + resolveConnection = jest.fn(async () => resolved); + applyResolvedConnection = jest.fn( + async (playlist: PlaylistMetaUpdate) => playlist + ); +} + +type EditorModule = Awaited< + ReturnType +>; + +function moduleWith(editorClass: typeof FakeEditor): EditorModule { + return { + AppStalkerPlaylistConnectionEditorService: editorClass, + } as unknown as EditorModule; +} + +describe('LazyStalkerPlaylistConnectionEditor', () => { + let lazy: LazyStalkerPlaylistConnectionEditor; + + beforeEach(() => { + TestBed.configureTestingModule({}); + lazy = TestBed.inject(LazyStalkerPlaylistConnectionEditor); + }); + + it('loads nothing until an editor method is called', () => { + const load = jest.fn(async () => moduleWith(FakeEditor)); + lazy.loadEditorModule = load; + + expect(load).not.toHaveBeenCalled(); + }); + + it('delegates both methods to the root-provided implementation, loading it once', async () => { + const load = jest.fn(async () => moduleWith(FakeEditor)); + lazy.loadEditorModule = load; + const playlist = { _id: 'p1' } as PlaylistMeta; + const source = { _id: 'p0' } as PlaylistMeta; + const update = { _id: 'p1', title: 'Renamed' } as PlaylistMetaUpdate; + + await expect(lazy.resolveConnection(playlist, source)).resolves.toBe( + resolved + ); + await expect( + lazy.applyResolvedConnection(update, { + preserveCurrentMetadata: true, + }) + ).resolves.toBe(update); + + const editor = TestBed.inject(FakeEditor); + expect(editor.resolveConnection).toHaveBeenCalledWith(playlist, source); + expect(editor.applyResolvedConnection).toHaveBeenCalledWith(update, { + preserveCurrentMetadata: true, + }); + expect(load).toHaveBeenCalledTimes(1); + }); + + it('retries the import after a failed chunk load', async () => { + const load = jest + .fn, []>() + .mockRejectedValueOnce(new Error('chunk failed')) + .mockResolvedValueOnce(moduleWith(FakeEditor)); + lazy.loadEditorModule = load; + const playlist = { _id: 'p1' } as PlaylistMeta; + + await expect(lazy.resolveConnection(playlist)).rejects.toThrow( + 'chunk failed' + ); + await expect(lazy.resolveConnection(playlist)).resolves.toBe(resolved); + expect(load).toHaveBeenCalledTimes(2); + }); +}); diff --git a/apps/web/src/app/services/lazy-stalker-playlist-connection-editor.ts b/apps/web/src/app/services/lazy-stalker-playlist-connection-editor.ts new file mode 100644 index 000000000..187a59e43 --- /dev/null +++ b/apps/web/src/app/services/lazy-stalker-playlist-connection-editor.ts @@ -0,0 +1,56 @@ +import { EnvironmentInjector, inject, Injectable } from '@angular/core'; +import type { + StalkerPlaylistConnectionEditor, + StalkerPlaylistConnectionResult, + StalkerResolvedConnectionApplyOptions, +} from '@iptvnator/playlist/shared/ui/stalker-connection-editor'; +import type { + PlaylistMeta, + PlaylistMetaUpdate, +} from '@iptvnator/shared/interfaces'; + +type EditorModule = + typeof import('./stalker-playlist-connection-editor.service'); + +/** + * Registered as STALKER_PLAYLIST_CONNECTION_EDITOR in app.config. The real + * editor depends on the whole Stalker portal data layer, which would + * otherwise be evaluated before the first paint although it is only needed + * when a user edits or re-checks a Stalker source. Every editor method is + * asynchronous, so the implementation is imported on first use and resolved + * from the root injector (it is providedIn: 'root'). + */ +@Injectable({ providedIn: 'root' }) +export class LazyStalkerPlaylistConnectionEditor implements StalkerPlaylistConnectionEditor { + private readonly injector = inject(EnvironmentInjector); + private editor: Promise | null = null; + + /** The dynamic import; a field so specs can substitute it. */ + loadEditorModule: () => Promise = () => + import('./stalker-playlist-connection-editor.service'); + + async resolveConnection( + playlist: PlaylistMeta, + sourcePlaylist?: PlaylistMeta + ): Promise { + const editor = await this.resolveEditor(); + return editor.resolveConnection(playlist, sourcePlaylist); + } + + async applyResolvedConnection( + playlist: PlaylistMetaUpdate, + options?: StalkerResolvedConnectionApplyOptions + ): Promise { + const editor = await this.resolveEditor(); + return editor.applyResolvedConnection(playlist, options); + } + + private resolveEditor(): Promise { + this.editor ??= this.loadEditorModule().then((module) => + this.injector.get(module.AppStalkerPlaylistConnectionEditorService) + ); + // A failed chunk load must not poison later attempts. + this.editor.catch(() => (this.editor = null)); + return this.editor; + } +} diff --git a/apps/web/src/app/services/player.service.spec.ts b/apps/web/src/app/services/player.service.spec.ts index 86ab12ca8..665fd25eb 100644 --- a/apps/web/src/app/services/player.service.spec.ts +++ b/apps/web/src/app/services/player.service.spec.ts @@ -1,7 +1,10 @@ import { TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { DataService } from '@iptvnator/services'; -import { ExternalPlayerSession, VideoPlayer } from '@iptvnator/shared/interfaces'; +import { + ExternalPlayerSession, + VideoPlayer, +} from '@iptvnator/shared/interfaces'; import { SettingsStore } from './settings-store.service'; import { PlayerService } from './player.service'; @@ -92,6 +95,57 @@ describe('PlayerService', () => { expect(result).toEqual(session); }); + it('opens the external-player info dialog once its chunk has loaded', async () => { + settingsStore.player.mockReturnValue(VideoPlayer.MPV); + dataService.sendIpcEvent.mockResolvedValue(undefined); + + await service.openResolvedPlayback( + { streamUrl: 'https://example.com/video.mp4', title: 'Example' }, + false + ); + await new Promise((resolve) => setTimeout(resolve)); + + expect(dialog.open).toHaveBeenCalledTimes(1); + expect(dataService.sendIpcEvent).toHaveBeenCalledWith( + 'OPEN_MPV_PLAYER', + expect.anything() + ); + }); + + it('still starts playback and logs when the info dialog fails to load', async () => { + const error = jest + .spyOn(console, 'error') + .mockImplementation(() => undefined); + const unhandled = jest.fn(); + process.on('unhandledRejection', unhandled); + try { + settingsStore.player.mockReturnValue(VideoPlayer.VLC); + dataService.sendIpcEvent.mockResolvedValue(undefined); + service.loadExternalPlayerInfoDialog = () => + Promise.reject(new Error('chunk failed')); + + await service.openResolvedPlayback( + { + streamUrl: 'https://example.com/video.mp4', + title: 'Example', + }, + false + ); + await new Promise((resolve) => setTimeout(resolve)); + + expect(dialog.open).not.toHaveBeenCalled(); + expect(dataService.sendIpcEvent).toHaveBeenCalledWith( + 'OPEN_VLC_PLAYER', + expect.anything() + ); + expect(error).toHaveBeenCalledTimes(1); + expect(unhandled).not.toHaveBeenCalled(); + } finally { + process.off('unhandledRejection', unhandled); + error.mockRestore(); + } + }); + it('forces external playback without changing the selected embedded player', async () => { const session: ExternalPlayerSession = { id: 'session-2', diff --git a/apps/web/src/app/services/player.service.ts b/apps/web/src/app/services/player.service.ts index a37e32b66..fa686c46e 100644 --- a/apps/web/src/app/services/player.service.ts +++ b/apps/web/src/app/services/player.service.ts @@ -1,6 +1,5 @@ import { Injectable, inject } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { ExternalPlayerInfoDialogComponent } from '@iptvnator/ui/playback/external-player-info-dialog'; import { DataService } from '@iptvnator/services'; import { ExternalPlayerSession, @@ -70,12 +69,12 @@ export class PlayerService { if (player === VideoPlayer.MPV) { if (!hideExternalInfoDialog) { - this.dialog.open(ExternalPlayerInfoDialogComponent); + this.showExternalPlayerInfoDialog(); } return await this.openExternalPlayback(playback, 'mpv'); } else if (player === VideoPlayer.VLC) { if (!hideExternalInfoDialog) { - this.dialog.open(ExternalPlayerInfoDialogComponent); + this.showExternalPlayerInfoDialog(); } return await this.openExternalPlayback(playback, 'vlc'); } @@ -83,12 +82,34 @@ export class PlayerService { return; } + /** + * Loaded on demand: the dialog's Material checkbox brings @angular/forms, + * and this service is created with the app, so a static import would put + * both on the initial path. Playback does not wait for the dialog. + */ + private showExternalPlayerInfoDialog(): void { + this.loadExternalPlayerInfoDialog() + .then(({ ExternalPlayerInfoDialogComponent }) => + this.dialog.open(ExternalPlayerInfoDialogComponent) + ) + .catch((error: unknown) => { + // Informational only: playback is already starting. + console.error( + 'Could not load the external player info dialog:', + error + ); + }); + } + + /** The dynamic import; a field so specs can substitute it. */ + loadExternalPlayerInfoDialog = () => + import('./external-player-info-dialog.lazy'); + async openExternalPlayback( playback: ResolvedPortalPlayback, player: ExternalPlayerName ): Promise { - const ipcEvent = - player === 'mpv' ? OPEN_MPV_PLAYER : OPEN_VLC_PLAYER; + const ipcEvent = player === 'mpv' ? OPEN_MPV_PLAYER : OPEN_VLC_PLAYER; return await this.dataService.sendIpcEvent( ipcEvent, diff --git a/apps/web/src/app/services/stalker-playlist-connection-editor.service.ts b/apps/web/src/app/services/stalker-playlist-connection-editor.service.ts index ed50c4526..0f74424cd 100644 --- a/apps/web/src/app/services/stalker-playlist-connection-editor.service.ts +++ b/apps/web/src/app/services/stalker-playlist-connection-editor.service.ts @@ -15,7 +15,7 @@ import { type StalkerPlaylistConnectionEditor, type StalkerPlaylistConnectionResult, type StalkerResolvedConnectionApplyOptions, -} from '@iptvnator/playlist/shared/ui'; +} from '@iptvnator/playlist/shared/ui/stalker-connection-editor'; import { normalizeStalkerPortalIdentity, type Playlist, diff --git a/docs/architecture/nx-workspace-boundaries.md b/docs/architecture/nx-workspace-boundaries.md index 9ea588c11..13bc2fe5a 100644 --- a/docs/architecture/nx-workspace-boundaries.md +++ b/docs/architecture/nx-workspace-boundaries.md @@ -236,6 +236,26 @@ and value-imports those two dependency-free modules directly. reason: the main `workspace-shell-util` barrel is imported eagerly, and the settings search index must stay in the lazy settings and shell chunks. +The web app's root shell (`app.component.ts`, `app.config.ts` and the services +they construct) is on the renderer's initial path, where a barrel costs its +whole library: esbuild keeps every Angular component module a barrel +re-exports, because their static definitions count as side effects. Importing +`WindowControlsComponent` and `DialogService` from `@iptvnator/ui/components` +once put the channel lists, EPG views, `@angular/forms`, `date-fns` and the +Stalker data layer into `main.js`. The root shell therefore uses file-level +entries: `@iptvnator/ui/components/window-controls`, +`@iptvnator/ui/components/confirm-dialog` and +`@iptvnator/playlist/shared/ui/stalker-connection-editor` (like the existing +`@iptvnator/ui/epg/progress-panel`), and loads anything used only on demand +through a local file it imports dynamically (the Stalker connection editor, +the release-notes dialog, the external-player info dialog). A local file, +not the library alias, is the dynamic-import target because +`@nx/enforce-module-boundaries` forbids static imports of a library the same +project also loads dynamically. `renderer.initialBytes` in +[performance journeys](performance-journeys.md) guards the result; to see why a +module is eager, build with `pnpm nx build web --stats-json` and follow the +static imports in `dist/apps/web/stats.json` from `apps/web/src/main.ts`. + For a buildable library that has a local `package.json`, its `name` must match the scoped alias. Nx uses that package name when rewriting buildable dependency paths to `dist/` during `@nx/js:tsc` builds. diff --git a/tools/performance/journey-baselines.json b/tools/performance/journey-baselines.json index 9e15f1e69..1e690f08f 100644 --- a/tools/performance/journey-baselines.json +++ b/tools/performance/journey-baselines.json @@ -3,10 +3,10 @@ "journeys": { "launch": { "renderer.initialBytes": { - "value": 2715087, + "value": 1626019, "unit": "bytes", - "updatedAt": "2026-09-26", - "evidencePr": 1695, + "updatedAt": "2026-09-27", + "evidencePr": 1712, "measuredWith": "pnpm nx build web && pnpm run perf:initial-bytes" } } diff --git a/tsconfig.base.json b/tsconfig.base.json index 988ce188f..828f042d5 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -37,6 +37,9 @@ "@iptvnator/playlist/shared/ui": [ "libs/playlist/shared/ui/src/index.ts" ], + "@iptvnator/playlist/shared/ui/stalker-connection-editor": [ + "libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/stalker-playlist-connection-editor.token.ts" + ], "@iptvnator/playlist/shared/util": [ "libs/playlist/shared/util/src/index.ts" ], @@ -81,6 +84,12 @@ ], "@iptvnator/pipes": ["libs/ui/pipes/src/index.ts"], "@iptvnator/ui/components": ["libs/ui/components/src/index.ts"], + "@iptvnator/ui/components/confirm-dialog": [ + "libs/ui/components/src/lib/confirm-dialog/dialog.service.ts" + ], + "@iptvnator/ui/components/window-controls": [ + "libs/ui/components/src/lib/window-controls/window-controls.component.ts" + ], "@iptvnator/m3u-state": ["libs/m3u-state/src/index.ts"], "@iptvnator/shared/m3u-utils": [ "libs/shared/m3u-utils/src/index.ts"