angular / angular-cli

CLI tool for Angular
https://cli.angular.dev
MIT License
26.78k stars 11.98k forks source link

Support multiple environments in the same build #3855

Open jnizet opened 7 years ago

jnizet commented 7 years ago

Please provide us with the following information:

OS?

Mac OSX (Sierra)

Versions.

angular-cli: 1.0.0-beta.24
node: 6.9.2
os: darwin x64
@angular/common: 2.4.1
@angular/compiler: 2.4.1
@angular/core: 2.4.1
@angular/forms: 2.4.1
@angular/http: 2.4.1
@angular/platform-browser: 2.4.1
@angular/platform-browser-dynamic: 2.4.1
@angular/router: 3.4.1
@angular/compiler-cli: 2.4.1

This is a feature request

I started internationalizing my application, and I met the following problem: when generating the bundle for the French locale (for example), I should include the locale-specific fr.js script of moment.js. Other libraries could also provide locale-specific JS files, or could need code that is specific to that locale (to internationalize a datepicker, for example).

I think the best way to do that would be to create an 'fr' environment file, simply containing

import 'moment/locale/fr.js';

and to use --env fr. Unfortunately, there doesn't seem to be a way to specify two different environments. And I wouldn't like to create a dev-fr environment, a prod-fr environment, a dev-en environment, a prod-en environment, etc.

Another use-case for that would be to create separate bundles for browsers, containing the polyfills that are needed for different browsers, and thus be able to generate a production bundle, for the French locale, and the chrome browser. I'm sure other use-cases could exist.

So I think it would be nice if angular-cli allowed specifying several environments. This is BTW a feature that exists in other build tools (like Maven profiles, or gradle properties, for example)

What do you think? Is there another nice way to achieve that?

clydin commented 7 years ago

This adds a large amount of complexity and for the listed use cases would require building multiple apps to support all desired locales/etc. Whereas it would most likely be preferred to have one built app that can support all desired locales/etc.

Why not just import all the locales your app supports? Or if there are routes for each locale, import in a lazy loaded module.

clydin commented 7 years ago

Also AOT + I18n using the CLI is not really a complete solution at this point.

jnizet commented 7 years ago

would require building multiple apps to support all desired locales

Well, that's the approach that the angular team seems to have chosen for i18n: one separate bundle per locale, with the translations first extracted into a messages file, then translated, then reinjected in the templates at build time by the AOT compiler.

Here's a quote from the angular.io i18n cookbook:

When you internationalize with the AOT compiler, you pre-build a separate application package for each language.

What am I missing here?

Why not just import all the locales your app supports?

Because that would increase the bundle size, especially if many languages need to be supported, and because it's in contradiction with the design principle that I quoted above, which consists in creating one bundle per locale, and thus not to provide all the translations into a single application.

clydin commented 7 years ago

My point on multiple apps was mainly geared towards the quantity of output builds via the use of the environment concept in this way. (i.e., x locales y browsers dev/prod = a large amount of builds to manage)

If only a handful of locales are required bundling them all can be viable. They would be packaged in the vendor bundle and cached locally. This may be required either way as a translation may support multiple locales.

The CLI is geared towards generating a production deployable app. The cookbook recipe provides a set of application "packages". For now with AOT, unfortunately, there is not much more that. The hope is the CLI will have first-class support for i18n and build an app containing the "packages" for all available translations.

Another option, if you're plan is to use server-side code to provide the relevant app bundles, is to provide momentjs the same way.

hansl commented 7 years ago

@clydin this is the current way of doing it for Angular (x locales * dev/prod). With Universal it would become much more easy to do what you're suggesting, but this is not the world we're living it right now.

@jnizet what you're asking for makes sense, and I'd rather have another solution instead; being able to import an environment file from another environment, which is not currently possible. But if we were to support it, this would work:


import {environment as devEnv} from './environment';

export const environment = Object.assign({}, devEnv, {
  lang: 'fr'
});
clydin commented 7 years ago

@hansl, what i meant was that a developer shouldn't have to run ng build for each translation. Angular's AOT mode requires individual builds but the CLI could manage this for the app as a whole. Additional infrastructure would still be needed to deploy. Although a CLI option could be added to provide a client side script to determine locale if a server-side setup was not desired.

hansl commented 7 years ago

Could, but we don't. Better support for i18n is not planned before the 1.0 final. For now the recommended way is to make a build for each locale you want to support.

jnizet commented 7 years ago

Thanks for your input, @hansl.

For the record, I deal with the multiple bundles generation at a higher level: I have a gradle build that builds everything (backend + frontend), by delegating to angular-cli for the frontend. So my current, successful, strategy is to

I then have a server-side handler that detects the locale from the HTTP request, and serves the appropriate index-xx.html file.

This suits my needs fine. The only, admittedly minor, inconvenience is that I can't statically import the appropriate moment locale JS file. I could hack a system where I would replace the environment.prod.ts file by the one containing the appropriate locale import, but I feel that this is something that angular-cli could be able to do by itself.

jnizet commented 7 years ago

@hansl I don't really understand the strategy you're suggesting, though. Where would I put the 4 lines of code that you posted, and how would I choose, from the command-line, that I want a prod build using the french locale-specific code?

filipesilva commented 7 years ago

@jnizet let me expand upon that solution. Assuming the default:

      "environments": {
        "source": "environments/environment.ts",
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }

So the important bit is that the file in source will, as far as the build system is concerned, ALWAYS be the file in dev (or whatever other env).

You can thus, not have dev actually be the same as source. You can have a separate environments/environment.dev.ts. And then you could import others into it, and extend it:

      "environments": {
        "source": "environments/environment.ts",
        "dev": "environments/environment.dev.ts",
        "prod": "environments/environment.prod.ts",
        "en-dev": "environments/environment.en-dev.ts",
        "fr-dev": "environments/environment.fr-dev.ts"
      }
// environments/environment.fr-dev.ts
import {environment as devEnv} from './environment.dev';

export const environment = Object.assign({}, devEnv, {
  lang: 'fr'
});

Then you could do ng build --env=fr-dev.

jnizet commented 7 years ago

@filipesilva please correct me if I'm wrong, but that would still force me to write a prod-fr and a prod-en environment, in addition to the dev-fr and the dev-en, and thus would still lead to code duplication.

filipesilva commented 7 years ago

@jnizet yes you would still need need to have prod-fr and the like. The solution I posted did away with duplication of dev/prod code but still left you with locale duplication.

The latter problem could be addressed by switching it up a bit though:

      "environments": {
        "source": "environments/environment.ts",
        "en-dev": "environments/environment.en-dev.ts",
        "en-prod": "environments/environment.en-prod.ts",
        "fr-dev": "environments/environment.fr-dev.ts",
        "fr-prod": "environments/environment.fr-prod.ts"
      }

together with these base files:

And then the 'combo' files:

// environments/environment.fr-dev.ts
import {environment as devEnv} from './environment.dev';
import {environment as langFr} from './environment.fr';

export const environment = Object.assign({}, devEnv, langFr);

I understand that it might not be as clean as you would hope, but this solution is available today with no extra design or compromises.

jnizet commented 7 years ago

OK, I understand now. Thanks for your input @filipesilva .

intellix commented 7 years ago

Was talking about this earlier in regards to Continuous Delivery and was given this issue to post my thoughts.

Deployed an app to production via Heroku Pipelines (Review, Staging, Production) yesterday and have also done deployment pipeline via Bitbucket/Bamboo before.

Bundling the environment config into a single package during build causes problems for deploying to a multitude of environments where only a config changes.

Within Heroku and Bamboo, you build an environment agnostic package through a build process and then deploy that same package to different environments, only changing configuration as it goes through the pipeline.

With the current bundling of environment config during build, it means we can't push an agnostic package and change config but we need to completely rebuild as the app makes it way through the pipeline.

Keeping environment configs out of the build and simply copying them to dist would open up the ability to use a single package across environments by dynamically loading the config via webpack, Node or Universal.

filipesilva commented 7 years ago

@intellix I know that's a very popular approach and works great for the scenario you propose. Is there anything blocking you from using it from the CLI side though?

The CLI does not have a specific facility for it but, architecturally, it shouldn't since that strategy is meant to be completely disconnected from the build step.

I think you can have ./src/env-config.json added it to the assets array and then you'd load it at runtime. You can then replace this file after deployment.

Although they are architecturally different, these strategies are not mutually exclusive and serve different purposes. For instance, using the separate config file you could never have different imports for each env, since that needs the build to be done differently.

intellix commented 7 years ago

I think there's nothing blocking me from doing it today, i'll do as you said and then dynamically load in that config

StickNitro commented 7 years ago

I would echo what @intellix posted, we use Bamboo for our CI and deployment pipeline with the added complexity that we have an Electron application which hosts an Angular 2 application bundled with Electron (i.e. through an MSI that the clients install).

Because of this we find we are currently forced to build for each environment passing --environment=env for each environment as there would be no way to change the config once the MSI is built and therefore no way to update the config on each client based on the environment.

For information, we have several different environments including dev, int, test, stage, preprod, training and prod, and there are variants of some of these environments (e.g. for clustered servers), so this presents us a problem that we have to generate multiple builds and artifacts during the build.

Has anyone else encountered this and found a solution?

filipesilva commented 7 years ago

@StickNitro if you need drop-in config files, you can just put them in ./src/assets/ and load them when your application starts:

import { Component, OnInit } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/toPromise';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'app';
  constructor(private http: Http) { }

  ngOnInit() {
    this.http.get('assets/config.json')
      .map(res => res.json())
      .toPromise()
      .then((config) => {
        // do stuff with the config
        console.log(config)
      });
  }
}

This way you don't have to rebuild your app, but you have to engineer your application to load config items at runtime.

The CLI provides build-time configuration, runtime configuration is up to you to implement.

peterlaraia commented 7 years ago

This isn't necessarily a cli specific question, but what I'm curious about is, and maybe someone can clear this up for me, what if I need to provide different values at the NgModule metadata level. I'm assuming there' no way to do that with both aot & a build once deploy many pipeline?

Like say I had

import { LibModule, LibConfig } from '3rd-party/lib';
import { env } from '../environments/environment';

const libConfig: LibConfig = { url: env.urlToWhatever };

@NgModule({
    imports: [ LibModule.forRoot(libConfig) ]
})
export class AppModule {}

As best as I can tell, you'd have to build for each env if you want to use aot, because I get the impression aot relies on the metadata provided in the NgModule to 'compile' the code, right?

With JIT you could technically do something like, set up your server to attach a header specficying the env, have a config.js file where you xhr/ping the server to get the header, then in the NgModule file use that to provide the env in the browser while compiling. (imagining the config.js as, where in the NgModule you would use getEnvConfig() ):

enum Configuration {
    dev = {url: 'devurl'},
    stage = {url: 'stageurl'},
    prod = {url: 'produrl'}
}

var envConfig;

export function getEnvConfig() {
    //if envConfig is defined, return
   //xhr, check header, set envConfig, return envConfig
}

I'm pretty sure you can't do that with aot, and that's just fine, I just want to make sure I understand correctly, cause some of this stuff can get pretty confusing to me.

filipesilva commented 7 years ago

Just want to mention that there's some more good discussion about this topic in https://github.com/angular/angular-cli/issues/7506. I ask you to reply here though, so we can keep this topic in a single issue.

delasteve commented 7 years ago

I did find a gist that is pretty complete implementation wise to possibly fix this issue.

https://gist.github.com/fernandohu/122e88c3bcd210bbe41c608c36306db9

I haven't implemented it yet, but plan on taking a closer look soon.

Hope this helps someone.

tggm commented 7 years ago

I just came across this limitation. We need to be able to deploy a package (dist/.) in multiple environments without having to recompile the whole thing in order to establish a continuous integration flow across multiple environments (DEV, QA, PROD, etc...).

The ng build command assumes that the build and deploy stages are the same thing which is wrong. Thus, the mechanism offered by the --environment=XXXX parameter, aimed at ease of use and helping the developer, is essentially useless beyond very simple development processes.

So, considering that it is indeed an objective simplifying the developer's life, can we expect a simple solution or should we rely on ugly and dirty hacks like the solution shared by @delasteve ?

I see a lot of issues being closed and referred back to this one to "continue the discussion". But the discussion has been going on since January. So, what's missing implement this ?

snarum commented 6 years ago

@filipesilva Your solution looked nice, but if I have my API url in the config.json file, how can I make sure that I don't do any calls to the api before the config.json is returned?

spottedmahn commented 6 years ago

We need to be able to deploy a package (dist/.) in multiple environments without having to recompile the whole thing in order to establish a continous integration flow across multiple environments (DEV, QA, PROD, etc...).

@tggm I couldn't agree more. It seems very surprising that this was not addressed/thought-about from the beginning of Angular. I wonder what I'm missing/not understanding.

intellix commented 6 years ago

@spottedmahn it's because it's not a CLI issue but an Application one that you can easily solve yourself today: https://github.com/angular/angular-cli/issues/3855#issuecomment-274803729

spottedmahn commented 6 years ago

I see, thanks for the link @intellix!

After further reading, it is the following I'm surprised about:

The CLI provides build-time configuration, runtime configuration is up to you to implement.

I understand this might not be a CLI issue but I would have hoped a common pattern would have been created/designed under the Angular framework. CD is a concern most apps will have.

Riccardo-Andreatta commented 6 years ago

This is an interesting article about managing environments and creating them dynamically as the original question was requesting.

Basically, it is an interesting solution to avoid having your environments variables and security keys hardcoded in the repositories: https://medium.com/@natchiketa/angular-cli-and-os-environment-variables-4cfa3b849659

I strongly suggest also to take a look at its first response that introduce a dynamic solution to create the environment file at building time instead of having dozen of environment files: https://medium.com/@h_martos/amazing-job-sara-you-save-me-a-lot-of-time-thank-you-8703b628e3eb

Maybe they are not the best solutions, but a trick to avoid setting up different environments files and commit them with all the security keys in the repository.

bailejl commented 6 years ago

I am a big fan of managing the configuration outside of the app. Configuration can change at anytime and it should not require a build/deployment to make a configuration change. If I have a feature toggle needing to be flipped, I want to only change it. Or there is an infrastructure change and a URL needs to change. Another build and push? I prefer not to. Besides these simple scenarios, environments are no longer static with infrastructure as code (IaC) tools.

A continuous delivery pipeline might appear "static", but it can change, as infrastructure or dependent services change. This is where configuration needs to change independent of the application. Compile time cannot support this without out going to the start of the continuous deliver pipeline. This does not take into account troubleshooting.

You have an issue in production, but you cannot troubleshoot in production. What is the next best thing? Create a new environment with your IaC tools and troubleshoot away. With compiled configuration, I have to do a new build with a new configuration. With a runtime config, I take the code from production and the IaC generated config file into the new environment. This reduces the the variables and makes it so much easier for troubleshooting, as the major difference is in the config files. This scenario assumes you can move data from production to your troubleshooting environment. Besides all of these concerns, a lot of my clients want to manage configuration external to the app.

A number of my clients have thousands of software projects each with multiple environments. This situation is best suited for runtime configuration changes. Or at a minimum, non-compiled externally managed configurations. In this train of thought, I recently set up a proof of concept using Flickr's example.

My current client wanted a system to push configuration changes out with no manual intervention. I used Flickr's configuration management system using Github and Consul, link below. They have well over a thousand software projects with multiple environments. So, runtime and not compile configuration management is crucial for this type of client.

The compiled configuration is a great thing for getting up an running, but when you need to do more, it is a hinderance. I understand the Angular CLI team's point of view on this issue, but it has impacted me when I did build a runtime configuration solution in Angular.

When Angular 2 came out, I create an app that did runtime configuration and it worked great. Then Angular 4 came along and all my dependencies changed. This broke my runtime configuration solution, specifically AngularFire2. Everything else worked, but I could not solve the AngularFire2 issues. I would like to see Angular have a compile and runtime option that Angular libraries to can work to support.

http://code.flickr.net/2016/03/24/configuration-management-for-distributed-systems-using-github-and-cfg4j/

tggm commented 6 years ago

We're using Atlassian Bamboo as a build and deploy tool. I just asked the programmers to stuff all the configuration variables (mainly URL's) in some settings.json and modify the application to read that file at startup.

On the build system (Bamboo) I created a deploy task to manually write a small (but clunky) json file, and stuff it inside the dist.ZIP file. Problem solved.

bailejl commented 6 years ago

@tggm Are you using AngularFire2 in your code, if so I would like to see how you are loading the configuration. I have not done anything with this problem in a year or more, but I have a side project that will need this shortly.

The problem I have run into is how AngularFire initializes in the @ngModules. Previously I had to use some custom code for AngularFire, but it changed with the move to Angular 4. The config would load from a file on the server and was handled by code in the main.ts. Any help would be appreciated.

Hesesses commented 6 years ago

@snarum @filipesilva "Your solution looked nice, but if I have my API url in the config.json file, how can I make sure that I don't do any calls to the api before the config.json is returned?"

Did you manage to solve this or have any ideas how to load the file syncronously?

snarum commented 6 years ago

@Hesesses Yes, i'm satisfied with my solution to this problem. I added the url in a file config.json

{
    "url": "http://localhost:55645/api/"
}

and then I have a class Appconfig

import { Inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import { HttpClient } from '@angular/common/http';
import 'rxjs/add/operator/toPromise';
@Injectable()
export class AppConfig {
    public url: string = null;
    constructor(private http: HttpClient) {
    }
    public load() {
        return this.http.get('config.json').toPromise().then(x=>{this.url = x['url'];
        });
    }
}

that I initialize in app.module.ts using APP_INITIALIZER:

....
 providers: [
    AppConfig,
    { provide: APP_INITIALIZER, useFactory: initConfig, deps: [AppConfig], multi: true },
    AdminService],
....

export function initConfig(config: AppConfig){return () => config.load()}

This should ensure that the url is loaded before any module, and you can use the AppConfig.url variable from your service.

Your service could look like this:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { AppConfig } from '../app.config';

@Injectable()  
export class AdminService {

  constructor(public http: HttpClient, private _config: AppConfig) { }

  ping():any {
    return this.http.get(this._config.url + 'admin/ping',{withCredentials:true});
  }
}

config.json would have to be added as an asset in .angular-cli.json to include it in the dist folder during build.

Hesesses commented 6 years ago

@snarum wow, that was fast! initConfig seems to be missing...?

edit:

export function initConfig(config:AppConfig) {
  return () => config.load();
}

Thank you so much!!!!!

snarum commented 6 years ago

@Hesesses You're right. I've updated comment.

jensbodal commented 6 years ago

APP_INITIALIZER did not work for me because I needed the service to be available while calling the forRoot method of another imported module. The module imports resolve before the providers do.

What did work for me though is creating a provider for the config in main.ts, then doing what I need with it from anywhere in AppModule. This can be improved a bit but for simplicity I put all of it together.

// main.ts

import { CONFIG } from './app.module';

fetch('/configs/config.json').then(data => data.json().then((config) => {
  platformBrowserDynamic(
    [{ provide: CONFIG, useValue: config }]
  )
  .bootstrapModule(AppModule)
  .catch(err => console.log(err));
}));

// app.module.ts

export const CONFIG = new InjectionToken<Config>('CONFIG');

constructor(@Inject(CONFIG) private config: Config) { }

If you can't use fetch because of browser support either use the HttpClient, native Xhr requests, or another library.

zijianhuang commented 6 years ago

I understand the design purpose of those environment files defined in .angular-cli.json are for the same app in different environments: testing, staging and production.

However, we also need to have customer level settings, different in different sites of production for different customers. For example, some settings are feature toggle -- turning on or off some features during startup of the frontend.

For such need, in .NET Framework, we have ApplicationSettings and UserSettings built in the Framework, both are loaded during startup before the first line of application codes is running.

I wish NG Cli supports such scenario too.

Before using NG Cli, I had been using Gulp. And I had "AppSettings.js" and "SiteSettings.js" both similar to environment.js. For example, "ApplicationSettings.js" store service endpoints different in testing, staging and production, while "SiteSettings.js" stores feature toggle info.

However, with NG Cli, I have to use "SiteSettings.json" and retrieve the json file during startup in App.component.ts. For us, we have a login screen, so the site settings could be loaded before the main screen is rendered after logged in.

However, if our app needs to show the main screen right after startup without login, we will be screwed up.

In fact, if we allow remembering password, the app may login automatically before "SiteSettings.json" is loaded, then we are screwed up.

It will be good that NG Cli allow such definition in .angular-cli.json:

"standalone": ["siteSettings.ts"]

So siteSettings.ts is compiled, but siteSettings.js is not bundled into any of the bundled files.

Then when the admin/support deploys the app, the guy may just use a simple text editor to alter siteSettings.js.

mjeson commented 6 years ago

A very rich discussion here . The silver lining appears on issue where build and bundling are tightly coupled in a single action, thus causing various other issues.

intellix commented 6 years ago

The thing is, you don't need anything from CLI for this and I don't believe it's their problem (unless they provide a server in the future perhaps?). You can just do something like this:

/src/environments/environment.ts

declare const NG_CONFIG: {[key: string]: any};

export const environment = {
  production: false,
  apiEndpoint: "https://staging.site.com/api",
  ...(typeof NG_CONFIG === 'undefined' ? {} : NG_CONFIG),
};

/src/index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Your site</title>
  <base href="/">
</head>
<body>
  <!-- Deploy -->
  <app-root></app-root>
</body>
</html>

/server/index.js


const express = require('express');
const app = express();

const PORT = process.env.PORT || 8080;
const NG_CONFIG = process.env.NG_CONFIG ? JSON.parse(process.env.NG_CONFIG) : {};
let content = '';

// Search for <!-- Deploy --> and replace with NG_CONFIG env var parsed as JSON
function prepareContent() {
  const templateFile = path.join(`${__dirname}/../dist/index.html`);

  return new Promise((resolve, reject) => fs.readFile(templateFile, 'utf8', function (error, data) {
    if (error) {
      return reject();
    }

    content = data.replace(/<!-- Deploy -->/g, `<script>const NG_CONFIG = ${JSON.stringify(NG_CONFIG)};</script>`);
    resolve();
  }));
}

app.use(express.static(`${__dirname}/../dist`));

app.get('/*', (req, res) => res.send(content));

// Prepare content and then start server listening
prepareContent().then(() => app.listen(PORT, () => console.log(`Listening on ${PORT}`)));

Now you can move 1x built bundle between servers, environments, pipelines.

server1:

export NG_CONFIG='{ "apiEndpoint": "https://staging1.site.com/api" }'
node server

server2:

export NG_CONFIG='{ "apiEndpoint": "https://staging2.site.com/api" }'
node server

I didn't quite test the above, cause I took it from something that already works and stripped it down/simplified (we have locales there too). I hope it helps or shows you how to accomplish this

You'll end up with an index.html like this served to users:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Your site</title>
  <base href="/">
</head>
<body>
  <script>const NG_CONFIG = {"apiEndpoint":"http://server1.site.com/api"};</script>
  <app-root></app-root>
  <script type="text/javascript" src="/inline.13205f40c1c8384d6852.bundle.js"></script>
  <script type="text/javascript" src="/polyfills.66ccb0c1f627bc1e97d9.bundle.js"></script>
  <script type="text/javascript" src="/scripts.ecdc2a292f67cb2e2879.bundle.js"></script>
  <script type="text/javascript" src="/main.a33b0b7bd94bc1fbb545.bundle.js"></script>
</body>
</html>
zijianhuang commented 6 years ago

Thanks @intellix. Your solution is working really well, since I want features toggle before the first line of the app codes is running, while loading config.json is not reliable because of the nature of asynchronous call.

Beware that the ... operator (spread oeprator) is new in ES6.

kayvanbree commented 6 years ago

We need this so much. There is a situation where all the hacky tricks I saw won't work. I am deploying to a couple of servers where one of them needs to run at http://example.com/ and the other at http://another-example.com/subfolder. The same problem would occur when you want to have an environment for different customers at http://example.com/customer1 and http://example.com/customer2, etc.

Not building environment.ts but rather placing it in the dist folder won't work for this situation, because when we load http://example.com/subfolder and then load environment.ts it will look in http://example.com/environment.ts or something like that. The same problem would occur when you use APP_INITIALIZER. The config file won't be in the correct location, because you want that location to be IN the config/environment file. As of now, I have found not 1 solution to this problem, except having to mess with our nice reverse proxy settings, running the application where we don't want it to run in some environments, etc.

There are way more problems that I don't want to make new builds for,which are solvable with APP_INITIALIZER, but I'd rather have in a build independent environment. For one, we use Wijmo. Terrible as that is already, I just heard we also need to generate a license for every domain we will be deploying to. Without an build independent settings we would have to create a build for every domain we run our application on. The same goes for switching on and off features. Our company does a lot of custom work for several customers. If we would do custom work, we'd also have to create a seperate build for every custom job.

Again, these last problemns could be solved by using APP_INITIALIZER, but the baseHref cannot.

kyubisation commented 6 years ago

We also had the problem, that we needed "runtime" configuration via environment variables. I created a small package to help with that: angular-server-side-configuration

jy2288 commented 6 years ago

May not fit everyone's needs, but a very simple solution using the environment.ts (or environment.prod.ts) file is like so:

/* production */
export let environment = {
  backend1:  'prod...',
  backend2:  'prod...',
  ...
};

/* dev */
if (location.hostname === 'frontend-dev...') {
  environment = {
    backend1:  'dev...',
    backend2:  'dev...',
    ...
  };
}

/* test */
if (location.hostname === 'frontend-test...') {
  environment = {
    backend1:  'test...',
    backend2:  'test...',
    ...
  };
}

My app runs locally (ng serve, using environment.ts) and gets built once for remote dev, test (and soon prod) deployments (using environments.prod.ts).

kayvanbree commented 6 years ago

@kyubisation For your solution I need to switch from my nginx-docker setup to serving with express.js right? (there is not much in your readme about serving the files except express.js in a code snippet)

[edit] And could you change the base href with your solution? Using the APP_INITIALIZER doesn't work for that.

kyubisation commented 6 years ago

Not necessarily. If you can execute a node.js script on startup, that works too. You don't need express.js. Currently it doesn't support changing anything else, but I can implement it. I should be able to do that this evening.

[edit] @kayvanbree I have implemented the functionality for your use case and released a new version.

kayvanbree commented 6 years ago

For everybody trying to set environment variables in Angular when starting a Docker container:

I worked two days to use @kyubisation's library to add a Docker container's environment variables to Angular. I wrote a tutorial that shows how it's done. It's not the most beautiful way to do this, but it get's the job done.

@infogulch That was the one indeed, updated this comment.

infogulch commented 6 years ago

@kayvanbree did you mean to link to a tutorial? Both links point to the same library. 😄 Edit: I think it's this one. Thanks for the post!

jensbodal commented 6 years ago

I see this discussion is still going, is there a reason the best option is not to simply fetch the config prior to bootstrapping? For AoT builds to have a runtime provided configuration file that is environment-agnostic you need to asynchronously request the config before your application bootstraps. You would need to make this file available at the same relative path in all environments.

Or, you make your config service return a promise for the config file and then if the config isn’t loaded, load it and cache it then return it. All downstream calls relying on the config would then also need to be async (not ideal). Again the relative path of the config file would need to be the same in each environment.

infogulch commented 6 years ago

fetch the config prior to bootstrapping

Yes, but that's still pretty broad. So the question is now: When exactly are you fetching it?

  1. After your entire bundle is downloaded, parsed, and execution runs to the point where it realizes it has no config whereupon it makes a separate full-length network round trip to fetch it (and twiddles it's thumbs while it's waiting).
  2. As soon as the browser loads the http resource and gets to <script>/assets/static-environment-file.js?

Option 2 is better than 1, but still not perfect. But there's actually a 3rd option here. The solution that kayvanbree wrote up, using kyubisation's angular-server-side-configuration, is to patch the index.html file once on server start to embed the full environment configuration directly in the first file that the browser downloads. No separate network requests at all, in exchange for a bit messier startup process for your docker container. This docker image is old but appears to be in the same spirit, except doing it with a shell script.

Honestly it would be nice if nginx could do this directly without a separate startup step.

jensbodal commented 6 years ago

The main.ts file is typically the initial entry point to an angular application. If load times are a concern you’re already lazily loading modules so the bare minimum is fetched in order to render your initial app. An environment specific config file would be fetched prior to the first render. So for option 1, it happens in main: the config service is hydrated and injected into the main app module prior to bootstrapping.

My understanding is that solution option 3 that was proposed is not AoT compatible.

kyubisation commented 6 years ago

I'm currently working on implementing a solution for AoT. This is not trivial due to the folding by the AoT compiler, but I have an approach that seems to work. I also want to provide a CLI for an easier usage on a potential server.