Skip to content
This repository has been archived by the owner. It is now read-only.

Environment proposal #762

Closed
danbucholtz opened this issue Feb 17, 2017 · 123 comments
Closed

Environment proposal #762

danbucholtz opened this issue Feb 17, 2017 · 123 comments
Milestone

Comments

@danbucholtz
Copy link
Contributor

@danbucholtz danbucholtz commented Feb 17, 2017

I am thinking we could do this:

  1. Introduce a new flag that gets passed in like this:

ionic serve --env qa or ionic run android --env prod

My first thought was that it's value would default to dev for non-prod builds, and prod for prod builds. Developer's can pass in whatever they want, though.

  1. Based on the value that is passed in (or inferred), the config section of the package.json is read. It could look something like this:
...

"config": {
   "ionic_env" : {
       "dev" : {
          "keyToReplace" : "devValueToReplaceWith",
          "keyTwoToReplace" : "devValueTwoToReplaceWith"
       },
       "qa" : {
          "keyToReplace" : "qaValueToReplaceWith",
          "keyTwoToReplace" : "qaValueTwoToReplaceWith"
       }
   }
},
...

If the ionic_env data is not there, we would just move on in the build process. If it is present, we would then perform the text replacement.

  1. Any computed values, sync or async, could be replaced after the build is done. We could either leave this up to the user to add on to the npm script section, or we could provide a hook into the postprocess step. I prefer the latter as it's easier to document and and we can probably make it a 1/2 second faster or so if we do it in app-scripts.

Feedback is appreciated.

Thanks,
Dan

@fiznool
Copy link
Contributor

@fiznool fiznool commented Feb 17, 2017

Looks interesting! I have a few questions.

Any computed values, sync or async, could be replaced after the build is done.

Could you expand on this? I didn't quite understand what you mean.

How would the app use these variables, what would the code look like?

One of the comments in another thread mentioned keeping sensitive values out of source control. Could there be a way of reading in a value from a process.env variable to satisfy this requirement?

@rolandjitsu
Copy link
Contributor

@rolandjitsu rolandjitsu commented Feb 17, 2017

@danbucholtz this looks great. Angular CLI has a similar implementation.

They have a --target which you can use to tell the CLI to either do a production build (AOT, minification, gzip, etc.) or a dev build. Then there's the --environment flag which we can use to choose the appropriate environment.

I'd disagree with the ionic_env working as proposed though. As you also pointed out, there might be a case when we need to compute some values, and that'd be difficult for some users to figure out. How about the following ionic_env:

"ionic_env": {
    "prod": "config/env.prod.js",
    "staging": "config/env.staging.js",
    "dev": "config/env.dev.js"
}

And each file would export the following:

const somePackage = require('somePackage');

module.exports = {
    SOME_VAR: somePackage.getSomeValueSync(),
    SOME_ASYNC_VAL: async function getSomeValueAsync() => {
        await asyncValue = somePackage.computeSomeValueAsync();
        return asyncValue;
    },
    VALUE_FROM_PROCESS: process.env.SOME_ENV_VAR_VAL
};

Then @ionic/app-scripts would require the appropriate env based on the config from ionic_env and for each key it will:

  • check if the value is primitive and use that if so
  • or if it's a function it would invoke and
    • check if the return value is primitive and use it if so
    • if it's a Promise or an Observable, wait until it's resolved or emits and return that
  • then after it successfully computed each value, it would replace where necessary

Perhaps there could be a better approach, but I thing this would work in most scenarios.

@fiznool
Copy link
Contributor

@fiznool fiznool commented Feb 17, 2017

Perhaps I'm missing something, but what sort of use cases would benefit from async computation of values?

@rolandjitsu
Copy link
Contributor

@rolandjitsu rolandjitsu commented Feb 17, 2017

Reading a file for instance (that's also possible to do sync). Or perhaps you store some keys in an AWS bucket and you'd like to pull those keys when you compile the app. I could think of plenty of use cases.

@fiznool
Copy link
Contributor

@fiznool fiznool commented Feb 17, 2017

Right, I'm with you now. 👍

One approach I've seen in other places is to allow exporting a function as well as an object inside the configuration file. This could simplify the logic from the app-scripts end and allow you to build the config object dynamically, and only return once you are ready.

Something along these lines:

// sync example
const somePackage = require('somePackage');

module.exports = {
  SOME_VAR: somePackage.getSomeValueSync();
  VALUE_FROM_PROCESS: process.env.SOME_ENV_VAR_VAL;
};
// async example
const somePackage = require('somePackage');

module.exports = function() {
  const config = {
    SOME_VAR: somePackage.getSomeValueSync();
    VALUE_FROM_PROCESS: process.env.SOME_ENV_VAR_VAL;
  };
  
  return somePackage.computeSomeValueAsync().then(val => {
    config.SOME_ASYNC_VAL = val;
    return config;
  });
};

app-scripts would then perform the following

  • If the export is an object, assume it is the config object and use it.
  • If the export is a function, assume it will return a promise. Call it, wait for it to be resolved and use the resolved value as the config object.

This has the benefit that app-scripts doesn't need to enumerate all the keys in the config object (what about nested keys?) and provides the most flexibility in your config script (what if the fetching of a second async value depends upon another one being resolved first?)

@rolandjitsu
Copy link
Contributor

@rolandjitsu rolandjitsu commented Feb 17, 2017

@fiznool true, that'd be much simpler and it would help if a value depends on another one. But the app scripts would still need to enum keys in order to replace, but that's just implementation detail.

@rolandjitsu
Copy link
Contributor

@rolandjitsu rolandjitsu commented Feb 17, 2017

Though I'd actually argue that if we export an object, app scripts could potentially run async tasks in parallel so if we'd have more than one property that evaluates to a promise/observable, it could run all of them at once and complete when all are done (something like a Promise.all([ ... ])/Observable.merge(...)).

Yet, the user could do this as well, but if app scripts would do it, it would spare the user of adding extra implementation.

I guess there are many ways this can be tackled, but the simpler the better.

@fiznool
Copy link
Contributor

@fiznool fiznool commented Feb 17, 2017

@ehorodyski
Copy link

@ehorodyski ehorodyski commented Feb 17, 2017

I like where @rolandjitsu is going, but instead of tying it down to a specific implementation, is there a way you can take the environment path being used from package.json in the ionic_env object, and then alias it, so it's import { <T> } from @ionic/config or import { <T> } from @ionic/env?

I don't know how the Angular library does aliasing so you can do import { HTTP } from '@angular/http', instead of import { HTTP } from 'angular/modules/wherever/it/lives/http', but that's how I'd implement it.

Give the users the ultimate control of building what they want to export from their environment/config files, but provide a way for those paths to be swapped out during building, and I think properly creating a module alias, which I don't know how to do, is the best approach.

@danbucholtz
Copy link
Contributor Author

@danbucholtz danbucholtz commented Feb 17, 2017

If we were to expose a hook and allow the user to load in async values, perhaps we could do something like this:

"config" : {
   "ionic_env" : {
      "dev" : "./scripts/dev.config.js",
      "qa" : "./scripts/qa.config.js",
      "prod" : "./scripts/prod.config.js"
      ... etc ...
   }
}

Those modules could then export a function that returns a Promise. That way it will always be async.

It could work like this:

module.exports = function(ionicEnvironment: string) {
   return readSomeAsyncValue().then((result: string) => {
      return {
         'keyOneToReplace' : 'valueOne',
         'keyTwoToReplace' : result
      }
   });
}

Something like this would work in the vast majority of use cases.

In an application's code, let's say you need to hit a different HTTP service for development and production.

You could write a service like this:

export class MyService {
   construtor(public http: Http){
   }

   makeServiceCall() {
      return this.http.makeRequest('$BACKEND_SERVICE_URL');
   }
}

Your implementation to replace it could look like this:

module.exports = function() {
   return Promise.resolve({
      '$BACKEND_SERVICE_URL' : 'localhost:8080/myService'
   });
}
@ehorodyski
Copy link

@ehorodyski ehorodyski commented Feb 17, 2017

@danbucholtz - I think that is really over-complicated.

Take a look at Webpack's resolve aliasing. This could be done dynamically when starting ionic serve or the other scripts, depending on the env variable:

alias: {
Utilities: path.resolve(__dirname, 'src/utilities/'),
Templates: path.resolve(__dirname, 'src/templates/')
}
Now, instead of using relative paths when importing like so:

import Utility from '../../utilities/utility';

you can use the alias:

import Utility from 'Utilities/utility';

The only implication to the user is that they'd need to store their config files outside the folder with their source code, as to not include all the files defined. Or maybe not, you can run a glob that excludes the other environment files not being used. Plus, there's no resolution during application running.

@josh-m-sharpe
Copy link

@josh-m-sharpe josh-m-sharpe commented Feb 17, 2017

For us this isn't necessarily just about env=dev/staging/prod. For some context, we're coming to ionic(v2) from a vanilla cordova app and we use the same codebase to generate multiple different apps primarily distinguished by an 'org_id'. (The app passes this value in server calls to receive different content, amongst a bunch of other differences). We also currently inject the internal and external version numbers since they vary (inconsistently) by org_id.

So, in a nutshell, just passing in "environment" wouldn't solve this problem for us. Why not make this feature align better with unix best practices and simply make all of the current env available inside the application somehow?

for example:

ORG_ID=123 ENVIRONMENT=staging ionic serve

For what it's worth, cordova (or node?) is doing this already automatically. That is, you can access 'process' inside a cordova hook without having to require anything. From that, it's just process.env.ORG_ID == '123'

@rolandjitsu
Copy link
Contributor

@rolandjitsu rolandjitsu commented Feb 17, 2017

@danbucholtz I agree, that'd be a reasonable solution. And I'm sure that it would cover most use cases.

@ehorodyski I think it's already possible to use aliasing if you use your own webpack config file (I cannot confirm it, never tried it). But I do see what you mean, though that would not work if you have async operations that need to run in order to compute the env values you want to have available in the app. It would also not work for the use case @josh-m-sharpe describes where you need some env vars that are not exposed by app scripts.

@josh-m-sharpe using the solution proposed would also solve what you're referring to. You'd just need to expose from process.env:

module.exports = function() {
    // process.env is an object and has all the env vars
    return Promise.resolve(process.env);
}

I think it's essential to have a more generic and universal solution, such as the one proposed by @danbucholtz in #762 (comment), that can cover most of the uses cases since the dev needs vary as we can see from the posts on this topic.

@biesbjerg
Copy link

@biesbjerg biesbjerg commented Feb 17, 2017

Am I the only one that really wants hinting of config keys, e.g. by defining a ConfigInterface? I have not seen it mentioned and I'm not sure how to do it elegantly, though...

@ehorodyski
Copy link

@ehorodyski ehorodyski commented Feb 18, 2017

@biesbjerg - Right, this is what I'd like accomplished as well. I need to brush up on how to do Typescript module aliasing, so that the paths can be dynamically generated when you run ionic serve.

If ionic-app-scripts doesn't want to implement something like this, I think it would be great to use for yourself. It's really not hard, I've done it before.

@coldAlphaMan
Copy link

@coldAlphaMan coldAlphaMan commented Feb 18, 2017

@danbucholtz all of the other stuff people are mentioning are great nice to haves. That said, i think your first comment is awesome enough and will suit most needs.

@fiznool
Copy link
Contributor

@fiznool fiznool commented Feb 19, 2017

@danbucholtz

You could write a service like this:

export class MyService {
   construtor(public http: Http){
   }

   makeServiceCall() {
      return this.http.makeRequest('$BACKEND_SERVICE_URL');
   }
}

Your implementation to replace it could look like this:

module.exports = function() {
   return Promise.resolve({
      '$BACKEND_SERVICE_URL' : 'localhost:8080/myService'
   });
}

Are you suggesting that '$BACKEND_SERVICE_URL' becomes a sort of 'magic string' that is replaced during compile time by the contents defined in the environment config file?

If so, I think this might be a bit confusing and difficult to maintain.

As others have suggested, an ideal solution would be something along the lines of the following, which could be achieved in webpack using resolve aliasing:

import { BACKEND_SERVICE_URL } from 'config';

export class MyService {
   construtor(public http: Http){
   }

   makeServiceCall() {
      return this.http.makeRequest(BACKEND_SERVICE_URL);
   }
}

I'm not sure how the above could be achieved with rollup. If this is a concern, a slightly lesser (but still very workable) version would be to add all config parameters to a global variable, e.g. ENV. With webpack, this could be achieved using the define plugin, and with rollup you could use the rollup replace plugin.

declare const ENV;

export class MyService {
   construtor(public http: Http){
   }

   makeServiceCall() {
      return this.http.makeRequest(ENV.BACKEND_SERVICE_URL);
   }
}

For those wishing for TypeScript autocomplete, ENV could instead be declared in src/declarations.d.ts as follows:

declare namespace ENV {
  const BACKEND_SERVER_URL: string;
  // Any other properties go here
}
@danbucholtz
Copy link
Contributor Author

@danbucholtz danbucholtz commented Feb 20, 2017

The resolve aliasing is interesting and wouldn't really require Webpack or Rollup to do it. From our perspective, a bundler is just a bundler, not a build tool. We don't dive into the world of plugins unless absolutely necessary, because they tighten our coupling to the tool. In our ideal world, we're going to swap out Webpack for a better bundler someday without 90% of our developers even noticing.

I think the aliasing leaves many use-cases unfulfilled where you need to load asynchronous data prior to a build. I guess you could do it ahead of time in a separate workflow/process and put it in an environment variable? I'm not sure, this seems a bit complicated to me.

I'll chat with some other team members this week and we'll figure out what we want to do. Any of these solutions will cover the vast majority of use cases.

Thanks,
Dan

@Ross-Rawlins
Copy link

@Ross-Rawlins Ross-Rawlins commented Feb 20, 2017

+1

@ehorodyski
Copy link

@ehorodyski ehorodyski commented Feb 20, 2017

I think the aliasing leaves many use-cases unfulfilled where you need to load asynchronous data prior to a build. I guess you could do it ahead of time in a separate workflow/process and put it in an environment variable? I'm not sure, this seems a bit complicated to me.

Could you provide some use-cases for where you'd load async data prior to a build -- that would be environmental configurations?

Maybe there's a disconnect in what certain people are talking about. In my view, and many others, this is analogous to having .properties files in a Java project. They'd stay in the repo and only the specific file would get included in the build. Would the Promise resolutions happen during run-time?

@riltsken
Copy link

@riltsken riltsken commented Feb 20, 2017

Just my perspective as a user, but I pretty much use environment variables for any configuration changes required among different environments or deploys. This means I can change a configuration value for prod, staging, dev, individual branch builds ... any N+ environments. Supporting N+ environments is easy with environment variables. Just use one template. When talking about property files though it becomes harder because now you need a property file per environment or to generate a property file per environment.

I'll post my specific project use cases that we have currently:

maxCacheAgeInDays
ionicCloudChannel
apiUrl
oauthClientId
logLevel
logentriesToken
googleMapsJavascriptApiUrl

In the individual threads I see this asked more than once for environment variable support. I could generate a property file I guess at buildtime, but that would basically be me duplicating what I am today in a different form. Below are some references to what I would consider are analogous solutions for the same problem.

references:

Anyway just some thoughts. I would really love environment variable support :).

@vovikdrg
Copy link

@vovikdrg vovikdrg commented Feb 21, 2017

Angular-cli has implemented and its easy to configure it in angular-cli file why not to do the same?

https://github.com/angular/angular-cli/blob/3c3f74c060cc79f3872230a18eb0c1dd8371065f/packages/%40angular/cli/blueprints/ng2/files/angular-cli.json#L24

@stevek-pro
Copy link

@stevek-pro stevek-pro commented Mar 8, 2017

We want this too!

@juarezpaf
Copy link

@juarezpaf juarezpaf commented Mar 8, 2017

We're currently using a script that runs before ionic serve and generate the correct file for us.
Let me explain it in more details here:

  1. We created an env folder in the root of the Ionic project and added this folder to our .gitignore
  2. Inside of the env folder we have different files:
./env
  production.json
  sandbox.json
  staging.json
  1. Each file has the variables we need, for example, our staging.json file looks like:
{
  "api_url": "'https://staging.api.url'",
  "client_id_login": "'566d41f...'",
  "client_secret_login": "'511480878...'",
  "google_maps_key": "'tEkoJK...'"
}
  1. We changed the scripts section inside of the package.json file to have a preionic phase and also an independent command:
"scripts": {
    "ionic:build": "ionic-app-scripts build",
    "ionic:serve": "ionic-app-scripts serve",
    "preionic:build": "node ./scripts/replace.env",
    "preionic:serve": "node ./scripts/replace.env",
    "generate-env": "node ./scripts/replace.env",
    "test": "ng test"
}
  1. Our replace.env.js file handles the generations of our config.ts file https://gist.github.com/juarezpaf/7a9007dfcef25ec24f7de900df8fe04e
  2. The config.ts.sample is very simple and it'll just expose these variables to use use across our app
export class AppConfig {
  static get API() {
    return ENV.api_url;
  }

  static get GOOGLE_MAPS_KEY() {
    return ENV.google_maps_key;
  }

  static get CREDENTIALS() {
    const credentials = {
      'login': {
        'client_id': ENV.login.client_id,
        'client_secret': ENV.login.client_secret
      }
    }
    return credentials;
  }
}
  1. We use the AppConfig in our AuthService:
import { AppConfig } from '../config/config';

@Injectable()
export class Auth {
  credentials: any;
  private apiUrl = AppConfig.API;
  
  constructor(public http: Http) {
    credentials.client_id = AppConfig.CREDENTIALS.login.client_id;
    credentials.client_secret = AppConfig.CREDENTIALS.login.client_secret;
  }
}
  1. With everything in place we can use it in 3 different ways:
  • npm run generate-env --sandbox
  • ionic serve --staging
  • ionic build:ios --production

I found some cons with this replace.env.js approach:

  • Local files with tokens and other sensitive info
  • If we need other environments we need to change this file;
  • Write the variables into a the config.ts file every time;
@jpbrown250
Copy link

@jpbrown250 jpbrown250 commented Mar 31, 2017

Hey guys, I have been working all day on a temporary solution to this thread that should work for most of the use cases I have read in this and a few other threads of the same topic. The main concerns I noticed form suggestions include:

  1. Fear of an update that could break a solution
  2. Unable to use environment variables
  3. Required changing the text in a file to switch environments
  4. Required cardinal knowledge of the script and how it functions (personally)

My project should solve all of those issues. If there are any others I am happy to work on it.
Git Repository

I have it fairly well spelled out in the README.md.

@emcniece
Copy link

@emcniece emcniece commented Nov 17, 2017

#762 (comment) (@mlegenhausen ) is AOT-compatible, which is a crucial feature for me. I am including environment-specific variables in the main @NgModule declaration:

@NgModule({
    imports: [
        CommonModule,
        BrowserModule,
        HttpModule,
        PagesModule,
        ComponentsModule,
        StoreModule.provideStore(reducer),
        EffectsModule.run(DeviceEffects),
        IonicModule.forRoot(AppComponent,
            // Disable page transitions for screenshot task
            {animate: environment.enableAnimation}
        ),
       ....

This can only be done by including a non-NgModule component - ngc fails if you use a solution like #762 (comment) (@juarezpaf).

@Daskus1
Copy link

@Daskus1 Daskus1 commented Dec 12, 2017

Are we going to see this feature any time soon?

@luckylooke
Copy link

@luckylooke luckylooke commented Dec 14, 2017

Angular CLI has parfect solution, which best fits my needs, see here. It would be very nice to have same mechanism in ionic projects. Thanks ;)

@danbucholtz
Copy link
Contributor Author

@danbucholtz danbucholtz commented Dec 15, 2017

We put this feature on hold because we are moving to the Angular CLI 🎉 .

Thanks,
Dan

@keithdmoore
Copy link

@keithdmoore keithdmoore commented Dec 19, 2017

@danbucholtz Any idea as to when the ionic-cli will be replaced by the Angular-CLI?

@danbucholtz
Copy link
Contributor Author

@danbucholtz danbucholtz commented Dec 21, 2017

As soon as possible. No date yet but it will coincide with Ionic 4.

Check out the core branch of the Ionic repo if you're interested in following along.

Thanks,
Dan

@keithdmoore
Copy link

@keithdmoore keithdmoore commented Dec 22, 2017

Thanks. That helps. I will need to implement something prior to that.

@emcniece
Copy link

@emcniece emcniece commented Dec 22, 2017

@keithdmoore:

  1. Extend webpack.config.js, inject your own customEnvPlugin:
// Extends ./webpack.default.config.js
// which is a direct copy of node_modules/@ionic/app-scripts/config/webpack.config.js
//
// We force devs to copy/paste to webpack.default.config.js so that upgrading
// @ionic/app-scripts is explicit, intentional, and reviewed.
//
// Added minimist, customEnvPlugin

var defaultConfig = require('./webpack.default.config.js');
var webpack = require('webpack');
var path = require('path');
var fs = require('fs');
var chalk = require('chalk');

var argv = require('minimist')(process.argv.slice(2));

// Build environment vars
var customEnvPlugin;

if(!!argv.env){
  fileName = './src/environments/environment.'+argv.env+'.ts';
  var fileExists = fs.existsSync(fileName);

  if(fileExists){
    console.log(chalk.bgGreen('CONFIG LOADED: ', fileName));

    customEnvPlugin = new webpack.NormalModuleReplacementPlugin(
      /src\/environments\/environment\.ts/,
      path.resolve('./src/environments/environment.'+argv.env+'.ts')
    );
  } else {
    console.log(chalk.bgRed('CONFIG SPECIFIED BUT MISSING: ', fileName));
    process.exit();
  }
} else {
  console.log('CONFIG DEFAULT: ', './src/environments/environment.ts');
  customEnvPlugin = new webpack.NormalModuleReplacementPlugin(
    /src\/environments\/environment\.ts/,
    path.resolve('./src/environments/environment.ts')
  );
}

defaultConfig.prod.plugins.push(customEnvPlugin);
defaultConfig.dev.plugins.push(customEnvPlugin);

module.exports = defaultConfig;
  1. Create src/environments/environment.ts, place your variables:
// Dev profile: animations & devtools

export const environment = {
  production: false,
  enableAnimation: true,
  enableDevTools: true,
  settings: {}
};

2.5 Clone src/environments/environment.ts to src/environments/environment.dev.ts or src/environments/environment.prod.ts as needed. The .dev and .prod are used in CLI

  1. Import into modules as needed. Example: src/app/app.module.ts:
...
import { environment } from '../environments/environment';
...

@NgModule({
    imports: [
        CommonModule,
        BrowserModule,
        HttpModule,
        PagesModule,
        ComponentsModule,
        ...
        IonicModule.forRoot(AppComponent,
            // Disable page transitions for screenshot task
            {animate: environment.enableAnimation}
...
  1. Run via CLI with flag: ionic serve -b --env=dev

Credits: this thread and https://www.williamghelfi.com/blog/2017/06/22/ionic-environments-webpack/

@mlegenhausen
Copy link

@mlegenhausen mlegenhausen commented Dec 22, 2017

I have to step back from my solution all these webpack solutions fail when you run the production build (--prod). The problem is that ngc is executed before the webpack configuration does happen. That means that during the static type analyses your default environment.ts is used and statically compiled in your application. After that webpack is executed and your environment.ts is replaced by e. g. environment.prod.ts. Now do you have following problem. When you try console.log(environment) you see your production configuration, but because ngc deconstructs your environment object during the compilation step everywhere where you have used an environment variable in your @NgModule configuration it stayes the default configuration. This error is hard to track down so I would not recommend to replace any module via webpack.

My recommendation is to use the angular cli with ionic when you need environment file support!

@emcniece
Copy link

@emcniece emcniece commented Dec 22, 2017

@mlegenhausen make the prod config the default environment file? Then when not using ngc (ie. non-production) you can specify --env=devand let webpack rewrite without issues.

To be clear, I have 3 environment files:

  • environment.ts
  • environment.prod.ts
  • environment.dev.ts

The first 2 files are identical - I include the environment.prod.ts for verbosity if anyone checks them out. This is working well for me so far.

@mlegenhausen
Copy link

@mlegenhausen mlegenhausen commented Dec 22, 2017

This does only work when you have only one production version. I need to configuer differntly for android ios and web.

@GFoley83
Copy link

@GFoley83 GFoley83 commented Feb 22, 2018

@emcniece @gshigeto Do your solutions support using a custom environment file e.g. environment.test.ts when using ngc? If not what modification would be needed?
I'd like to be able to build APKs for the various environments e.g. ionic cordova build android --test.

@emcniece
Copy link

@emcniece emcniece commented Feb 22, 2018

@GFoley83 Yes, AOT (ngc) compilation works well with this pattern.

The downside of the provided example is that "production" builds end up with double flags, but that's not the end of the world. Here's a sample of my package.json scripts:

"build-aot": "ionic-app-scripts build --release --prod --env=prod",
"production-ios": "ionic cordova build ios --buildConfig ./config/build-ios.json --release --prod --env=prod",
"production-android": "ionic cordova build android --release --prod --env=prod",
"production-browser": "ionic cordova build browser --release --prod --env=prod",

The --release and --prod flags are for Ionic. The --env=prod is the custom flag for the provided example Webpack config modification, which matches the ./src/environments/environment.prod.ts file. Using --prod or --release compiles the code with ngc, not sure exactly which one.

Oh, again as a disclaimer, that is totally not "my" solution, it came from William Ghelfi.

@bladerunner41
Copy link

@bladerunner41 bladerunner41 commented Feb 22, 2018

@GFoley83
Copy link

@GFoley83 GFoley83 commented Feb 22, 2018

I ended up using a combo of @emcniece and @gshigeto solutions, so massive thanks to them.

I wanted to have environment variables work in Ionic exactly like how they do in Angular CLI (especially given Ionic 4 is somewhere on the horizon), whilst also supporting Karma and Protractor tests.

To use the environment variables, just add import { environment } from '@env/environment';.
Also supports ngc so I can build an Android APK with any environment config e.g. my environment.test.ts config.

This solution also supports using ionic serve with any of your configurations e.g.
ionic serve --env=prod

I've managed all this with the following setup:

/src/app/environments folder contains

environment.model.ts
environment.ts
environment.prod.ts
environment.test.ts

/tsconfig.json

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "baseUrl": "./src",
    "declaration": false,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "paths": {   
      "@env/*": [
        "environments/*"
      ]
    },
    "lib": [
      "dom",
      "es2016"
    ],
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "target": "es5"
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "src/**/*.spec.ts"
  ],
  "typeRoots": [
    "node_modules/@types"
  ],
  "compileOnSave": false,
  "atom": {
    "rewriteTsconfig": false
  }
}

/config/webpack.config.js

var chalk = require("chalk");
var defaultConfig = require('@ionic/app-scripts/config/webpack.config.js');
var ionic_env = process.env.IONIC_ENV;
var fs = require('fs');
var path = require('path');
var env = require('minimist')(process.argv.slice(2)).env || process.env.IONIC_ENV || 'dev';
var webpack = require('webpack');

defaultConfig.dev.resolve.alias = {
    "@env/environment": path.resolve(environmentPath('dev'))
};

defaultConfig.prod.resolve.alias = {
    "@env/environment": path.resolve(environmentPath('prod'))
};

console.log(chalk.yellow.bgBlack('\nUsing ' + env + ' environment variables.\n'));

if (!!env) {
    var pathToCustomEnvFile = path.resolve(environmentPath(env));

    defaultConfig[env] = defaultConfig[ionic_env];
    defaultConfig[env].resolve.alias = {
        "@env/environment": pathToCustomEnvFile
    };

    var customEnvPlugin = new webpack.NormalModuleReplacementPlugin(
        /src\/environments\/environment\.ts/, pathToCustomEnvFile
    );

    defaultConfig.prod.plugins.push(customEnvPlugin);
    defaultConfig.dev.plugins.push(customEnvPlugin);
}

module.exports = function() {
    return defaultConfig;
};


function environmentPath(env) {
    var filePath = './src/environments/environment' + (env === '' ? '' : '.' + env) + '.ts';
    if (fs.existsSync(filePath)) {
        return filePath;
    }

    console.log(chalk.red.bgWhite('\n' + filePath + ' does not exist!\n'));
    process.exit();
}

/test-config/webpack.test.js

var webpack = require('webpack');
var path = require('path');
var fs = require('fs');
var chalk = require('chalk');

module.exports = {
    devtool: 'inline-source-map',

    resolve: {
        alias: {
            '@env/environment': path.resolve(environmentPath('dev')),
        },
        extensions: ['.ts', '.js']
    },

    module: {
        rules: [{
                test: /\.ts$/,
                loaders: [{
                    loader: 'ts-loader'
                }, 'angular2-template-loader']
            },
            {
                test: /\.html$/,
                loader: 'html-loader?attrs=false'
            },
            {
                test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
                loader: 'null-loader'
            }
        ]
    },

    plugins: [
        new webpack.ContextReplacementPlugin(
            // The (\\|\/) piece accounts for path separators in *nix and Windows
            /(ionic-angular)|(angular(\\|\/)core(\\|\/)@angular)/,
            root('./src'), // location of your src
            {} // a map of your routes
        )
    ]
};

function environmentPath(env) {
    var filePath = './src/environments/environment' + (env === '' ? '' : '.' + env) + '.ts';
    if (fs.existsSync(filePath)) {
        return filePath;
    }

    console.log(chalk.red('\n' + filePath + ' does not exist!\n'));
    process.exit();
}

function root(localPath) {
    return path.resolve(__dirname, localPath);
}

/package.json

....
        "ionic": "ionic",
        "build": "ionic build",
        "build:test": "ionic build --env=test",
        "build:prod": "ionic build --prod",
        "build:android": "ionic cordova build android --dev",
        "build:android:test": "ionic cordova build android --prod --env=test",
        "build:android:prod": "ionic cordova build android --prod",
....
@raajmayukh
Copy link

@raajmayukh raajmayukh commented Mar 15, 2018

@keithdmoore Thanks for your idea to fix this solution. I have followed your solution and able to load files as per what's been set as fileName. But with this solution is it possible to access process.argv.env in TS files anywhere in the application?

I need to access them in my app to load some Mock services for different environments. Earlier(Angular 4 and ionic app scripts 1.x) i was achieving this by setting process.argv.NODE_ENV. I was using Opaque token for the same. Now with Angular 5 and ionic app scripts 3.1.x i am using Injection token and trying to access process.argv.env together with your solution but it always comes as undefined in TS files. Any idea on this?

I had created an issue earlier in Ionic repo, probably should have created here: Here is the link with all info:

[(https://github.com/ionic-team/ionic-framework/issues/14150)]

@keithdmoore
Copy link

@keithdmoore keithdmoore commented Mar 16, 2018

I’m sure you could. I followed someone else’s suggestion above. I have a name attribute in my environment model that is the same as what is in the file name. I can access the name in my ts files if I need to.

@tabirkeland
Copy link

@tabirkeland tabirkeland commented Mar 16, 2018

All, with Ionic 4 release approaching, I know that ENV vars will be handled. If you are using Ionic 3.9.2, here is what I have been using for months to handle ENV vars. It's been pieced together from this and a few other threads.

https://gist.github.com/tabirkeland/a17c67b2f1ea3331d94db34ed7191c34

@writer0713
Copy link

@writer0713 writer0713 commented Apr 2, 2018

made a starter project with detail explanations.

https://github.com/writer0713/ionic-environment-setting/blob/master/README.md

@tabirkeland
Copy link

@tabirkeland tabirkeland commented Apr 3, 2018

@writer0713 nice one. So I assume my gist helped?

@writer0713
Copy link

@writer0713 writer0713 commented Apr 4, 2018

@tabirkeland
Yes, I read so many different articles, but nothing worked. After I followed your code, it worked. Thanks.

@daraul
Copy link

@daraul daraul commented May 22, 2018

@writer0713 your readme was a life saver. Worked like a charm
Thanks to you and @tabirkeland

@cleverappdesign
Copy link

@cleverappdesign cleverappdesign commented Oct 12, 2018

made a starter project with detail explanations.

https://github.com/writer0713/ionic-environment-setting/blob/master/README.md

@writer0713 @tabirkeland thanks!! It works so well!

@sana-moussa
Copy link

@sana-moussa sana-moussa commented Dec 7, 2018

I ended up using a combo of @emcniece and @gshigeto solutions, so massive thanks to them.

I wanted to have environment variables work in Ionic exactly like how they do in Angular CLI (especially given Ionic 4 is somewhere on the horizon), whilst also supporting Karma and Protractor tests.

To use the environment variables, just add import { environment } from '@env/environment';.
Also supports ngc so I can build an Android APK with any environment config e.g. my environment.test.ts config.

This solution also supports using ionic serve with any of your configurations e.g.
ionic serve --env=prod

I've managed all this with the following setup:

/src/app/environments folder contains

environment.model.ts
environment.ts
environment.prod.ts
environment.test.ts

/tsconfig.json

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "baseUrl": "./src",
    "declaration": false,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "paths": {   
      "@env/*": [
        "environments/*"
      ]
    },
    "lib": [
      "dom",
      "es2016"
    ],
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "target": "es5"
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "src/**/*.spec.ts"
  ],
  "typeRoots": [
    "node_modules/@types"
  ],
  "compileOnSave": false,
  "atom": {
    "rewriteTsconfig": false
  }
}

/config/webpack.config.js

var chalk = require("chalk");
var defaultConfig = require('@ionic/app-scripts/config/webpack.config.js');
var ionic_env = process.env.IONIC_ENV;
var fs = require('fs');
var path = require('path');
var env = require('minimist')(process.argv.slice(2)).env || process.env.IONIC_ENV || 'dev';
var webpack = require('webpack');

defaultConfig.dev.resolve.alias = {
    "@env/environment": path.resolve(environmentPath('dev'))
};

defaultConfig.prod.resolve.alias = {
    "@env/environment": path.resolve(environmentPath('prod'))
};

console.log(chalk.yellow.bgBlack('\nUsing ' + env + ' environment variables.\n'));

if (!!env) {
    var pathToCustomEnvFile = path.resolve(environmentPath(env));

    defaultConfig[env] = defaultConfig[ionic_env];
    defaultConfig[env].resolve.alias = {
        "@env/environment": pathToCustomEnvFile
    };

    var customEnvPlugin = new webpack.NormalModuleReplacementPlugin(
        /src\/environments\/environment\.ts/, pathToCustomEnvFile
    );

    defaultConfig.prod.plugins.push(customEnvPlugin);
    defaultConfig.dev.plugins.push(customEnvPlugin);
}

module.exports = function() {
    return defaultConfig;
};


function environmentPath(env) {
    var filePath = './src/environments/environment' + (env === '' ? '' : '.' + env) + '.ts';
    if (fs.existsSync(filePath)) {
        return filePath;
    }

    console.log(chalk.red.bgWhite('\n' + filePath + ' does not exist!\n'));
    process.exit();
}

/test-config/webpack.test.js

var webpack = require('webpack');
var path = require('path');
var fs = require('fs');
var chalk = require('chalk');

module.exports = {
    devtool: 'inline-source-map',

    resolve: {
        alias: {
            '@env/environment': path.resolve(environmentPath('dev')),
        },
        extensions: ['.ts', '.js']
    },

    module: {
        rules: [{
                test: /\.ts$/,
                loaders: [{
                    loader: 'ts-loader'
                }, 'angular2-template-loader']
            },
            {
                test: /\.html$/,
                loader: 'html-loader?attrs=false'
            },
            {
                test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
                loader: 'null-loader'
            }
        ]
    },

    plugins: [
        new webpack.ContextReplacementPlugin(
            // The (\\|\/) piece accounts for path separators in *nix and Windows
            /(ionic-angular)|(angular(\\|\/)core(\\|\/)@angular)/,
            root('./src'), // location of your src
            {} // a map of your routes
        )
    ]
};

function environmentPath(env) {
    var filePath = './src/environments/environment' + (env === '' ? '' : '.' + env) + '.ts';
    if (fs.existsSync(filePath)) {
        return filePath;
    }

    console.log(chalk.red('\n' + filePath + ' does not exist!\n'));
    process.exit();
}

function root(localPath) {
    return path.resolve(__dirname, localPath);
}

/package.json

....
        "ionic": "ionic",
        "build": "ionic build",
        "build:test": "ionic build --env=test",
        "build:prod": "ionic build --prod",
        "build:android": "ionic cordova build android --dev",
        "build:android:test": "ionic cordova build android --prod --env=test",
        "build:android:prod": "ionic cordova build android --prod",
....

this is the only efficient solution that worked for me, thanks!

@vdias38
Copy link

@vdias38 vdias38 commented Jan 16, 2019

@GFoley83 I've tested your solution but with AOT in specific cases it doesn't work, I ignore the reason.
How to reproduce the error:

  1. log any attribute from your env file on app.module and app.component. The attribute should have different value for each env.
  2. run AOT build with --env=prod ionic build --env=prod --prod
  3. you should observe on your console that app.module log value from dev and app.component log value from prod

It's annoying if you use firebase and need to initiate it with env vars on your app.module (AngularFireModule.initializeApp(ENV.firebase)). At the moment I didn't find a good solution. @writer0713 suggest to rewrite env file using npm hooks, I'll check it.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
You can’t perform that action at this time.