Closed danbucholtz closed 6 years ago
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?
@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:
Perhaps there could be a better approach, but I thing this would work in most scenarios.
Perhaps I'm missing something, but what sort of use cases would benefit from async computation of values?
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.
Right, I'm with you now. :+1:
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
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?)
@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.
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.
The difficulty there though, is that if one async value depended on another, you wouldn't be able to resolve the configuration correctly if app-scripts ran everything in parallel.
Better to put the logic in the hands of the user, IMHO.
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.
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'
});
}
@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.
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'
@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 https://github.com/driftyco/ionic-app-scripts/issues/762#issuecomment-280666058, that can cover most of the uses cases since the dev needs vary as we can see from the posts on this topic.
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...
@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.
@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.
@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
}
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
+1
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?
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 :).
Angular-cli has implemented and its easy to configure it in angular-cli file why not to do the same?
We want this too!
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:
env
folder in the root of the Ionic project and added this folder to our .gitignore
env
folder we have different files:
./env
production.json
sandbox.json
staging.json
staging.json
file looks like:
{
"api_url": "'https://staging.api.url'",
"client_id_login": "'566d41f...'",
"client_secret_login": "'511480878...'",
"google_maps_key": "'tEkoJK...'"
}
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"
}
replace.env.js
file handles the generations of our config.ts
file https://gist.github.com/juarezpaf/7a9007dfcef25ec24f7de900df8fe04eThe 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;
}
}
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; } }
8. 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;
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:
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.
There is a very clean implementation at https://github.com/driftyco/ionic-app-scripts/pull/683#issuecomment-287401855 which I'm using successfully.
@pbowyer , does it work for you with ionic build browser --prod
?
I've commented on #683 as it fails for me when the AoT Compiler kicks in.
Solution in #683 fails with
[17:52:03] Error: Error encountered resolving symbol values statically. Could not resolve @app/config relative to XXX/src/app/app.module.ts., resolving symbol AppModule in...
@vkniazeu That exact command works for me - but I can't see anything in the output saying AoT is running, so can't definitively confirm.
@pbowyer , thanks! It's the ngc started ...
line which indicates that angular-cli AoT compiler is being used. It should be default if the --prod
flag is used.
Are you using latest Ionic and Angular packages in your project?
@vkniazeu That line's present in the output, so it's being used. It's not present if I omit --prod
.
As to versions, I'm on:
Cordova CLI: 6.5.0
Ionic Framework Version: 2.2.0
Ionic CLI Version: 2.2.1
Ionic App Lib Version: 2.2.0
Ionic App Scripts Version: 1.1.4
ios-deploy version: 1.9.1
ios-sim version: 5.0.13
OS: OS X El Capitan
Node Version: v6.10.0
Xcode version: Xcode 8.2.1 Build version 8C1002
@pbowyer , thank you for your time. Strangely, it fails for another person as well with the same error and same setup. My package versions are newer, but I don't think this is the reason for the resolving symbol values statically
error. I'll keep digging.
@pbowyer The solution described in https://github.com/driftyco/ionic-app-scripts/pull/683 does not work with AOT. And ngc
should fail for import ... from '@app/config'
as it will not be able to resolve it since it's never declared.
@rolandjitsu , thanks for bringing this up again. Do you have an idea of how to overcome this lack of declaration for AoT to work? Or, perhaps, you have an alternative solution that would be similar in simplicity?
@vkniazeu I currently use Rollup instead of Webpack and rollup-plugin-replace.
rollup.config.js
:
// For reference, check the following:
// https://github.com/driftyco/ionic-app-scripts/blob/master/config/rollup.config.js
// Config borrowed from: https://github.com/driftyco/ionic-cli/issues/1205#issuecomment-255744604
const nodeResolve = require('rollup-plugin-node-resolve');
const commonjs = require('rollup-plugin-commonjs');
const globals = require('rollup-plugin-node-globals');
const builtins = require('rollup-plugin-node-builtins');
const json = require('rollup-plugin-json');
// We use the replace plugin to provide the correct env variables
const replace = require('rollup-plugin-replace');
// We will use the machine ip to make HTTP requests to the local machine from an external device while in dev mode.
const ip = require('ip');
// We need to expose the app id to the app.
const ionicConfig = require('../ionic.config.json');
// NOTE: If IONIC_ENV is not set, we check if the CLI args contain the '--prod' flag.
function isProd() {
return process.env.NODE_ENV === 'production' || process.env.IONIC_ENV === 'prod' || process.argv.slice(2)
.some((arg) => arg.indexOf('--prod') !== -1)
}
// https://github.com/rollup/rollup/wiki/JavaScript-API
const config = {
/**
* entry: The bundle's starting point. This file will
* be included, along with the minimum necessary code
* from its dependencies
*/
entry: process.env.IONIC_APP_ENTRY_POINT,
/**
* sourceMap: If true, a separate sourcemap file will
* be created.
*/
sourceMap: true,
/**
* format: The format of the generated bundle
*/
format: 'iife',
/**
* dest: the output filename for the bundle in the buildDir
*/
dest: process.env.IONIC_OUTPUT_JS_FILE_NAME,
/**
* plugins: Array of plugin objects, or a single plugin object.
* See https://github.com/rollup/rollup/wiki/Plugins for more info.
*/
plugins: [
builtins(),
commonjs(),
nodeResolve({
module: true,
jsnext: true,
main: true,
browser: true,
extensions: ['.js']
}),
globals(),
json(),
replace({
values: {
'{{API_HOST}}': isProd() ? 'https://api.domain.com' : `http://${ip.address()}:8888`,
'{{IONIC_ENV}}': isProd() ? 'prod' : 'dev',
'{{APP_ID}}': ionicConfig.app_id
},
// Config
exclude: 'node_modules/**'
})
]
};
module.exports = config;
env.ts
(I placed it under src/app/
):
export const env: any = '{{IONIC_ENV}}';
export const apiHost = '{{API_HOST}}';
export const appId = '{{APP_ID}}';
export function isProd(): boolean {
return env === 'prod';
}
And you just import from env
wherever you need. It should work with AOT as well.
Though I'm encountering other issues with Rollup that are unrelated.
I'm not sure about a fix for @app/config
, I imagine making a declaration file should suffice, but I'm not sure how ngc will work with it since it needs to resolve the values statically.
@rolandjitsu , thank you for sharing your setup. I will consider it as my project grows if an Ionic own solution is not available by then.
After reading above and googling, I got too many ideas for env variable setup and now fully confused about what is best for scale-able enterprise application? should I wait for ionic team implementation for ionic-3
Too many solutions are out there right now, most of them are not fully functional with prod development. We need a official solution and support from the ionic team for this.
Hello. Based off the great work by @juarezpaf in his previous comment, we're using something very similar.
Advantages for us with this approach are that it's relatively simple, does not involve changing webpack or Rollup config, works with prod / AOT and uses --env=[environment] flag.
We are able to use the standard ionic serve
command, and default to a development env, or use the --env flag when we require a different environment e.g. ionic build ios --prod --env=production
Our setup is as follows (very similar to juarezpaf with a few modifications!).
./env
production.json
staging.json
development.json
Example of .env/development.json
{
"environment": "development",
"endpoints": {
"server1": "https://....",
"server2": "https://...."
}
}
./scripts/environment.js
and reference it in package.json available at https://gist.github.com/rossholdway/16724496806b66a162ee6cbf8bfc5def"scripts": {
...
"ionic:watch:before": "node ./scripts/environment",
"ionic:build:before": "node ./scripts/environment",
...
}
NOTE: If you're using a version of ionic-cli < 3.4.0 use preionic:build
and preionic:serve
This will generate ./src/app/app.config.ts
.
In your module import the config with import { APP_CONFIG, AppConfig } from './app.config';
and add as a provider
providers: [
...
{
provide: APP_CONFIG,
useValue: AppConfig
}
]
Now you can import your environment specific app config where needed via Angular DI. e.g.
import { Component, Inject } from '@angular/core';
import { APP_CONFIG } from './app.config';
@Component({
selector: 'example-page',
templateUrl: 'example-page.html'
})
export class ExamplePage {
constructor(@Inject(APP_CONFIG) private config) {
console.log(config.environment);
console.log(config.endpoints.server1);
}
}
There are some disadvantages, mainly that we can't use TypeScript in app.config.ts easily, but apart from that we are yet to run into any issues.
Usage examples
ionic serve
(Not specifying the --env flag will default to using .env/development.json)
ionic build ios --prod --env=production
(Will use .env/production.json to build app.config.ts)
app-scripts is a never ending project. I'll hopefully have some time to tackle this in the coming weeks.
Thank you for being patient. 💯
Thanks, Dan
+1
@danbucholtz any news on your process? Or what would be a proper solution? Would the solution @rossholdway provided, be the best solution for now?
Soon. Very soon! So here's what on my road map:
Migrate to a "fesm" build of ionic-angular. It did not make it for this weeks release, but should be in in two weeks in the next release. This is basically bundling code with rollup before bundling it again with webpack. The reason for this is to avoid unnecessary closures. Our testing shows this should knock off around ~1 second of app start-up time on mobile devices. (I estimate this will take 2 more days)
Make some fixes to navigation & deep linking in ionic-angular (I estimate this will take two weeks).
I think that's all I have that is higher priority than the environment stuff. We'll get this in soon.
Thanks, Dan
I personally reckon this issue should be higher priority than most, because having different configs for different environments is just something a good framework should have from the start if you want it to be used in professional settings.
This is also the reason why I'm so puzzled by Angular not having an official logging provider / solution.
But of course I'm glad you'll be finally working on it 🎉
Folks,
Look at: Ionic 2 Environment Variables: The Best Way
This solution works like a charm. I implemented it in my project. I even think that it removes this issue from so high priority.
@rossholdway you solution works for me only if I use npm run ionic:serve
instead of ionic serve
. Any hints?
Nevermind, it's working. It just doesn't log anything when run via ionic serve
.
@trumbitta Sorry, I'm not sure why it wouldn't be working correctly with ionic serve
for your setup. I'd just stick with using npm run ionic:serve
for now as it sounds like it won't be long until we have support for environments built directly into app scripts 🎉
Hi @danbucholtz, sorry to bother you and thanks for your work!
Any news?
This would be really essential for me also. Anything that can be done to help you guys?
I am thinking we could do this:
ionic serve --env qa
orionic run android --env prod
My first thought was that it's value would default to
dev
for non-prod builds, andprod
for prod builds. Developer's can pass in whatever they want, though.config
section of the package.json is read. It could look something like this: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.npm script
section, or we could provide a hook into thepostprocess
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