ngx-runtime-initializer
Version:
Angular package for loading runtime configuration, managing app settings, and injecting services with post-initialization callbacks, compatible with standalone Angular 19+ applications.
297 lines (288 loc) • 11.1 kB
JavaScript
import * as i0 from '@angular/core';
import { signal, Injectable, inject, provideAppInitializer } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { firstValueFrom, timeout, catchError, throwError, of } from 'rxjs';
/**
* Implementation of the runtime application configuration.
*
* Holds core configuration values (API URL, debug mode, request timeout)
* and the current status of the application.
*
* Can be initialized partially using a `Partial<RuntimeConfig>` object,
* with optional override for the `status` flag.
*
* @example
* // Default initialization
* const config = new AppConfig();
*
* // Initialize with custom API URL and enable debug
* const config = new AppConfig({
* coreConfig: { apiURL: 'https://api.example.com', debug: true, requestTimeout: 20000 }
* }, true);
*/
class AppConfig {
/**
* Core configuration object containing API URL, debug flag, and request timeout.
*/
coreConfig = {
apiURL: '__BACKEND__',
debug: false,
requestTimeout: 30000,
};
/**
* Status of the application (true = up/active, false = down/maintenance).
*/
status = false;
/**
* Creates a new AppConfig instance.
*
* @param init - Partial configuration to override default values.
* @param status - Optional status override (default is `false`).
*/
constructor(init, status = false) {
Object.assign(this, init);
this.status = status;
}
}
/**
* Service for managing the application's runtime configuration.
*
* Provides reactive access to the configuration using Angular signals,
* allows updating configuration at runtime, and exposes convenience
* methods to retrieve configuration values or application status.
*
* Additionally, it sets global variables `$CORE_API` and `$DEBUG`
* when the configuration is updated.
*/
class AppConfigService {
/**
* Internal writable signal holding the current application configuration.
*/
config = signal(new AppConfig());
/**
* Updates the application configuration.
*
* This will also update the global variables:
* - `$CORE_API` with `config.coreConfig.apiURL`
* - `$DEBUG` with `config.coreConfig.debug`
*
* @param config - The new configuration to set.
*/
setConfig(config) {
globalThis.$CORE_API = config.coreConfig.apiURL;
globalThis.$DEBUG = config.coreConfig.debug;
this.config.set(config);
}
/**
* Returns the current status of the application.
*
* @returns `true` if the app is up/active, `false` if it is down/maintenance.
*/
getStatus() {
return this.config().status;
}
/**
* Returns the reactive `WritableSignal` holding the configuration.
*
* Consumers can subscribe to this signal for reactive updates.
*
* @returns The `WritableSignal<AppConfig>` instance.
*/
getConfigSignal() {
return this.config;
}
/**
* Returns the current configuration snapshot.
*
* @returns The current `AppConfig` object.
*/
getConfig() {
return this.config();
}
/**
* Retrieves a specific property from the current configuration.
*
* @typeParam T - Expected type of the property.
* @param key - The key of the property in `AppConfig`.
* @returns The value of the property if it exists, otherwise `undefined`.
*
* @example
* const apiUrl = service.getConfigProperty<string>('coreConfig').apiURL;
*/
getConfigProperty(key) {
return this.config()[key];
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: AppConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: AppConfigService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: AppConfigService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
/**
* Factory function to create a route guard that checks the application status.
*
* The guard compares the current application status from {@link AppConfigService}
* with the expected `shouldBeOk` value. If the actual status does not match,
* the user is redirected to the provided `redirectUrl`.
*
* @param shouldBeOk - Expected status:
* - `true`: route is accessible only if the app is up/active
* - `false`: route is accessible only if the app is down/maintenance
* @param redirectUrl - URL to redirect to when the status does not match `shouldBeOk`
* @returns A `CanActivateFn` that enforces the status check.
*
* @example
* // Only allow access when the app is up
* const guard = createStatusGuard(true, '/maintenance');
*
* // Only allow access when the app is down
* const guard = createStatusGuard(false, '/');
*/
function createStatusGuard(shouldBeOk, redirectUrl) {
return (route, state) => {
const router = inject(Router);
const isStatusOK = inject(AppConfigService).getStatus();
if (isStatusOK !== shouldBeOk) {
router.navigate([redirectUrl], { replaceUrl: true });
return false;
}
return true;
};
}
/**
* Predefined guard that only allows access when the application is UP.
* Redirects to `/maintenance` if the application is down.
*/
const InMaintenanceGuard = createStatusGuard(true, '/maintenance');
/**
* Predefined guard that only allows access when the application is DOWN.
* Redirects to `/` if the application is up.
*/
const MaintenanceGuard = createStatusGuard(false, '/');
/**
* Options for configuring the runtime initializer.
*/
class RuntimeInitializerOptions {
/** URL of the runtime configuration JSON */
configUrl = 'config.json';
/** Array of Angular service classes to inject and pass to callbacks */
servicesToInject = [];
/**
* Optional async callback executed after successful configuration load.
* Receives the loaded `AppConfig` and an object mapping injected services by class name.
*/
postInit = async () => { };
/**
* Optional callback executed if initialization fails.
* Receives the error and injected services object.
*/
handleInitializationFailure = () => { };
/**
* Constructor for fast initialization or custom values.
* @param configUrl - URL of the config JSON
* @param servicesToInject - Array of Angular services to inject
* @param postInit - Callback after successful initialization
* @param handleInitializationFailure - Callback on initialization failure
*/
constructor(configUrl, servicesToInject, postInit, handleInitializationFailure) {
if (configUrl)
this.configUrl = configUrl;
if (servicesToInject)
this.servicesToInject = servicesToInject;
if (postInit)
this.postInit = postInit;
if (handleInitializationFailure)
this.handleInitializationFailure = handleInitializationFailure;
}
}
/**
* Provides an Angular application initializer that loads runtime configuration from a JSON file
* and optionally injects additional services.
*
* @param options - Configuration options for the runtime initializer.
*
* @returns A provider for Angular's `APP_INITIALIZER`.
*
* @example
* // Basic usage: load config from default 'config.json'
* provideRuntimeInitializer({});
*
* @example
* // With custom services and post-init callback
* provideRuntimeInitializer({
* configUrl: '/assets/runtime-config.json',
* servicesToInject: [MyService1, MyService2],
* postInit: async (config, services) => {
* console.log('Config loaded:', config);
* services.MyService1?.initialize();
* },
* handleInitializationFailure: (error, services) => {
* console.warn('Failed to initialize runtime config:', error);
* }
* });
*/
function provideRuntimeInitializer({ configUrl = 'config.json', servicesToInject = [], postInit = async () => { }, handleInitializationFailure = () => { }, }) {
return provideAppInitializer(async () => {
const http = inject(HttpClient);
const appService = inject(AppConfigService);
const injectedServices = {};
servicesToInject.forEach((cls) => {
try {
injectedServices[cls.name.replace('_', '')] = inject(cls, { optional: true });
}
catch {
injectedServices[cls.name.replace('_', '')] = undefined;
}
});
try {
const runtimeConfig = await firstValueFrom(http.get(configUrl).pipe(timeout(3000), catchError((err) => {
if (err.name === 'TimeoutError') {
return throwError(() => new Error(`[RuntimeInitializer] Timeout: Could not load config from ${configUrl}`));
}
if (err instanceof HttpErrorResponse) {
return throwError(() => new Error(`[RuntimeInitializer] Failed to load config from ${configUrl}: ${err.status} ${err.statusText}`));
}
return throwError(() => err);
})));
if (runtimeConfig.coreConfig.debug) {
console.log(runtimeConfig);
}
if (!runtimeConfig?.coreConfig) {
throw new Error('[RuntimeInitializer] Missing coreConfig in config.json');
}
if (!runtimeConfig.coreConfig.apiURL) {
throw new Error('[RuntimeInitializer] coreConfig.apiURL is required');
}
if (typeof runtimeConfig.coreConfig.debug !== 'boolean') {
throw new Error('[RuntimeInitializer] coreConfig.debug must be boolean');
}
if (typeof runtimeConfig.coreConfig.requestTimeout !== 'number') {
throw new Error('[RuntimeInitializer] coreConfig.requestTimeout must be a number');
}
appService.setConfig(new AppConfig(runtimeConfig, true));
if (postInit) {
await postInit(appService.getConfig(), injectedServices);
}
return of(runtimeConfig);
}
catch (error) {
console.error('Failed to load runtime config', error);
const fallbackConfig = new AppConfig();
appService.setConfig(fallbackConfig);
if (handleInitializationFailure) {
handleInitializationFailure(error, injectedServices);
}
return of(fallbackConfig);
}
});
}
/*
* Public API Surface of ngx-runtime-initializer
*/
/**
* Generated bundle index. Do not edit.
*/
export { AppConfig, AppConfigService, InMaintenanceGuard, MaintenanceGuard, RuntimeInitializerOptions, createStatusGuard, provideRuntimeInitializer };
//# sourceMappingURL=ngx-runtime-initializer.mjs.map