nrwl / nx

Smart Monorepos ยท Fast CI
https://nx.dev
MIT License
23.28k stars 2.32k forks source link

best practices to share environment variables with lib modules #208

Closed xmlking closed 6 years ago

xmlking commented 6 years ago

in my lib module CoreModule I need to access environment which part of my app myapp

libs/core/src/core.module.ts

import {environment} from "../../../apps/myapp/src/environments/environment";

TSLint is complinting this kind of inport.

looking for guideline how we can share app's environment with lib module.

dereklin commented 6 years ago

Interesting you would create a Core module in libs. To me, libs should contain the supporting modules, services, components, etc. But maybe it's just naming...

Anyways, for me, I use InjectionToken to provide the environment or a slice of it. And then in the libs modules/service, I inject the environment there. like:

https://github.com/dereklin/nx-demo-jest/blob/lazy-5/apps/app1/src/app/services/index.ts

https://github.com/dereklin/nx-demo-jest/blob/lazy-5/libs/simple-http-service/src/simple-http-service.ts

xmlking commented 6 years ago

Good idea @dereklin thanks. I had a question on best practice where to keep shared and core modules , and looking for opinion. My reasoning for sharing them is , in most cases I have similar shared modules for all my apps. https://twitter.com/xmlking/status/953812533046996992?ref_src=twcamp%5Eshare%7Ctwsrc%5Em5%7Ctwgr%5Eemail%7Ctwcon%5E7046%7Ctwterm%5E1

dereklin commented 6 years ago

If your meaning of Core to be equivalent of Shared, then it makes sense. I understand/interpret Core differently. To me, Core is the Core of the App and should live inside the App.

I think it makes sense to make a ShareModule and put all the shared stuff there, components, services, etc. And put the ShareModule in libs. But I would also use index.ts file to export individual items in the libs folder so that they can be located flatly and easily. One consideration if at some point you may need to deploy a component to npm or some internal package system to lock down the version. So you need to think about how to address these situations.

xmlking commented 6 years ago

I am using ShareModule to encapsulate Angular and commonly used 3rd party modules (Material Design, flex-layout, Angular's CommonModule, FormsModule etc) and importing ShareModule into each Lazy-loaded Feature Modules (which are also under lib) My CoreModule consists shared Services, and some Components that are only used by AppModule. This CoreModule is imported only into AppModule, basically to keep AppModule slim.

I am still modeling reusable services/components as Modules under lib

This is described in Angular Style Guide and here

Wanted to get other peoples opinion on this approach: is still valid for monorepo/nx paradigm!

dereklin commented 6 years ago

Keep in mind that the whole libs folder is already shared between apps. And in general, making small units will help code reusability. And making small units usually means making these units flat. The downside of a flat structure is when you have a thousand units, it becomes difficult to find what you are looking for. This is art rather than science. I think we have to make trade-offs and pick the optimal solution for the project and team.

Are you lazy loading the FeatureModule with a route? If so, is the SharedModule only available to FeatureModule? Or how do you lazy load a FeatureModule without using a route?

xmlking commented 6 years ago

here is my experimental workspace setup: https://github.com/xmlking/nx-starter-kit

step-by-step instructions tor reproduce: https://github.com/xmlking/nx-starter-kit/blob/master/PLAYBOOK.md

Still have some issues with some ng generators, I will open separate issues.

PS: moved CoreModule to apps/default app

xmlking commented 6 years ago

I fixed following long-path warnings that TSlint and intelliJ is complaining by following @tomastrajan blog's best praticies. Before

//some code in lib's modules
import {environment} from "../../../apps/default/src/environments/environment";
import { PageTitleService } from '../../../../../apps/default/src/app/core/services/page-title/page-title.service';

After

import { environment } from '@env/environment';
import { PageTitleService } from '@default/core';

my tsconfig.json now looks as:

    "paths": {
      "@nx-starter-kit/*": [
        "libs/*"
      ],
      "@default/*": ["apps/default/src/app/*"],
      "@env/*": ["apps/default/src/environments/*"]
    }

Should nx-cli automatically do this for us?

tomastrajan commented 6 years ago

I think that the issue here as described by the @dereklin is that the stuff which goes to libs is supposed to be reusable in a "standalone" manner. That means it should not really depend directly on other parts of application.

CoreModule is not a very good candidate for extracting into separate standalone lib becouse is heavily coupled with the app itself and is used in the most cases in the AppModule as mentioned above.

Example of libs style functionality can be a stateless service which does some calculation or transformation of data. The key here is that the service SHOULD receive everything from the caller.

In your example that means that only the app imports environment ( could very well do it with TS path aliases like @env/environment) but then passes env properties to the libs service during the function call as a parameter.

That way libs functionality remains stateless and standalone and hence more reusable.

Hope that helps! ๐Ÿ˜‰

dereklin commented 6 years ago

Importing from apps could create code entanglement issues

ThomasBurleson commented 6 years ago

@xmlking - Defining an @env/* path is an interesting idea. Perhaps we should add this to the schematics. And document using environment in libs files.

vsavkin commented 6 years ago

Imo the best way to share env variables is to do something like this:

import {config} from '../environments/environment';

@NgModule({
    providers: [
        {
            provider: 'someEndpointUrl', useValue: config.someEndpointUrl
        }
    ]
})
class AppModule {
}

Then do this somewhere in your lib:

@Component({...})
class MyComponent {
    constructor(@Inject('someEndpointUrl') endpointUrl: string) {
    }
}
  1. It keeps your libs env independent.
  2. It allows you to build libs once, without knowing the apps they are used by or the env they ran in. In CLI 1.x this isn't a big deal, but it will get more important once we start more aggressively cache the results of lib compilation.

Using @env/ has some advantages though. It allows to use to improve treeshaking in some situations.

zbarbuto commented 6 years ago

@vsavkin The problem with this approach is that you can't make the most of tree shaking. Let's say you want to use a development module when running locally and a production noop module instead when building for production - you would want the development module tree shaken out of prod builds. You want to be able to do something like this:

import { prod } from 'environments';
const DevelopModule = prod ? NoopModule : DevelopmentModule;
@NgModule({
  imports: [DevelopModule],
 //...
})
export class MyModule {}

Which should cause DevelopmentModule to get shaken out, i.e. what you want (imagine it's quite large with a lot of test data).

@env/ seems to be the only way to do this in a libs module that I can see.

JackRus commented 5 years ago

Hi! @xmlking - did everything as described above. Getting this error when building my lib:

`BUILD ERROR error TS6059: File 'E:/repos/Ins/Ins.Web/approot/src/environments/environment.ts' is not under 'rootDir' 'E:\repos\Ins\Ins.Web\approot\projects\ins\src'. 'rootDir' is expected to contain all source files.

Am I missing something? Using Angular + CLI and etc v 7.1.0. Thx in advance!

I really would love to use "@env", can't make it work.

xmlking commented 5 years ago

So far it is working for me. Example project https://github.com/xmlking/ngx-starter-kit

GrandSchtroumpf commented 5 years ago

The @env/*: ['some-place/*'] works well with Angular apps, but it doesn't work with node-api. I have to use @env/environment: ['some-place/environment.ts'] for the node-api to build. Any idea why ?

xmlking commented 5 years ago

@GrandSchtroumpf I am using import { environment as env } from '@env-api/environment'; check here in case you need example https://github.com/xmlking/ngx-starter-kit/blob/develop/apps/api/src/app/notifications/subscription/subscription.service.ts

ZenSoftware commented 5 years ago

I thought you guys would find this useful: Using Abstract Classes As Dependency-Injection Tokens For Swappable Behaviors

I have 3 Angular apps in an Nx project that share some common environment variables. A public site, a private portal, and an admin app. In my libs, I am using an abstract Environment class that defines common environment variables.

export abstract class Environment {
  abstract readonly production: boolean;
  abstract readonly appUrls: {
    readonly public: string;
    readonly portal: string;
    readonly admin: string;
  };
}

Then I changed the 3 environment.ts files to be as follows.

import { Environment } from '@my-lib-prefix/common';

class EnvironmentImpl implements Environment {
  production = false;
  appUrls = {
    public: 'http://localhost:4200',
    portal: 'http://localhost:4201',
    admin: 'http://localhost:4202'
  };
}

export const environment = new EnvironmentImpl();

The environment.prod.ts would of course be symmetric to the dev environment.ts. Then I provide the respective environment dependency, in the root app.module.ts for each of the Angular apps.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { Environment } from '@my-lib-prefix/common';
import { environment } from '../environments/environment';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [{ provide: Environment, useValue: environment }],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now any component can inject environment dependencies in a clean, and common manner.

import { Component } from '@angular/core';
import { Environment } from '@my-lib-prefix/common';

@Component({
  selector: 'my-login',
  templateUrl: './my-login.component.html'
})
export class MyLoginComponent {
  constructor(private env: Environment) {}
}

It enforces each environment.ts to implement the "common" environment variables defined in the abstract class. Also, each respective EnvironmentImpl can be extended with their own specific environment variables specific to the app. This approach seems very flexible and clean. Cheers! ^_^

rsyhanes commented 5 years ago

@ZenSoftware I do like the approach you presented. My only contribution would be that instead of using a class for representing the Configuration contract, you could use as well an interface with the caveat you would need to create an InjectionToken for that interface as lookup key. Either way you instantiate something, your Config Class or the InjectionToken for the interface.

ZenSoftware commented 5 years ago

@rsyhanes I myself was actually trying to use TypeScript Interfaces to solve this problem. Then I learned how to use TypeScript Abstract Classes as Dependency Injection Tokens. This approach seems to have the benefit of injecting dependencies via the class name, instead of an InjectionToken. Environment variables are often used by many members of a team. Keeping its use as simple as possible should help to ensure the code base remains sustainable. (โˆฉ^o^)โŠƒโ”โ˜†ใ‚œ.*

Josh-Hicks commented 5 years ago

It seems like all these @env/ examples reference a specific default app. Is it possible to get the current app instead? Or in other words, get the app that's currently invoking this lib.

xmlking commented 5 years ago

Create as many named environments you need in root tsconfig.json Generally I will have one for each app in /apps

"@env-api/environment": ["apps/api/src/environments/environment.ts"],
"@env-webapp/environment": ["apps/webapp/src/environments/environment.ts"],
Josh-Hicks commented 5 years ago

The problem is that the lib shouldn't have to reference just one app explicitly. If I have 3 apps that all use the same lib, how will the lib know which environment to use?

xmlking commented 5 years ago

@Josh-Hicks agree. may be we need a common env that is independent of all apps. lib/shared/src/environments/environment.ts ? other idea: multiple configs merging at runtime from : https://twitter.com/yurzui/status/1138816561848995841

Josh-Hicks commented 5 years ago

Sounds like this specific problem may not be possible to solve.

// in lib import {environment} from '...'; // <-- this is unknown at this point because it could be used in ANY app const googleMapsParams: { myKey: environment.key // <-- specific to the app that's using this lib } @NgModule({ imports: [ AgmCoreModule.forRoot(googleMapsParams) // <-- this needs an environment config ]}) export class MyLibModule {}

This tweet explains the issue. https://twitter.com/MannIsaac/status/1139257629769961475

tcoz commented 5 years ago

Maybe you can solve it by using forRoot and dynamically returning a given module setup from the lib module itself. I came up with this a little while ago so my lib would know not to import a module used to provide an in memory api. I will provide example shortly (not at my machine).

tcoz commented 5 years ago

I wrote it up on my website actually, go here and select the dynamic modules article.

http://www.tcoz.com/newtcoz/#/errata

The trick is to feed in the env via forRoot, so whatever is loading it becomes the relevant env. You can then switch on the env data and return a given setup. It was one piece of the ideal mono repo picture nx didn't seem to solve.

Extending and tinkering with this notion,I've managed a few interesting things that don't seem otherwise possible.

Bielik20 commented 5 years ago

I have stumbled upon this issue some time ago. In my case I have multiple frontend apps (web and ionic) sharing the same config, and multiple backend apps (node/express and firebase functions) sharing the same config. After giving it some though I have decided on a solution that would employ angular.cli.

  1. Remove environments folder from each app.
  2. In the root create:
    • environments
      • frontend
      • environment.ts
      • environment.prod.ts
      • backend
      • environment.ts
      • environment.prod.ts
  3. In tsconfig.json add paths:
    "@env/frontend": ["environments/frontend/environment.ts"],
    "@env/backend": ["environments/backend/environment.ts"],
  4. For each of your apps in angular.cli replace:
    "fileReplacements": [
    {
    "replace": "apps/web-app/src/environments/environment.ts",
    "with": "apps/web-app/src/environments/environment.prod.ts"
    }
    ],

with:

"fileReplacements": [
  {
    "replace": "environments/frontend/environment.ts",
    "with": "environments/frontend/environment.prod.ts"
  }
],
  1. For each backend app accordingly.

This will let you use your environment as you would normally in apps and libs. The only downside is that you don't have any health checks that would prevent you from importing backend environment in frontend lib/app. I don't imagine myself doing that so I settled on this solution.

Still if you worry that it may be a problem for you then you could still use this solution but not provide this environment in the root and through tsconfig paths but create separate libs for frontend and backend environments and then use nx taging to disallow import from those paths in certain apps/libs.

Hope it helps.

lukedupuis commented 5 years ago

There's a relatively simple way to make the environment object available to a shared service.

After passing the service into a component constructor, set an "environment" property on the instance of the service, rather than trying to set it on the class.

Service

@Injectable({
  providedIn: 'root'
})
export class Service {

  environment: any;

  aFunctionThatUsesEnvironment() {
    console.log(this.environment);
  }

}

Component

import { environment } from '../../environments/environment';

export class Component {

  constructor(private service: Service) {
    this.service.environment = environment;
    this.service.aFunctionThatUsesEnvironment();
  }

}

The environment object is printed to the console when the component is called.

michelcve commented 5 years ago

I don't really understand all the discussion here. IMO Victor's solution is the best approach.

Your libraries/classes should NOT depend on the environment directly in your classes, you should use InjectionTokens instead, for all the reasons Victor already stated.

It's not hard to use InjectionTokens anyway, so there is no real good reason to do it any other way.

tcoz commented 5 years ago

You're absolutely right; you don't really understand all the discussion. The solution you refer to would not resolve all contexts, certainly not the one I encountered.

On Jul 1, 2019, at 3:01 AM, michelcve notifications@github.com wrote:

I don't really understand all the discussion here. IMO Victor's solution is the best approach.

Your libraries/classes should NOT depend on the environment directly in your classes, you should use InjectionTokens instead, for all the reasons Victor already stated.

It's not hard to use InjectionTokens anyway, so there is no real good reason to do it any other way.

โ€” You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

michelcve commented 5 years ago

@tcoz Perhaps I don't, but I haven't seen an example yet, where it is really required to have your environment available in your module. There might be edge cases, as Victor stated, but usually it's bad architecture.

My main point being is that the specific question by XMLKing was aswered by Victor. In my opinion, specific corner cases are best discussed in a seperate issue (where you even might have a chance of Nrwl joining in on the discussion), and keeping this thread 'clean', and the proper solution (for most cases) easily found.

But alas, we're beyond that point now, aren't we ;-).

As you're sure to bring up your specific problem, I've taken a look at your site.

Correct me if I'm wrong, but basically you want to load a different module, depending on some environment flag.

In my opinion, that kind of customization is part of your application (hence it is configured there). As such, you should also do the conditional loading of the modules there. Thus, no need to pass your environment to the modules as well.

Basically the same concept as you did, but then in your AppModule. E.g.:

imports: [ BrowserModule, env.production ? MyProductionModule : MyDevModule ],

If you really really want to do that stuff in your library (I wouldn't recommend it myself), your approach would probably fit. But again, I personally wouldn't recommend it, based upon "seperation of concerns".

lukedupuis commented 5 years ago

I was getting an error saying ngMetadataName was undefined when trying to inject the environment variable into a shared service, seemingly because injecting a provider into a service that's provided in root causes a circular dependency?

I can use Victor's excellent method without any trouble to inject into components.

To get the environment object into a shared service instance while avoiding the undefined error, now I'm using an app initializer provider with a factory.

app.initializers.ts

export const EnvironmentInitializer = {
  provide: APP_INITIALIZER,
  deps: [Injector],
  multi: true,
  useFactory: function initEnvironment(injector: Injector): () => Promise<any> {
    return () => {
      const initEnvironmentOn = [ ApiService ];
      initEnvironmentOn.forEach(Class => injector.get(Class).environment = environment);
      return new Promise<any>(resolve => resolve());
    }
  }
};

app.module.ts

import { EnvironmentInitializer } from './app.initializers';

providers: [
  EnvironmentInitializer
],

api.service.ts (Still have to define the variable)

environment: any;

sandeepsuvit commented 5 years ago

A solution that worked for me following @vsavkin comment Create a folder named app-config under libs and add an index.ts file inside app-config folder. This lib can be shared across all your apps. Add the following content inside the index.ts file

import { InjectionToken } from '@angular/core';

export const APP_CONFIG = new InjectionToken('Application config');

Open base tsconfig.json file and add the path for app-config so that it can be useful for importing into your app with @app-workspace/app-config

"paths": {
      "@app-workspace/ui": ["libs/ui/src/index.ts"],
      "@app-workspace/auth": ["libs/auth/src/index.ts"],
      "@app-workspace/utils": ["libs/utils/src/index.ts"],
      "@app-workspace/app-config": ["libs/app-config/index.ts"]
    }

Now inside your apps open the file under apps/app1/src/app/app.module.ts and make the following changes for the providers array

import { APP_CONFIG } from '@app-workspace/app-config';
import { environment } from '../environments/environment';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    AppRoutingModule,
  ],
  providers: [
    { provide: APP_CONFIG, useValue: environment}
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Here is the sample environment.ts file that is residing under app1

export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000/api',
};

You can use app-config inside your shared libs as-well, for example lets say you are making api calls from within your shared lib libs/auth/src/lib/services/auth.service.ts

import { APP_CONFIG } from '@app-workspace/app-config';
import { Inject, Injectable } from '@angular/core';

@Injectable()
export class AuthService {

  constructor(
    @Inject(APP_CONFIG) private appConfig: any
  ) {
        console.log(this.appConfig.apiUrl); // This will print `http://localhost:3000/api`
    }
}

Hope this is helpful :) Also on a sidenote, you may have to restart your application if you get any error while using the imports.

tcoz commented 5 years ago

The issue with this though, if I understand it correctly, is while this will be available to the components within your app, the environment data is available via being provided. This means it will not be available at the time the modules are being initialized, which means you can't use it to dynamically configure a module (which is typically done via TheModule.forRoot). I came up with a similar approach a while back and realized this solution didn't do the job I was looking for. I get around this by feeding the environment data to any module that needs to dynamically configure itself (such as altering its imports based on environment, which is extremely useful in CI/CD) by doing what I mention before.

If you're wondering "what reason would you ever want to alter the imports of a module", I use angular in-memory-api to provide HTTP interception for mock data. But I don't want that interception in production. You use the feature by importing the module, so I wanted a way to say, "if you're in production, don't import this module." Providers don't fit the bill here, they are available too late in the lifecycle.

lukedupuis commented 5 years ago

Yes @sandeepsuvit, thank you, that is helpful, and the challenge I faced was that the environment path in the libs/app-config/index.ts isn't dynamic, so you can't point to this app's environment, and then that app's environment, because ..environments/environment just points to one specific environment, so it always points to the same environment.

Here's another approach to enable pointing to different environment files, more aligned with https://angular.io/guide/singleton-services#the-forroot-pattern than my previous attempts. I also acknowledge this is slightly off base from proper library, since I currently have a few .ts files in a flatter universal/shared/services/.ts, universal/shared/modules/.ts, etc. structure, so my apologies for extending the closed issue further. The typescript path aliases are neat too.

shared.module.ts

import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';

import { ApiService } from '../services/api.service';

@NgModule({
  declarations: [],
  imports: [
    CommonModule
  ]
})
export class SharedModule {

  static forRoot(environment: any): ModuleWithProviders {
    return {
      ngModule: SharedModule,
      providers: [
        ApiService,
        { provide: 'environment', useValue: environment },
      ]
    };
  }

}

app.module.ts

import { InjectionToken, NgModule } from '@angular/core';
import { SharedModule } from '../../../../shared/modules/shared.module';
import { environment } from '../environments/environment';

@NgModule({
  declarations: [],
  imports: [
    SharedModule.forRoot(environment)
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

api.service.ts

import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core';

import { catchError, shareReplay } from 'rxjs/operators';
import { of } from 'rxjs';
import { ServerResponse } from '../interfaces/server-response';

const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
  body: {}
};

@Injectable()
export class ApiService {

  constructor(
    @Inject('environment') private environment,
    private httpClient: HttpClient
  ) {}

  get(url: string) {
    const apiUrl = this.environment.urls.api + url;
    return this.httpClient.get<ServerResponse>(apiUrl)
      .pipe(catchError((error: HttpErrorResponse) => { return of(error); }), shareReplay());
  }

}

ApiService can now be referenced from the shared folder by any app-specific component and it will use the correct environment for that app.

simonua commented 5 years ago

@vsavkin, thank you for your approach. It's worked for me once I solved one small typo.

provider: 'someEndpointUrl', useValue: config.someEndpointUrl

should be

provide: 'someEndpointUrl', useValue: config.someEndpointUrl

Would you be able to update your example, please? Thank you!

SachinShekhar commented 5 years ago

All solutions make environmental variables available in components and services. But, I need it inside @ngModule decorator. How to achieve it?

Here's my exact use case: I am factoring out Auth section (which uses Firebase) of one app into a shared library because I need to add more apps to the workspace which can make use of existing Auth section. The Problem: Official AngularFireModule expects Firebase config details in @ngModule using initializeApp() method and all of my apps need to use different Firebase config (api keys etc). My Approach: I am trying to pass config to AuthModule using forRoot() convention and then trying to use in-built APP_INITIALIZER provider inside the Auth lib. But, I have zero idea how to inject the config data inside AngularFireModule.InitializeApp().

Can Nx replace a plain text file inside a lib based on what app is trying to execute/ import it? Any help will be appreciated even if it's beyond the scope of Angular or Nx.

EDIT:

I solved the issue by using static property in the service (in shared lib) which was receiving config data. APP_INITIALIZER is the solution!

chungminhtu commented 4 years ago

EDIT:

I solved the issue by using static property in the service (in shared lib) which was receiving config data. APP_INITIALIZER is the solution!

I have just has the same problem with this, could you share the code, pls?

SachinShekhar commented 4 years ago

EDIT:

I solved the issue by using static property in the service (in shared lib) which was receiving config data. APP_INITIALIZER is the solution!

I have just has the same problem with this, could you share the code, pls?

firebase.module.ts file:

import { NgModule, ModuleWithProviders, APP_INITIALIZER } from '@angular/core';
import { CommonModule } from '@angular/common';

import { AngularFireModule } from '@angular/fire';
import { AngularFireAuthModule } from '@angular/fire/auth';

import { ModuleConfig } from './module-config.model';
import { FirebaseConfigService } from './firebase.config';

import { FirebaseService } from './firebase.service';

@NgModule({
  imports: [
    CommonModule,
    AngularFireModule.initializeApp(FirebaseConfigService.data),
    AngularFireAuthModule
  ],
  providers: [FirebaseService]
})
export class FirebaseServiceModule {
  static config(data: ModuleConfig): ModuleWithProviders {
    return {
      ngModule: FirebaseServiceModule,
      providers: [
        FirebaseConfigService,
        {
          provide: APP_INITIALIZER,
          multi: true,
          useFactory: (firebaseConfig: FirebaseConfigService) => () => {
            firebaseConfig.setFirebaseConfig(data);
          },
          deps: [FirebaseConfigService]
        }
      ]
    };
  }
}

firebase.config.ts file:

import { Injectable, Inject } from '@angular/core';
import { ModuleConfig } from './module-config.model';

@Injectable()
export class FirebaseConfigService {
  static data = {
    apiKey: '',
    authDomain: '',
    databaseURL: '',
    projectId: '',
    storageBucket: '',
    messagingSenderId: '',
    appId: ''
  };

  setFirebaseConfig(moduleConfig: ModuleConfig) {
    return new Promise<void>((resolve, reject) => {
      FirebaseConfigService.data.apiKey = moduleConfig.firebaseAPIKey;
// Do this for all fields..
      resolve();
    });
  }
}

module-config.model.ts file:

export interface ModuleConfig {
  firebaseAPIKey: string;
// Put all fields which you want to transfer to the module
}

Now, to import this module into main module, use this:

FirebaseServiceModule.config({
// Pass configuration in adherence with ModuleConfig interface
})
chungminhtu commented 4 years ago

Thank you for quick response! this is definitely help.

Melmoth-the-Wanderer commented 4 years ago

@SachinShekhar, @chungminhtu. Have you tried it with angular 8? When I try to test your solution I am not able to override the value of any FirebaseConfigService.data properties. It never changes from the default. Does this approach works for passing configuration to the angular library or only to angular feature (module)?

SachinShekhar commented 4 years ago

@Melmoth-the-Wanderer Yes, I use it with Angular 8 and you can pass the configuration data to a library (which is basically a feature module with special path configured with TS compiler).

You must be making some mistakes.

JefferE commented 4 years ago

@SachinShekhar, I must be doing something wrong as well. You did not post your ./firebase.service file so I must be doing something wrong in the one I created.

I'm getting:

ERROR Error: Uncaught (in promise): Error: Your API key is invalid, please check you have copied it correctly.

NOTE: I have verified that the key I'm passing in the config is indeed correct.

The minute I try to inject any Firebase module into my FirebaseService constructor I get the invalid apiKey error so the config does not seem to be getting set OR my service file is just not set up properly.

import { Injectable } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import {
  AngularFirestore,
  AngularFirestoreDocument
} from '@angular/fire/firestore';
import { of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { User } from '@nx-on-fire-io/shared/auth';

@Injectable({
  providedIn: 'root'
})
export class FirebaseService {

  constructor(
    private afAuth: AngularFireAuth,
    private afs: AngularFirestore,
  ) {

  }

  user() {
    return this.afAuth.authState.pipe(
      switchMap(user => {
        if (user) {
          return this.afs.doc<User>(`users/${user.uid}`).valueChanges();
        } else {
          return of(null);
        }
      })
    );
  }

}
SachinShekhar commented 4 years ago

@JefferE This setup is independent of firebase.service file.

Try debugging by console logging the configuration in a component first. Make sure that your firebase configuration object exactly matches the one provided by firebase (if you are passing few fields like apiKey only, that can be the issue).

JefferE commented 4 years ago

@SachinShekhar Yes, my config is correct, it works in a project that is not trying to use my shared auth lib.

However, I now have this working but I'm not understanding why. My files are exactly the same as yours above and below is my main app module.

My app and shared auth lib ONLY works if I ALSO configure AngularFireModule.initializeApp here in my main app module in addition to doing it in my FirebaseServiceModule with the passed config.

import { FirebaseServiceModule } from '@nx-on-fire-io/shared/auth';
import { SharedAuthModule } from '@nx-on-fire-io/shared/auth';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import { UiMaterialModule } from '@nx-on-fire-io/ui-material';
import { AppRoutingModule } from './app-routing.module';

import { environment } from '../environments/environment';
import { AngularFireModule } from '@angular/fire';
@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    UiMaterialModule,
    AppRoutingModule,
    SharedAuthModule,
    BrowserAnimationsModule,
    FirebaseServiceModule.config(environment.firebase),
    AngularFireModule.initializeApp(environment.firebase),
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}
SachinShekhar commented 4 years ago

@JefferE What's the output of console.log()?

BTW, double initialization is bad thing which can cause unexpected behavior. Don't do this.

JefferE commented 4 years ago

Thanks for your help with this!

If I don't do the double initialization (i.e. I only call AngularFireModule.initializeApp in FirebaseServiceModule),

I get this the minute the component that uses the shared service loads:

ERROR Error: Uncaught (in promise): Error: Your API key is invalid, please check you have copied it correctly.
    at resolvePromise (:4200/polyfills.js:4032)
    at resolvePromise (:4200/polyfills.js:3989)
    at :4200/polyfills.js:4093
    at ZoneDelegate.invokeTask (:4200/polyfills.js:3626)
    at Object.onInvokeTask (:4200/vendor.js:99230)
    at ZoneDelegate.invokeTask (:4200/polyfills.js:3625)
    at Zone.runTask (:4200/polyfills.js:3403)
    at drainMicroTaskQueue (:4200/polyfills.js:3794)
JefferE commented 4 years ago

@SachinShekhar, I also forgot to mention that if I step through the code I can see that the config is indeed passed into the shared lib and my values seem to be getting set into the static var as expected,

JefferE commented 4 years ago

@SachinShekhar

This is where the crash occurs. The minute this constructor runs:

export class FirebaseService {

  constructor(
    private afAuth: AngularFireAuth,
    private afs: AngularFirestore,
  ) {

  }

If I comment out AngularFireAuth and only inject AngularFirestore then it complains that appId must be a string (just another way of saying it doesn't like my config).

SachinShekhar commented 4 years ago

@JefferE Show me your module-config.model.ts and firebase.config.ts files if you followed my naming convention (or, show equivalent files).