Usually, an Angular application has a /src/environments folder which contains two files:
environment.prod.ts
environment.ts
But currently, there is no way to pass different variables into the example angular app depending on the build environment.
It would be really handy to
environment.ts file to the devserverenvironment.ts with environment.prod.ts for the prodserverThere are two ways you could model this in Bazel.
1) devserver and bundler both get all the prod and dev main/configuration files as inputs, and the programs spawned by Bazel are sensitive to an environment variable that affects their behavior.
2) dev and prod are different entry points into the application, and those entry points import different configurations. This is better for the dependency graph - since the prod configurations aren't a dependency of the devserver target, and the dev configurations aren't a dependency of the production bundle, it means those targets don't have to be rebuilt unnecessarily. I imagine configurations change a lot and it would be a pain to wait for a slow prod re-bundle/optimize when you only changed the dev config.
We currently prefer the second and have shown it in https://github.com/bazelbuild/rules_nodejs/tree/master/examples/angular/src
I don't know if we should necessarily build an example showing how to do it the first way because it's a suboptimal dependency graph shape. What's the impact of switching to 1? Is it a burden to change application code to have different entry points? Is the resulting developer experience not as good?
@alexeagle Ok, the second way is definitely better.
I think when you talk about "different entry points", you refer to main.dev.ts and main.prod.ts?
But how would I add my environment variables to those files? Because usually we would import environment from './environments/environment.
Is it possible to export an object from main.dev.ts and main.prod.ts and import it from everywhere in the application?
@flolu yeah the main.dev.ts and main.prod.ts would be the way to go. The idea with this is you don't use the "CLI" way of doing it by importing from an environmentt file, instead you'd do something else like setting a variable on your window object.
What we do in our repository is only have a flag as to if the application was build in prod mode or dev mode, then we load in the rest of the config dynamically at runtime.
@Toxicable Setting a variable on the window object is a great idea and thus I've tried it and it works for runtime settings. The problem is that it won't work for conditional NgModule imports:
let devs = [];
if ((window as any).PRODUCTION) {
devs = [StoreDevtoolsModule.instrument()];
}
@NgModule({
declarations: [AppComponent],
imports: [...devs],
bootstrap: [AppComponent],
})
export class AppModule {}
Any ideas?
one thing you could do is
StoreDevtoolsModule.instrument({logOnly: window.PRODUCTION})
or another way is using a turnery since Angular is able to evaluate it
imports: window.prod ? [] : [StoreDevtoolsModule.instrument()]
@Toxicable It seems as if I am doing something wrong. Here is what I've tried:
StoreDevtoolsModule.instrument({ logOnly: (window as any).PRODUCTION }),
...error:
ReferenceError: window is not defined
imports: [
window.PRODUCTION ? [] : StoreDevtoolsModule.instrument()
]
...error:
error NG1010: Value at position 2 in the NgModule.imports of AppModule is not a reference: [ob
ject Object]
imports: window.PRODUCTION ? [] : [StoreDevtoolsModule.instrument()]
...error:
error NG1010: Expected array when reading the NgModule.imports of AppModule
And this is how my main files look like:
import { enableProdMode } from '@angular/core';
import { platformBrowser } from '@angular/platform-browser';
import { AppModule } from './app/app.module';
enableProdMode();
platformBrowser().bootstrapModule(AppModule);
(window as any).PRODUCTION = true;
import { platformBrowser } from '@angular/platform-browser';
import { AppModule } from './app/app.module';
platformBrowser().bootstrapModule(AppModule);
(window as any).PRODUCTION = false;
@Toxicable Another problem is, that I can't use the window object since it does not exist on the server. (When using server-side rendering) :thinking:
Hmm interesting, we don't actually use this approach since we don't have any dynamic module imports. How would you do it if you had the usually Angular CLI environment file?
For the NodeJS stuff you'd just want to do a global/window nullish check.
Assign to global if NodeJS, otherwise window if browser
@Toxicable With the usual Angular CLI env file I would have done the following:
import { environment } from 'src/environments/environment';
imports: [
environment.production ? [] : StoreDevtoolsModule.instrument(),
]
This worked without any problems.
I've tried the global/window null check etc. But it doesn't seem to work well because:
main.prod.ts global and window were undefined on the serverenv.ts which reads the global/window prod var is called before main.ts (according to logs) I think I will need something that actually changes a file in the devserver or/and prodapp. I've tried creating a Bazel rule that runs a shell script, which would replace a env.ts with the content of a env.prod.ts file for the prodapp, but this didn't seem to work.
@Toxicable @alexeagle @rayman1104 Do you think we can get the solution like drafted below to work?
env.dev.ts and env.prod.tsmove_and_rename rule will create a env/index.tsenv/index.ts is the file you will import from the source codedevserver will pull in //env:dev and bundle-es2015 will pull in //env:prodsrc/env/BUILD.bazel
move_and_rename(
name = "prod_env.ts",
file = "env.prod.ts",
)
move_and_rename(
name = "dev_env.ts",
file = "env.dev.ts",
)
ts_library(
name = "prod",
srcs = [":prod_env.ts"],
)
ts_library(
name = "dev",
srcs = [":dev_env.ts"],
)
src/BUILD.bazel
ts_devserver(
name = "devserver",
# ...
deps = [":src", "//env:dev"]
)
# ...
rollup_bundle(
name = "bundle-es2015",
# ...
deps = [":src", "//env:prod"]
)
This is how the implementation for move_and_rename might look like:
def _move_and_rename_impl(ctx):
in_file = ctx.file.file
out_file = ctx.actions.declare_file("index.ts")
ctx.actions.run_shell(
inputs = [in_file],
outputs = [out_file],
progress_message = "Creating env/index.ts",
command = "cat %s > %s" % (in_file.path, out_file.path),
)
return [DefaultInfo(files = depset([out_file]))]
move_and_rename = rule(
implementation = _move_and_rename_impl,
attrs = {
"file": attr.label(
mandatory = True,
allow_single_file = True,
),
},
)
For the feature modules to compile, they also need an environment file. Thus I need to add //env:dev to the deps of all ng_modules that import the environment file, which is not a big deal.
But when compiling the prodapp there is a conflict between the //env:dev from the feature modules and the //env:prod from the rollup_bundle
ERROR: file 'src/environments/index.ts' is generated by these conflicting actions:
Label: //src/environments:dev_env.ts, //src/environments:prod_env.ts
Is it possible to resolve the conflict by somehow overwriting existing files in the move_and_rename?
As for the first, nonoptimal way, we did that with @gregmagolan recently, and I'll leave those snippets here, since it may be useful sometimes, i.e. when migrating the projects that already use environments/environment.ts substitution which is (_for some reason, I have no idea why_) the recommended way in Angular docs.
environments_sub rule
def environments_sub(envs = []):
src_select = {}
for env in envs:
native.config_setting(
name = env,
values = {"define": "configuration=%s" % env},
)
src_select[":%s" % env] = ":environment.%s.ts" % env
src_select["//conditions:default"] = "environment.ts"
copy_file(
name = "environments_sub",
src = select(src_select),
out = "environment.ts",
)
you can take copy_file from bazel-skylib or use your own implementation. Also, can be inspired from this repo's copy_to_bin rule since it's cross-compatible (also need to copy copy_bash, copy_cmd):
environments/BUILD.bazel
environments_sub([
"prod",
"dev",
])
ts_library(
name = "environments",
srcs = [":environments_sub"],
visibility = ["//:__subpackages__"],
)
So, after that prod build can be run just with additional flag:
bazel run //src:prodserver --define configuration=prod
Each configuration will be a complete rebuild of the output tree which is how it works with webpack as well.
Also, there is an issue with AOT in a split compilation environment, so exporting an Object from envirnonment.ts and using it inside angular decorators won't work. As a workaround for this, we export all values from environment.ts as simple constants.
environment.ts
export const production = false;
export const gatewayHost = 'http://localhost:3000';
and then in all ts files just need to substitute the imports:
// import { environment } from '../environments/environment';
import * as environment from '../environments/environment';
@flolu I guess it answers your question.
But still would be cool to see the example how to import variables either from main.ts or in runtime, so no need to rebuild the whole tree for different environments. I guess we can't have window.production in decorators because of AOT? and also can't import vars directly from main.ts cause it will be a cyclic dependency?
@rayman1104 Awesome :tada: This worked with some slight modifications to copy_to_bin. Thank you!
But still would be cool to see the example how to import variables either from main.ts or in runtime
Definitely. Not sure if AOT is the problem, but I can confirm that event if it would work it would probably be not a "nice" solution (with window/global)
And the other downside is that the runtime method would prevent dynamically loading NgModule imports like this:
@NgModule({
imports: [
env.production ? [] : StoreDevtoolsModule.instrument(),
],
})
export class AppModule {}
@Toxicable Or is it possible to inject imports into NgModule at runtime?
You could also just have separate modules you bootstrap depending on which environment you're in.
app/app.component.ts
// ...
@Component({
// ...
});
export class AppComponent {}
app/app.module.ts
// ...
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
exports: [AppComponent],
});
export class AppModule {}
app/app-dev.module.ts
// ...
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
// import dev env vars
import { environment } from '../environment/environment.dev';
import { ENVIRONMENT } from '../environment/token';
@NgModule({
// throw in whatever dev dependencies you use
imports: [
AppModule,
],
providers: [
{
provide: ENVIRONMENT,
useValue: environment,
},
],
bootstrap: [AppComponent],
});
export class AppDevModule {}
app/app-prod.module.ts
// ...
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
// import prod env vars
import { environment } from '../environment/environment.prod';
import { ENVIRONMENT } from '../environment/token';
@NgModule({
// throw in whatever prod dependencies you use
imports: [
AppModule,
],
providers: [
{
provide: ENVIRONMENT,
useValue: environment,
},
],
bootstrap: [AppComponent],
});
export class AppProdModule {}
main.dev.ts
// ...
import { AppDevModule } from './app/app-dev.module';
await platformBrowser().bootstrapModule(AppDevModule);
main.prod.ts
// ...
import { AppProdModule } from './app/app-prod.module';
enableProdMode();
await platformBrowser().bootstrapModule(AppProdModule);
@marcus-sa That is a really clever solution, too :100: I like it.
Thank you!
Can this be added to the examples? We were using the angular CLI and found that environment.prod.ts is ignored entirely by the generate bazel targets
@prestonvanloon I might make a pull request at the weekend if you really need it?!
In the meantime you can look at the two commits I mentioned above or just checkout this code:
https://github.com/flolu/centsideas/tree/dev/services/client/app
I can figure it out based on the comments here and your examples. However, this issue to add an example was closed without adding an example, so I am asking to please reopen the issue and/or add the example.
Additionally, Is there any way to have a migration path for angular cli to bazel? This was quite surprising to learn that the bazel version of angular doesn't maintain feature parity with the default cli template with the environment files.
Most helpful comment
You could also just have separate modules you bootstrap depending on which environment you're in.