From 5adf59834832227c9c8483d349474a75d3d73fe1 Mon Sep 17 00:00:00 2001 From: Preston Lamb Date: Wed, 15 Nov 2023 22:51:24 -0700 Subject: [PATCH 1/7] feat: convert to using an injection token instead of forRoot --- .../src/app/app.module.ts | 19 +++++++++++------- .../src/lib/runtime-config-loader.module.ts | 20 ++----------------- .../runtime-config-loader.service.ts | 16 +++++++-------- .../src/lib/runtime-config.ts | 10 ++++++++++ 4 files changed, 31 insertions(+), 34 deletions(-) diff --git a/apps/runtime-config-loader-demo/src/app/app.module.ts b/apps/runtime-config-loader-demo/src/app/app.module.ts index 3c0df45..05536d0 100644 --- a/apps/runtime-config-loader-demo/src/app/app.module.ts +++ b/apps/runtime-config-loader-demo/src/app/app.module.ts @@ -1,18 +1,23 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; -import { RuntimeConfigLoaderModule } from 'runtime-config-loader'; +import { + RUNTIME_CONFIG_LOADER_CONFIG, + RuntimeConfigLoaderModule, +} from 'runtime-config-loader'; import { AppComponent } from './app.component'; @NgModule({ declarations: [AppComponent], - imports: [ - BrowserModule, - RuntimeConfigLoaderModule.forRoot({ - configUrl: './assets/config/config.json', - }), + imports: [BrowserModule, RuntimeConfigLoaderModule], + providers: [ + { + provide: RUNTIME_CONFIG_LOADER_CONFIG, + useValue: { + configUrl: './assets/config/config.json', + }, + }, ], - providers: [], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/libs/runtime-config-loader/src/lib/runtime-config-loader.module.ts b/libs/runtime-config-loader/src/lib/runtime-config-loader.module.ts index 2eab2b4..5b751cd 100644 --- a/libs/runtime-config-loader/src/lib/runtime-config-loader.module.ts +++ b/libs/runtime-config-loader/src/lib/runtime-config-loader.module.ts @@ -1,6 +1,5 @@ import { HttpClientModule } from '@angular/common/http'; -import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core'; -import { RuntimeConfig } from './runtime-config'; +import { APP_INITIALIZER, NgModule } from '@angular/core'; import { RuntimeConfigLoaderService } from './runtime-config-loader/runtime-config-loader.service'; export function initConfig(configSvc: RuntimeConfigLoaderService) { @@ -19,19 +18,4 @@ export function initConfig(configSvc: RuntimeConfigLoaderService) { }, ], }) -export class RuntimeConfigLoaderModule { - static forRoot( - config: RuntimeConfig - ): ModuleWithProviders { - return { - ngModule: RuntimeConfigLoaderModule, - providers: [ - { - provide: RuntimeConfig, - useValue: config, - }, - RuntimeConfigLoaderService, - ], - }; - } -} +export class RuntimeConfigLoaderModule {} diff --git a/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts b/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts index 1362d81..380203e 100644 --- a/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts +++ b/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts @@ -1,20 +1,18 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable, Optional } from '@angular/core'; -import { RuntimeConfig } from '../runtime-config'; -import { forkJoin, Observable, of, Subject, zip } from 'rxjs'; +import { Injectable, Optional, inject } from '@angular/core'; +import { Observable, Subject, forkJoin, of } from 'rxjs'; import { catchError, take, tap } from 'rxjs/operators'; +import { RUNTIME_CONFIG_LOADER_CONFIG, RuntimeConfig } from '../runtime-config'; @Injectable() export class RuntimeConfigLoaderService { - private configUrl: string | string[] = './assets/config.json'; + private config = inject(RUNTIME_CONFIG_LOADER_CONFIG); + private configUrl: string | string[] = + (this.config && this.config.configUrl) || './assets/config.json'; private configObject: any = null; public configSubject: Subject = new Subject(); - constructor(private _http: HttpClient, @Optional() config: RuntimeConfig) { - if (config) { - this.configUrl = config.configUrl; - } - } + constructor(private _http: HttpClient) {} loadConfig(): Observable { const urls: string[] = Array.isArray(this.configUrl) diff --git a/libs/runtime-config-loader/src/lib/runtime-config.ts b/libs/runtime-config-loader/src/lib/runtime-config.ts index d39f91b..4d595ea 100644 --- a/libs/runtime-config-loader/src/lib/runtime-config.ts +++ b/libs/runtime-config-loader/src/lib/runtime-config.ts @@ -1,3 +1,5 @@ +import { InjectionToken } from '@angular/core'; + export class RuntimeConfig { configUrl: string | string[]; @@ -5,3 +7,11 @@ export class RuntimeConfig { this.configUrl = obj.configUrl || './assets/config.json'; } } + +export const RUNTIME_CONFIG_LOADER_CONFIG = new InjectionToken( + 'RUNTIME_CONFIG_LOADER_CONFIG', + { + providedIn: 'root', + factory: () => ({ configUrl: './assets/config.json' }), + } +); From 721c7da4f0378c02b1bbcee5d83d3c1b81d10384 Mon Sep 17 00:00:00 2001 From: Preston Lamb Date: Wed, 15 Nov 2023 23:05:51 -0700 Subject: [PATCH 2/7] feat: add the ability to have a local config file that is not checked in and overwrites other config options while in local dev mode --- .gitignore | 6 +++++- .../src/app/app.module.ts | 4 +++- .../src/assets/config/config.json | 1 + .../runtime-config-loader.service.ts | 20 ++++++++++++++++--- .../src/lib/runtime-config.ts | 12 +++++++++-- 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index c4db8f8..b10540c 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,8 @@ testem.log .DS_Store Thumbs.db -.nx/cache \ No newline at end of file +.nx/cache + +# Local Config +.env +local.config.json diff --git a/apps/runtime-config-loader-demo/src/app/app.module.ts b/apps/runtime-config-loader-demo/src/app/app.module.ts index 05536d0..ad44d1d 100644 --- a/apps/runtime-config-loader-demo/src/app/app.module.ts +++ b/apps/runtime-config-loader-demo/src/app/app.module.ts @@ -1,4 +1,4 @@ -import { NgModule } from '@angular/core'; +import { NgModule, isDevMode } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RUNTIME_CONFIG_LOADER_CONFIG, @@ -15,6 +15,8 @@ import { AppComponent } from './app.component'; provide: RUNTIME_CONFIG_LOADER_CONFIG, useValue: { configUrl: './assets/config/config.json', + localConfigUrl: './assets/config/local.config.json', + useLocalConfig: isDevMode(), }, }, ], diff --git a/apps/runtime-config-loader-demo/src/assets/config/config.json b/apps/runtime-config-loader-demo/src/assets/config/config.json index 919fe08..f21ecea 100644 --- a/apps/runtime-config-loader-demo/src/assets/config/config.json +++ b/apps/runtime-config-loader-demo/src/assets/config/config.json @@ -1,3 +1,4 @@ { + "unique": "This is a unique value", "testValue": "This is a test" } diff --git a/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts b/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts index 380203e..6d102c7 100644 --- a/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts +++ b/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts @@ -1,18 +1,22 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable, Optional, inject } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Observable, Subject, forkJoin, of } from 'rxjs'; import { catchError, take, tap } from 'rxjs/operators'; -import { RUNTIME_CONFIG_LOADER_CONFIG, RuntimeConfig } from '../runtime-config'; +import { RUNTIME_CONFIG_LOADER_CONFIG } from '../runtime-config'; @Injectable() export class RuntimeConfigLoaderService { private config = inject(RUNTIME_CONFIG_LOADER_CONFIG); private configUrl: string | string[] = (this.config && this.config.configUrl) || './assets/config.json'; + private localConfigUrl: string = + (this.config && this.config.localConfigUrl) || ''; private configObject: any = null; public configSubject: Subject = new Subject(); - constructor(private _http: HttpClient) {} + constructor(private _http: HttpClient) { + console.log(this.config); + } loadConfig(): Observable { const urls: string[] = Array.isArray(this.configUrl) @@ -23,6 +27,16 @@ export class RuntimeConfigLoaderService { this.makeHttpCall(url) ); + if (this.localConfigUrl && this.config.useLocalConfig) { + const localConfig = this.makeHttpCall(this.localConfigUrl).pipe( + catchError(() => { + return of({}); + }) + ); + + observables.push(localConfig); + } + return forkJoin(observables).pipe( tap((configDataArray: any[]) => { this.configObject = configDataArray.reduce( diff --git a/libs/runtime-config-loader/src/lib/runtime-config.ts b/libs/runtime-config-loader/src/lib/runtime-config.ts index 4d595ea..a3f22e9 100644 --- a/libs/runtime-config-loader/src/lib/runtime-config.ts +++ b/libs/runtime-config-loader/src/lib/runtime-config.ts @@ -1,10 +1,14 @@ -import { InjectionToken } from '@angular/core'; +import { InjectionToken, isDevMode } from '@angular/core'; export class RuntimeConfig { configUrl: string | string[]; + localConfigUrl?: string; + useLocalConfig? = isDevMode(); constructor(obj: any = {}) { this.configUrl = obj.configUrl || './assets/config.json'; + this.localConfigUrl = obj.localConfigUrl || ''; + this.useLocalConfig = obj.useLocalConfig || isDevMode(); } } @@ -12,6 +16,10 @@ export const RUNTIME_CONFIG_LOADER_CONFIG = new InjectionToken( 'RUNTIME_CONFIG_LOADER_CONFIG', { providedIn: 'root', - factory: () => ({ configUrl: './assets/config.json' }), + factory: () => ({ + configUrl: './assets/config.json', + localConfigUrl: '', + useLocalConfig: isDevMode(), + }), } ); From 5e34935f8a69c4a47bee18039f5e46bb049548a4 Mon Sep 17 00:00:00 2001 From: Preston Lamb Date: Wed, 15 Nov 2023 23:22:54 -0700 Subject: [PATCH 3/7] chore: use lodash merge to merge the objects so they properly merge deeply --- .../src/assets/config/config.json | 6 +++++- .../runtime-config-loader.service.ts | 3 ++- package-lock.json | 14 ++++++++++++++ package.json | 10 ++++++---- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/apps/runtime-config-loader-demo/src/assets/config/config.json b/apps/runtime-config-loader-demo/src/assets/config/config.json index f21ecea..9aca9a9 100644 --- a/apps/runtime-config-loader-demo/src/assets/config/config.json +++ b/apps/runtime-config-loader-demo/src/assets/config/config.json @@ -1,4 +1,8 @@ { "unique": "This is a unique value", - "testValue": "This is a test" + "testValue": "This is a test", + "deepMergeExample": { + "attr1": "value 1", + "attr2": "value 2" + } } diff --git a/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts b/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts index 6d102c7..24dac7c 100644 --- a/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts +++ b/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts @@ -3,6 +3,7 @@ import { Injectable, inject } from '@angular/core'; import { Observable, Subject, forkJoin, of } from 'rxjs'; import { catchError, take, tap } from 'rxjs/operators'; import { RUNTIME_CONFIG_LOADER_CONFIG } from '../runtime-config'; +import { merge } from 'lodash'; @Injectable() export class RuntimeConfigLoaderService { @@ -41,7 +42,7 @@ export class RuntimeConfigLoaderService { tap((configDataArray: any[]) => { this.configObject = configDataArray.reduce( (acc, configData) => { - return { ...acc, ...configData }; + return merge(acc, configData); }, {} ); diff --git a/package-lock.json b/package-lock.json index bb91413..905514c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@angular/platform-browser-dynamic": "17.0.2", "@angular/router": "17.0.2", "@nx/angular": "17.1.2", + "lodash": "^4.17.21", "rxjs": "~6.6.0", "tslib": "^2.0.0", "zone.js": "0.14.2" @@ -40,6 +41,7 @@ "@nx/workspace": "17.1.2", "@schematics/angular": "17.0.0", "@types/jest": "29.4.4", + "@types/lodash": "^4.14.201", "@types/node": "14.14.33", "@typescript-eslint/eslint-plugin": "6.11.0", "@typescript-eslint/parser": "6.11.0", @@ -6388,6 +6390,12 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, + "node_modules/@types/lodash": { + "version": "4.14.201", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.201.tgz", + "integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -27015,6 +27023,12 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, + "@types/lodash": { + "version": "4.14.201", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.201.tgz", + "integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ==", + "dev": true + }, "@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", diff --git a/package.json b/package.json index c9621f8..8be3f81 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,11 @@ "@angular/platform-browser": "17.0.2", "@angular/platform-browser-dynamic": "17.0.2", "@angular/router": "17.0.2", + "@nx/angular": "17.1.2", + "lodash": "^4.17.21", "rxjs": "~6.6.0", "tslib": "^2.0.0", - "zone.js": "0.14.2", - "@nx/angular": "17.1.2" + "zone.js": "0.14.2" }, "devDependencies": { "@angular-devkit/build-angular": "17.0.0", @@ -38,11 +39,13 @@ "@angular/compiler-cli": "17.0.2", "@angular/language-service": "17.0.2", "@nx/cypress": "17.1.2", + "@nx/eslint": "17.1.2", "@nx/eslint-plugin": "17.1.2", "@nx/jest": "17.1.2", "@nx/workspace": "17.1.2", "@schematics/angular": "17.0.0", "@types/jest": "29.4.4", + "@types/lodash": "^4.14.201", "@types/node": "14.14.33", "@typescript-eslint/eslint-plugin": "6.11.0", "@typescript-eslint/parser": "6.11.0", @@ -62,7 +65,6 @@ "prettier": "2.7.1", "ts-jest": "29.1.0", "ts-node": "10.9.1", - "typescript": "5.2.2", - "@nx/eslint": "17.1.2" + "typescript": "5.2.2" } } From c656dc83aa13d75f59b9b064e25e14e2d19f0925 Mon Sep 17 00:00:00 2001 From: Preston Lamb Date: Wed, 15 Nov 2023 23:34:28 -0700 Subject: [PATCH 4/7] docs: update the README --- README.md | 35 +++++++++++++++++----------- libs/runtime-config-loader/README.md | 35 +++++++++++++++++----------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 59ec41e..9d1d3d4 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,14 @@ imports: [..., RuntimeConfigLoaderModule, ...], That's it; it's that simple. In the `RuntimeConfigLoaderModule`, the `APP_INITIALIZER` token is used to run a function which loads the configuration from a file or an API endpoint that can be used throughout the application. -If you implement the library exactly as it is above, the configuration file needs to be in the `./assets/config.json` location as mentioned above. If you'd like to load the file from a different location, provide that location in the `.forRoot()` method when importing the `RuntimeConfigLoaderModule`: +If you implement the library exactly as it is above, the configuration file needs to be in the `./assets/config.json` location as mentioned above. If you'd like to load the file from a different location, provide that using an injection token in the app's main providers array: ```ts -imports: [ +providers: [ ..., - RuntimeConfigLoaderModule.forRoot( - { configUrl: './path/to/config/config.json' } + { + provide: RUNTIME_CONFIG_LOADER_CONFIG, + useValue: { configUrl: './path/to/config/config.json' } ), ...] ``` @@ -30,29 +31,35 @@ imports: [ If you want to load multiple files, the value of `configUrl` should be an array of strings: ```ts -imports: [ +providers: [ ..., - RuntimeConfigLoaderModule.forRoot( - { configUrl: ['./path/to/config/config-1.json', './path/to/config/config-2.json'] } + { + provide: RUNTIME_CONFIG_LOADER_CONFIG, + useValue: { configUrl: ['./path/to/config/config-1.json', './path/to/config/config-2.json'] } ), ...] ``` > Make sure that the path(s) you provide here is accessible by the Angular application, meaning that the file is somewhere the app can load it. In my opinion, the `assets` folder is the easiest place to work from. -## Multiple Config Paths +## Local Config File -One reason you may want to load multiple configuration objects is so that you can set the configuration on your machine without affecting anyone else. For example, you could have a `local.config.json` file that is not included in source control. Some of the values in that file would overwrite the values in a config file that everyone can use. Another use case is that some config values don't change between environments, and some do. The ones that don't change could go in one file, the ones that do change could go in a second file. Each developer/environment can provide the second file with values they want or need. +One reason you may want to load multiple configuration objects is so that you can set the configuration on your machine without affecting anyone else. For example, you could have a `local.config.json` file that is not included in source control. Some of the values in that file would overwrite the values in a config file that everyone can use. There is an improved way of providing the local config file for your app. There are two new attributes on the config object, `localConfigUrl` and `useLocalConfig`. The first, `localConfigUrl`, is the path to the local config file. Again, generally, this file generally won't be checked in to source control. The second is called `userLocalConfig`. The recommendation is to set the value of this to `isDevMode()` from `@angular/core`, but you could set the value of this to true or false based on any other condition you would like. -It's important to know that if an attribute is repeated in two configuration files, the latest value is kept. So, let's say you have `apiUrl` in both files, `config-1.json` and `config-2.json`. Let's assume the files are passed in to the `forRoot` method like this: +The values in the local config file will overwrite all previous values on other config objects that have been loaded. This way you can have a general app config file that everyone uses, but you can overwrite it on your machine without worrying about checking it in and messing up other people's applications. + +> Note: If you provide a file URL and tell the service to load that config, it will load it. If the file doesn't exist, it will produce a 404. That's why it's recommended that the `useLocalConfig` value is set to `isDevMode()`, so that you don't have a 404 show up in production when it shouldn't. ```ts imports: [ ..., - RuntimeConfigLoaderModule.forRoot( - { configUrl: ['./path/to/config/config-1.json', './path/to/config/config-2.json'] } + { + provide: RUNTIME_CONFIG_LOADER_CONFIG, + useValue: { + configUrl: './path/to/config/config.json', + localConfigUrl: './path/to/config/local.config.json', + useLocalConfig: isDevMode() + } ), ...] ``` - -In this case, the `apiUrl` value from `config-2.json` will override the value from `config-1.json`. diff --git a/libs/runtime-config-loader/README.md b/libs/runtime-config-loader/README.md index 59ec41e..9d1d3d4 100644 --- a/libs/runtime-config-loader/README.md +++ b/libs/runtime-config-loader/README.md @@ -16,13 +16,14 @@ imports: [..., RuntimeConfigLoaderModule, ...], That's it; it's that simple. In the `RuntimeConfigLoaderModule`, the `APP_INITIALIZER` token is used to run a function which loads the configuration from a file or an API endpoint that can be used throughout the application. -If you implement the library exactly as it is above, the configuration file needs to be in the `./assets/config.json` location as mentioned above. If you'd like to load the file from a different location, provide that location in the `.forRoot()` method when importing the `RuntimeConfigLoaderModule`: +If you implement the library exactly as it is above, the configuration file needs to be in the `./assets/config.json` location as mentioned above. If you'd like to load the file from a different location, provide that using an injection token in the app's main providers array: ```ts -imports: [ +providers: [ ..., - RuntimeConfigLoaderModule.forRoot( - { configUrl: './path/to/config/config.json' } + { + provide: RUNTIME_CONFIG_LOADER_CONFIG, + useValue: { configUrl: './path/to/config/config.json' } ), ...] ``` @@ -30,29 +31,35 @@ imports: [ If you want to load multiple files, the value of `configUrl` should be an array of strings: ```ts -imports: [ +providers: [ ..., - RuntimeConfigLoaderModule.forRoot( - { configUrl: ['./path/to/config/config-1.json', './path/to/config/config-2.json'] } + { + provide: RUNTIME_CONFIG_LOADER_CONFIG, + useValue: { configUrl: ['./path/to/config/config-1.json', './path/to/config/config-2.json'] } ), ...] ``` > Make sure that the path(s) you provide here is accessible by the Angular application, meaning that the file is somewhere the app can load it. In my opinion, the `assets` folder is the easiest place to work from. -## Multiple Config Paths +## Local Config File -One reason you may want to load multiple configuration objects is so that you can set the configuration on your machine without affecting anyone else. For example, you could have a `local.config.json` file that is not included in source control. Some of the values in that file would overwrite the values in a config file that everyone can use. Another use case is that some config values don't change between environments, and some do. The ones that don't change could go in one file, the ones that do change could go in a second file. Each developer/environment can provide the second file with values they want or need. +One reason you may want to load multiple configuration objects is so that you can set the configuration on your machine without affecting anyone else. For example, you could have a `local.config.json` file that is not included in source control. Some of the values in that file would overwrite the values in a config file that everyone can use. There is an improved way of providing the local config file for your app. There are two new attributes on the config object, `localConfigUrl` and `useLocalConfig`. The first, `localConfigUrl`, is the path to the local config file. Again, generally, this file generally won't be checked in to source control. The second is called `userLocalConfig`. The recommendation is to set the value of this to `isDevMode()` from `@angular/core`, but you could set the value of this to true or false based on any other condition you would like. -It's important to know that if an attribute is repeated in two configuration files, the latest value is kept. So, let's say you have `apiUrl` in both files, `config-1.json` and `config-2.json`. Let's assume the files are passed in to the `forRoot` method like this: +The values in the local config file will overwrite all previous values on other config objects that have been loaded. This way you can have a general app config file that everyone uses, but you can overwrite it on your machine without worrying about checking it in and messing up other people's applications. + +> Note: If you provide a file URL and tell the service to load that config, it will load it. If the file doesn't exist, it will produce a 404. That's why it's recommended that the `useLocalConfig` value is set to `isDevMode()`, so that you don't have a 404 show up in production when it shouldn't. ```ts imports: [ ..., - RuntimeConfigLoaderModule.forRoot( - { configUrl: ['./path/to/config/config-1.json', './path/to/config/config-2.json'] } + { + provide: RUNTIME_CONFIG_LOADER_CONFIG, + useValue: { + configUrl: './path/to/config/config.json', + localConfigUrl: './path/to/config/local.config.json', + useLocalConfig: isDevMode() + } ), ...] ``` - -In this case, the `apiUrl` value from `config-2.json` will override the value from `config-1.json`. From 9b64340d3bc6cc1650c8ca9ebc82c3db83727c4c Mon Sep 17 00:00:00 2001 From: Preston Lamb Date: Wed, 15 Nov 2023 23:36:20 -0700 Subject: [PATCH 5/7] chore: remove console log and update to use inject instead of constructor --- .../runtime-config-loader.service.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts b/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts index 24dac7c..de1e440 100644 --- a/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts +++ b/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts @@ -1,13 +1,14 @@ import { HttpClient } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; +import { merge } from 'lodash'; import { Observable, Subject, forkJoin, of } from 'rxjs'; import { catchError, take, tap } from 'rxjs/operators'; -import { RUNTIME_CONFIG_LOADER_CONFIG } from '../runtime-config'; -import { merge } from 'lodash'; +import { RUNTIME_CONFIG_LOADER_CONFIG, RuntimeConfig } from '../runtime-config'; @Injectable() export class RuntimeConfigLoaderService { - private config = inject(RUNTIME_CONFIG_LOADER_CONFIG); + private _http: HttpClient = inject(HttpClient); + private config: RuntimeConfig = inject(RUNTIME_CONFIG_LOADER_CONFIG); private configUrl: string | string[] = (this.config && this.config.configUrl) || './assets/config.json'; private localConfigUrl: string = @@ -15,10 +16,6 @@ export class RuntimeConfigLoaderService { private configObject: any = null; public configSubject: Subject = new Subject(); - constructor(private _http: HttpClient) { - console.log(this.config); - } - loadConfig(): Observable { const urls: string[] = Array.isArray(this.configUrl) ? this.configUrl From f67e394c78ffca8cab51316d285c55b529b081d7 Mon Sep 17 00:00:00 2001 From: Preston Lamb Date: Wed, 15 Nov 2023 23:55:50 -0700 Subject: [PATCH 6/7] test: make sure tests still work by going back to constructor DI instead of inject --- .../runtime-config-loader.service.spec.ts | 165 ++++++++---------- .../runtime-config-loader.service.ts | 9 +- 2 files changed, 78 insertions(+), 96 deletions(-) diff --git a/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.spec.ts b/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.spec.ts index 09698fc..65d9399 100644 --- a/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.spec.ts +++ b/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.spec.ts @@ -40,44 +40,35 @@ describe('RuntimeConfigLoaderService', () => { expect(service).toBeTruthy(); }); - it( - 'should load the config object', - waitForAsync(() => { - mockHttpService.get = jest - .fn() - .mockReturnValue(of(mockConfigData1)); - service.loadConfig().subscribe(); - - const config = service.getConfig(); - expect(config).toStrictEqual(mockConfigData1); - }) - ); - - it( - 'should return the key from the config object', - waitForAsync(() => { - mockHttpService.get = jest - .fn() - .mockReturnValue(of(mockConfigData1)); - service.loadConfig().subscribe(); - - const configKey = service.getConfigObjectKey('apiUrl'); - expect(configKey).toStrictEqual(mockConfigData1.apiUrl); - }) - ); - - it( - 'should handle a load error', - waitForAsync(() => { - mockHttpService.get = jest - .fn() - .mockReturnValue(throwError('Test Error')); - service.loadConfig().subscribe(); - - const config = service.getConfig(); - expect(config).toStrictEqual(null); - }) - ); + it('should load the config object', waitForAsync(() => { + mockHttpService.get = jest + .fn() + .mockReturnValue(of(mockConfigData1)); + service.loadConfig().subscribe(); + + const config = service.getConfig(); + expect(config).toStrictEqual(mockConfigData1); + })); + + it('should return the key from the config object', waitForAsync(() => { + mockHttpService.get = jest + .fn() + .mockReturnValue(of(mockConfigData1)); + service.loadConfig().subscribe(); + + const configKey = service.getConfigObjectKey('apiUrl'); + expect(configKey).toStrictEqual(mockConfigData1.apiUrl); + })); + + it('should handle a load error', waitForAsync(() => { + mockHttpService.get = jest + .fn() + .mockReturnValue(throwError('Test Error')); + service.loadConfig().subscribe(); + + const config = service.getConfig(); + expect(config).toStrictEqual(null); + })); }); describe('Double Config URL', () => { @@ -93,60 +84,48 @@ describe('RuntimeConfigLoaderService', () => { expect(service).toBeTruthy(); }); - it( - 'should load the config object from both sources', - waitForAsync(() => { - mockHttpService.get = jest - .fn() - .mockReturnValueOnce(of(mockConfigData1)) - .mockReturnValueOnce(of(mockConfigData2)); - service.loadConfig().subscribe(); - - const config = service.getConfig(); - expect(config).toStrictEqual(mockConfigData2); - }) - ); - - it( - 'should return the key from the config object after combining both sources', - waitForAsync(() => { - mockHttpService.get = jest - .fn() - .mockReturnValueOnce(of(mockConfigData1)) - .mockReturnValueOnce(of(mockConfigData2)); - service.loadConfig().subscribe(); - - const configKey = service.getConfigObjectKey('apiUrl'); - expect(configKey).toStrictEqual(mockConfigData2.apiUrl); - }) - ); - - it( - 'should handle a load error when the first file fails', - waitForAsync(() => { - mockHttpService.get = jest - .fn() - .mockReturnValueOnce(throwError('Test Error')) - .mockReturnValueOnce(of(mockConfigData2)); - service.loadConfig().subscribe(); - - const config = service.getConfig(); - expect(config).toStrictEqual(null); - }) - ); - - it( - 'should handle a load error when the second file fails', - waitForAsync(() => { - mockHttpService.get = jest - .fn() - .mockReturnValueOnce(of(mockConfigData1)) - .mockReturnValueOnce(throwError('Test Error')); - service.loadConfig().subscribe(); - - const config = service.getConfig(); - expect(config).toStrictEqual(null); - }) - ); + it('should load the config object from both sources', waitForAsync(() => { + mockHttpService.get = jest + .fn() + .mockReturnValueOnce(of(mockConfigData1)) + .mockReturnValueOnce(of(mockConfigData2)); + service.loadConfig().subscribe(); + + const config = service.getConfig(); + expect(config).toStrictEqual(mockConfigData2); + })); + + it('should return the key from the config object after combining both sources', waitForAsync(() => { + mockHttpService.get = jest + .fn() + .mockReturnValueOnce(of(mockConfigData1)) + .mockReturnValueOnce(of(mockConfigData2)); + service.loadConfig().subscribe(); + + const configKey = service.getConfigObjectKey('apiUrl'); + expect(configKey).toStrictEqual(mockConfigData2.apiUrl); + })); + + it('should handle a load error when the first file fails', waitForAsync(() => { + mockHttpService.get = jest + .fn() + .mockReturnValueOnce(throwError('Test Error')) + .mockReturnValueOnce(of(mockConfigData2)); + service.loadConfig().subscribe(); + + const config = service.getConfig(); + expect(config).toStrictEqual(null); + })); + + it('should handle a load error when the second file fails', waitForAsync(() => { + mockHttpService.get = jest + .fn() + .mockReturnValueOnce(of(mockConfigData1)) + .mockReturnValueOnce(throwError('Test Error')); + service.loadConfig().subscribe(); + + const config = service.getConfig(); + expect(config).toStrictEqual(null); + })); }); }); diff --git a/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts b/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts index de1e440..7fa4609 100644 --- a/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts +++ b/libs/runtime-config-loader/src/lib/runtime-config-loader/runtime-config-loader.service.ts @@ -1,5 +1,5 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; +import { Inject, Injectable, inject } from '@angular/core'; import { merge } from 'lodash'; import { Observable, Subject, forkJoin, of } from 'rxjs'; import { catchError, take, tap } from 'rxjs/operators'; @@ -7,8 +7,6 @@ import { RUNTIME_CONFIG_LOADER_CONFIG, RuntimeConfig } from '../runtime-config'; @Injectable() export class RuntimeConfigLoaderService { - private _http: HttpClient = inject(HttpClient); - private config: RuntimeConfig = inject(RUNTIME_CONFIG_LOADER_CONFIG); private configUrl: string | string[] = (this.config && this.config.configUrl) || './assets/config.json'; private localConfigUrl: string = @@ -16,6 +14,11 @@ export class RuntimeConfigLoaderService { private configObject: any = null; public configSubject: Subject = new Subject(); + constructor( + private _http: HttpClient, + @Inject(RUNTIME_CONFIG_LOADER_CONFIG) private config: RuntimeConfig + ) {} + loadConfig(): Observable { const urls: string[] = Array.isArray(this.configUrl) ? this.configUrl From 3d24a9ca28e210c91e5cb9d354bafe9c0fd737f4 Mon Sep 17 00:00:00 2001 From: Preston Lamb Date: Wed, 15 Nov 2023 23:58:54 -0700 Subject: [PATCH 7/7] chore: update package version --- libs/runtime-config-loader/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/runtime-config-loader/package.json b/libs/runtime-config-loader/package.json index 0c0b12d..ab3602f 100644 --- a/libs/runtime-config-loader/package.json +++ b/libs/runtime-config-loader/package.json @@ -1,6 +1,6 @@ { "name": "runtime-config-loader", - "version": "5.0.2", + "version": "6.0.0", "author": { "email": "preston.j.lamb@gmail.com", "name": "Preston Lamb", @@ -23,9 +23,9 @@ "config" ], "peerDependencies": { - "@angular/common": ">=13.0.0", - "@angular/core": ">=13.0.0", - "@angular/platform-browser-dynamic": ">=13.0.0", + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@angular/platform-browser-dynamic": ">=16.0.0", "rxjs": ">=6.6.0" }, "dependencies": {