feat(web): enable runtime PWA configuration

This commit is contained in:
4gray committed 2026-05-15 11:20:35 +02:00
1 parent f0af34edef
commit eafc95bef0
9 files changed
+110 -37

No files matched your search

+5 -2
View File
@@ -22,6 +22,7 @@
"polyfills": ["zone.js"],
"tsConfig": "apps/web/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"serviceWorker": "ngsw-config.json",
"assets": [
"apps/web/src/favicon.ico",
"apps/web/src/assets",
@@ -99,12 +100,14 @@
]
},
"development": {
"serviceWorker": false,
"optimization": false,
"extractLicenses": false,
"sourceMap": true
},
"electron-e2e": {
"baseHref": "./",
"serviceWorker": false,
"optimization": false,
"extractLicenses": false,
"sourceMap": true
@@ -166,9 +169,9 @@
"continuous": true,
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "web:build",
"buildTarget": "web:build:pwa",
"port": 4200,
"staticFilePath": "dist/apps/web/browser",
"staticFilePath": "dist/apps/web",
"spa": true
}
}
+2 -1
View File
@@ -40,6 +40,7 @@ import {
} from './services/portal-navigation-actions.service';
import { providePortalPlaybackPositions } from './services/portal-playback-positions.service';
import { PwaService } from './services/pwa.service';
import { shouldEnableServiceWorker } from './services/runtime-config';
import { provideWorkspaceShellActions } from './services/workspace-shell-actions.service';
// AoT requires an exported function for factories
@@ -113,7 +114,7 @@ export const appConfig: ApplicationConfig = {
provideRouterStore(),
...(AppConfig.production ? [] : [provideStoreDevtools({ maxAge: 25 })]),
provideServiceWorker('ngsw-worker.js', {
enabled: AppConfig.production && !!window.electron,
enabled: shouldEnableServiceWorker(),
registrationStrategy: 'registerWhenStable:30000',
}),
importProvidersFrom(
+2 -1
View File
@@ -25,6 +25,7 @@ import {
logPortalDebugEvent,
logPortalDebugRequest,
} from '@iptvnator/portal/shared/util';
import { getRuntimeBackendUrl } from './runtime-config';
interface PwaXtreamResponse {
readonly payload?: unknown;
@@ -69,7 +70,7 @@ export class PwaService extends DataService {
]);
/** Proxy URL to avoid CORS issues */
corsProxyUrl = AppConfig.BACKEND_URL;
corsProxyUrl = getRuntimeBackendUrl();
constructor() {
super();
@@ -0,0 +1,39 @@
import { resolveBackendUrl, shouldEnableServiceWorker } from './runtime-config';
describe('runtime config helpers', () => {
it('uses runtime BACKEND_URL when provided', () => {
expect(
resolveBackendUrl(
{ BACKEND_URL: ' http://self-hosted.local/api ' },
'https://fallback.example'
)
).toBe('http://self-hosted.local/api');
});
it('falls back to the build-time backend URL when runtime config is missing or blank', () => {
expect(resolveBackendUrl(undefined, 'https://fallback.example')).toBe(
'https://fallback.example'
);
expect(resolveBackendUrl({}, 'https://fallback.example')).toBe(
'https://fallback.example'
);
expect(
resolveBackendUrl(
{ BACKEND_URL: ' ' },
'https://fallback.example'
)
).toBe('https://fallback.example');
});
it('enables service worker only for production builds with browser support', () => {
expect(
shouldEnableServiceWorker(true, {
serviceWorker: {},
} as Navigator)
).toBe(true);
expect(
shouldEnableServiceWorker(false, { serviceWorker: {} } as Navigator)
).toBe(false);
expect(shouldEnableServiceWorker(true, {} as Navigator)).toBe(false);
});
});
@@ -0,0 +1,27 @@
import { AppConfig } from '../../environments/environment';
export interface IptvnatorRuntimeConfig {
readonly BACKEND_URL?: string;
}
export function resolveBackendUrl(
runtimeConfig: IptvnatorRuntimeConfig | undefined,
fallbackUrl: string
): string {
const runtimeUrl = runtimeConfig?.BACKEND_URL?.trim();
return runtimeUrl || fallbackUrl;
}
export function getRuntimeBackendUrl(): string {
return resolveBackendUrl(
globalThis.window?.__IPTVNATOR_CONFIG__,
AppConfig.BACKEND_URL
);
}
export function shouldEnableServiceWorker(
production = AppConfig.production,
navigatorRef: Navigator | undefined = globalThis.navigator
): boolean {
return production && !!navigatorRef && 'serviceWorker' in navigatorRef;
}
+1
View File
@@ -0,0 +1 @@
window.__IPTVNATOR_CONFIG__ = window.__IPTVNATOR_CONFIG__ || {};
+2 -5
View File
@@ -20,6 +20,7 @@
<link rel="manifest" href="manifest.webmanifest" />
<link rel="apple-touch-icon" href="assets/icons/apple-touch-icon.png" />
<link rel="icon" type="image/x-icon" href="assets/icons/favicon.ico" />
<script src="assets/app-config.js"></script>
<style>
/* Inline splash — paints immediately while the JS bundle and the
styles.css (~300KB) are still downloading and Angular is
@@ -68,11 +69,7 @@
</style>
</head>
<body class="mat-app-background">
<div
id="initial-splash"
role="status"
aria-label="Loading IPTVnator"
>
<div id="initial-splash" role="status" aria-label="Loading IPTVnator">
<span class="splash-mark">IPTVnator</span>
<span class="splash-spinner" aria-hidden="true"></span>
</div>
Vendored
+3
View File
@@ -37,6 +37,9 @@ declare global {
}
interface Window {
__IPTVNATOR_CONFIG__?: {
BACKEND_URL?: string;
};
electron: {
onPlaylistRefreshEvent?: (
callback: (data: PlaylistRefreshEvent) => void
+29 -28
View File
@@ -1,30 +1,31 @@
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/manifest.webmanifest",
"/*.css",
"/*.js"
]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
]
}
}
]
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/manifest.webmanifest",
"/*.css",
"/*.js"
]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"!/assets/app-config.js",
"/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
]
}
}
]
}