Skip to content

Support multiple environments in the same build #3855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
jnizet opened this issue Jan 4, 2017 · 65 comments
Open

Support multiple environments in the same build #3855

jnizet opened this issue Jan 4, 2017 · 65 comments

Comments

@jnizet
Copy link
Contributor

jnizet commented Jan 4, 2017

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
Copy link
Member

clydin commented Jan 4, 2017

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
Copy link
Member

clydin commented Jan 4, 2017

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

@jnizet
Copy link
Contributor Author

jnizet commented Jan 4, 2017

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
Copy link
Member

clydin commented Jan 4, 2017

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
Copy link
Contributor

hansl commented Jan 4, 2017

@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
Copy link
Member

clydin commented Jan 4, 2017

@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
Copy link
Contributor

hansl commented Jan 4, 2017

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
Copy link
Contributor Author

jnizet commented Jan 4, 2017

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

  • loop through my supported locales in the gradle build,
  • for each locale, delete the dist directory, then call angular-cli with the appropriate locale-specific options,
  • copy the dist directory elsewhere and rename index.html to index-${language}.html
  • generate my final jar file by bundling all the files that the various angular-cli builds have generated

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
Copy link
Contributor Author

jnizet commented Jan 4, 2017

@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
Copy link
Contributor

filipesilva commented Jan 4, 2017

@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
Copy link
Contributor Author

jnizet commented Jan 5, 2017

@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
Copy link
Contributor

@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:

  • environments/environment.dev.ts
  • environments/environment.prod.ts
  • environments/environment.en.ts
  • environments/environment.fr.ts

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
Copy link
Contributor Author

jnizet commented Jan 5, 2017

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

@intellix
Copy link
Contributor

intellix commented Jan 9, 2017

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
Copy link
Contributor

@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
Copy link
Contributor

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
Copy link

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
Copy link
Contributor

@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
Copy link

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.

@kevcjones-archived
Copy link

TBH in our situation i'm about to run into this problem - we have 2 production servers - a staging and a live. Given Angulars predilection for build for an environment i have to build what is basically the same app twice. It takes about 4 minutes to build each one... and my entire build process is only 15 minutes in total.. so thats a pretty sizeable % i'm spending.

I quite like Quang's option here. We're only deploying to an S3 bucket... so not really serving up right now to get clever with the server-side ideas.. and i want to be able to have this environment ready before the angular apps gets going.... by being a local file i replace during the deployment perhaps i can simply map it over the previous environment.... should work... saves me some time...

Am i missing any down times, these files are so small a sync download really isn't going to be an issue.

@filipesilva filipesilva added the feature Issue that requests a new feature label Oct 3, 2019
@ngbot ngbot bot modified the milestones: needsTriage, Backlog Oct 3, 2019
@patrickmichalina
Copy link

I wrote a solution for Angular Universal apps located at ng-env-transfer-state. It won't help directly with client only builds, but might help some others who are using Angular Universal.

@rohithgopip
Copy link

I am working on a project which is not yet live. The application has a quarterly release cycle and each quarter will have to do 10 different builds to ship it to customers. Unfortunately, the application is not deployed in cloud. Its a real challenge now.

@tfrijsewijk
Copy link

tfrijsewijk commented Jan 29, 2020

I made this a while back:

Dockerfile

FROM node:10 as build

WORKDIR /app

COPY package.json /app/package.json
COPY yarn.lock /app/yarn.lock
RUN yarn --frozen-lockfile

COPY . /app
RUN yarn build --prod --output-path=dist

FROM nginx:stable-alpine

WORKDIR /usr/share/nginx/html

RUN apk add gettext

COPY --from=build /app/dist .

EXPOSE 4200

CMD envsubst < index.html > index.html && nginx -g "daemon off;"

index.html:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>RvsWebPoc</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">

  <script>
    window.env = {
      apiUrl: '${API_URL}'
    };
  </script>
</head>
<body>
  <app-root></app-root>
</body>
</html>
import { Injectable } from '@angular/core';

@Injectable()
export class Settings {
  constructor() {
    const env = (window as any).env;

    this.apiUrl = env.apiUrl !== '' && env.apiUrl !== '${API_URL}'
      ? env.apiUrl
      : '//localhost:3000';
  }

  apiUrl: string;
}
  1. Inject settings: Settings and use settings.apiUrl.
  2. Start the container docker run --env API_URL="https://api.example.com" my-image

Notes:

  • I now used a specific variable apiUrl, but you could make it iterable (for each property convert to UPPER_CASE and check if variable is set or else fall back to default)
  • Perhaps Settings should actually be named Environment
  • I got the idea of someone who actually made a whole site dedicated to configurating your application by changing the index.html file, but I can't find it anymore. I'm also not sure how much of the above setup is my idea. If I completely stole it, credits to the original author :-) If I find the site again I'll come back on this

@Echarnus
Copy link

This really should been made possible. It's ridiculous you have to trigger new builds after config changes. Neither is this correct when using build systems such as Azure Devops.

@dgp1130
Copy link
Collaborator

dgp1130 commented Jun 12, 2020

/cc @aikidave, see docs point at the end.

We discussed this amongst the CLI team and wanted to share our thoughts about this particular FR. We definitely understand the desire to manage multiple configurations within a single build, particularly when most DevOps workflows today encourage promoting a single build over re-building per environment.

The Angular CLI itself is fundamentally a build-time (and developer convenience) tool. We compile the application and output a dist/ directory that has all the resources necessary to run the application. The CLI's job ends here. It is then up to the application developer to actually serve these resources to end users (via Nginx, Express, CDN, or any other server their heart desires).

The core request of "Multiple environments in the same build" conflicts with this design. The CLI can only apply build-time operations; so generating multiple builds is easy (ie. for internationalization), but when trying to output a single build that supports multiple runtime configurations, our options are inherently limited. Because the CLI only outputs a build, we don't actually have any hooks into the runtime server architecture that would allow us to propagate environment data or even just serve a particular JSON file for a particular environment. As a result, there's not much the CLI can do here which would be particularly helpful.

There are a few ways applications can accomplish what they want without requiring special support in the CLI.

Asset JSON Files

Assets are included in builds as static files and will be available across all environments. These assets could simply be JSON files which include all the relevant metadata for an application. You could then create a ConfigService or define an APP_INITIALIZER which fetches the right JSON file for your environment based on query parameters or the host URL. This guide in particular breaks down compile-time vs run-time configuration and how to use APP_INITIALIZER to fetch asset JSON at startup.

While this allows different configurations across environments, the main limitation here is that all the configuration data must be static and known at build-time. This also requires an additional network request at application start, which can be bad for performance (though HTTP2 server push could help a lot with that). All the files are also available across all environments, so you shouldn't have sensitive data in here such as a dev instance API key. Alternatively you could hack the server to hide certain files in certain environments or alter the responses to requests for asset JSON files, though in that scenario I would probably recommend the second approach...

Fetch HTTP Endpoint

Because the previous suggestion must have static data, what about loading dynamic data? In that scenario, asset files would not be sufficient, but a traditional HTTP endpoint would serve that purpose. It could dynamically choose which environment to use and read the relevant data from whatever database/static files/magnetic tape backend it has. This could also be wrapped in a service which fetches this data from the HTTP endpoint, or it could be loaded with APP_INITIALIZER to be included on startup.

This requires backend support and is a bit of a "heavy-weight" option as a result. The biggest downside is that it still requires a subsequent HTTP request on startup, which can be sub-optimal for performance.

Universal

Angular Universal is in the unique position of actually running server-side code as part of the Angular and does have the relevant hooks to inject environment information. A few libraries are already suggested in this issue which can handle this particular problem. Even without a library this could be done by server-side rendering a <script /> tag with window.appConfig = Object.freeze({ /* environment */ }). Then the Angular app could read the global value when needed (or wrap it in a service).

Universal is more intended for server-side rendering, which isn't totally related to this FR, but it can definitely be made to work with it. This approach does require an application to set up Universal, and using SSR may not be desired in the first place, as environment configuration is a tangential concern. Also using SSR on executable JavaScript is a bit iffy on the security perspective and could easily become an XSS attack vector. The safer option would just be to serve a dynamic JSON response per the second suggestion (Fetch HTTP Endpoint).

Hopefully these suggestions provide some ideas of how to handle loading multiple configurations within a single build. At the end of the day, there's not a whole lot the CLI can do here as a simple build tool. We do see some opportunities here to improve our documentation. Most of the concerns raised in this issue are pretty common use cases (use an API key from environment variable, promote a single build in a DevOps pipeline, feature flags, etc.). Some comprehensive docs on how build-time vs run-time configuration works in Angular and workflows/recipes for these common use cases would likely be very helpful for developers struggling to find the best way of supporting such common requirements. Switching this to a docs issue for further follow up.

@dgp1130 dgp1130 added area: docs Related to the documentation and removed needs: discussion On the agenda for team meeting to determine next steps feature Issue that requests a new feature labels Jun 12, 2020
@kyubisation
Copy link
Contributor

@dgp1130 Thank you for the extensive comment. While I understand the apprehension of adding additional complexity to the CLI, I still have to ask; I am the author of angular-server-side-configuration (which falls into the first paragraph of the Universal section of your answer) and it achieves most of the desired functionality in about 1250 loc (including tests and ng-add, ng-update schematics, and an additional 400 for the runtime CLI written in Go). Is that really too much complexity to have in the Angular CLI?

@dgp1130
Copy link
Collaborator

dgp1130 commented Jun 15, 2020

@kyubisation, it's not about introducing too much complexity, it's about the abstractions provided by each aspect of the Angular toolchain and where a given feature makes the most sense to implement.

Since the CLI is a build tool, it simply outputs a directory containing the application. At that point the CLI's job is done, so further manipulating that build to work with multiple environments is outside the scope of the CLI. By contrast, Universal is intended to enable server-side rendering support for a provided Angular build (which arguably could include server-side "rendering" of environment values).

If you want direct support for a system similar to what your library provides, then that would make more sense as a FR to Universal than it does to the CLI. The CLI as it stands today, just isn't the right tool to solve this particular problem.

@tillkuhn
Copy link

@tfrijsewijk Many thanks, I have just applied your environment based Injectable approach and it works perfectly. Just note that with recent versions of Alpine base images, for reasons beyond my knowledge you can not longer use the "inplace" envsubst pattern (which will result in an empty file), but need to create a temporary copy, i.e ...

CMD ["sh","-c", "cp /www/index.html /www/index.html.tmpl && \
      envsubst '$RUNTIME_VAL' </www/index.html.tmpl >/www/index.html  (...)

@csisy
Copy link

csisy commented Sep 6, 2022

Is this related to #10612 ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests