Skip to content

Commit

Permalink
feat(stark-build): add support for environment variables at runtime (…
Browse files Browse the repository at this point in the history
…importing environment.ts file) and at compilation time (using webpack Define plugin)

ISSUES CLOSED: #50
  • Loading branch information
christophercr committed Jun 26, 2018
1 parent e909416 commit 8f246f6
Show file tree
Hide file tree
Showing 62 changed files with 532 additions and 356 deletions.
235 changes: 183 additions & 52 deletions docs/ENVIRONMENTS.md
Original file line number Diff line number Diff line change
@@ -1,87 +1,218 @@
# Environments support

## Environment.ts
Stark provides 2 different ways to get environment information depending on your needs:

In the `src/environments/model.ts` is provided the Environment interface as follows
- at runtime by importing the environment.ts file
- at compilation time by checking the global/ambient variables set by Webpack

```
...
export interface Environment {
production: boolean;
hmr: boolean;
...
```
## Environment information at runtime (environment.ts)

`production`, which is a boolean, will specify is your application runs under a production environment,
while `hmr` will indicate if it runs under a Hot Module Replacement environment.
Stark provides the `StarkEnvironment` interface which describes which information you can get from the current environment.
Such environment interface is defined as follows:

This interface will then be defined by default in `src/environments/environment.ts`:
```typescript
import { NgModuleRef } from "@angular/core";

export interface StarkEnvironment {
/**
* Whether the current environment is production
*/
production: boolean;
/**
* Array of providers to be included only in this environment.
* For example: you might want to add a detailed logging provider only in development.
*/
ENV_PROVIDERS: any[];
/**
* Function to modify/decorate the NgModule Instance created by Angular for a given platform.
* Useful to enable/disable some Angular specifics such as the debug tools.
*/
decorateModuleRef(modRef: NgModuleRef<any>): NgModuleRef<any>;
}
```
...
export const environment: Environment = {
production: false,
hmr: false,
showDevModule: true,
...

In your project, the files to define the different environments will be located in `src/environments`:

```txt
|
+---src
| |
| +---environments # configuration variables for each environment
| | | #
| | | environment.e2e.prod.ts # e2e tests configuration
| | | environment.hmr.ts # development with HMR (Hot Module Replacement) configuration
| | | environment.prod.ts # production configuration
| | \ environment.ts # development configuration
| |
| \ ...
|
\ ...
```

## How to find out which environment is your application is currently using?
Then in each file, an `environment` constant of type `StarkEnvironment` should be exported providing the values needed for each environment:

All you have to do is to import the environment.ts constant anywhere you want in your application:
```typescript
// environment.prod.ts

import { NgModuleRef } from "@angular/core";
import { disableDebugTools } from "@angular/platform-browser";
import { StarkEnvironment } from "@nationalbankbelgium/stark-core";

export const environment: StarkEnvironment = {
production: true,
ENV_PROVIDERS: [ProductionOnlyProvider],

decorateModuleRef(modRef: NgModuleRef<any>): NgModuleRef<any> {
disableDebugTools(); // disable debug tools in production
return modRef;
}
};
```

### How to get environment variables in your application?

All you have to do is to import the `environment.ts` constant anywhere you want in your application.

**You should always import from `environments/environment` because Webpack will internally replace such file with the right environment file as defined in the `fileReplacements` option in the `angular.json` file.**

This way, you will be able to programmatically read the different environment variables you need.
For example, you can determine which providers you will include for a specific environment in your AppModule:

```typescript
// the environment file should always be imported from this path
import { NgModule } from "@angular/core";
import { environment } from "environments/environment";

@NgModule({
bootstrap: [AppComponent],
declarations: [AppComponent],
imports: [...],
providers: [
environment.ENV_PROVIDERS,
...
]
})
export class AppModule { ... }
```

This way, you will be able to programmatically determine which environment is used in your app
with simple checks, as follows:
### How to add a new environment?

First, create your new environment.ts file in the `src/environments` folder.

Then, make sure your new environment implements the `StarkEnvironment` interface. For example, the `environment.dummy-env.ts` file:

```typescript
import { NgModuleRef } from "@angular/core";
import { StarkEnvironment } from "@nationalbankbelgium/stark-core";

export const environment: StarkEnvironment = {
production: false / true,
ENV_PROVIDERS: [],

decorateModuleRef(modRef: NgModuleRef<any>): NgModuleRef<any> {
/* do something here */
}
};
```
environment.production
// if the output of this line is true, we are in production environment.
// Otherwise, we are in hmr environment.

Finally, define the file replacement of your new environment in the `angular.json` file so that Webpack can replace the default file `environments/environment` with your new file:

```text
{
...
"dummyEnv": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.dummy-env.ts"
}
]
}
}
```

## How to add a new environment ?
### How to add more properties to the environment file?

In case you want to add more properties, you should first create your own interface which should extend the `StarkEnvironment` interface.
For example:

First, create you new Ts file in the `src/environments` folder.
```typescript
import { StarkEnvironment } from "@nationalbankbelgium/stark-core";

export interface YourOwnEnvironment extends StarkEnvironment {
someProperty: any;
}
```

Then, make sure your new environment extends the `src/environments/model.ts` interface.
Then adapt the different environment files to add the new properties:

Finally, define the options and file replacement of your new environment in the `angular.json` file.
```typescript
import { NgModuleRef } from "@angular/core";
import { StarkEnvironment } from "@nationalbankbelgium/stark-core";

For exemple, the environment.dummyEnv.ts file:
export const environment: StarkEnvironment = {
production: false / true,
ENV_PROVIDERS: [],
someProperty: "some value", // your new property

decorateModuleRef(modRef: NgModuleRef<any>): NgModuleRef<any> {
/* do something here */
}
};
```
import { enableProdMode, NgModuleRef } from "@angular/core";
import { disableDebugTools } from "@angular/platform-browser";
import { Environment } from "./model";

enableProdMode();
## Environment information at compilation time (Webpack global variables)

export const environment: Environment = {
Thanks to the Webpack [DefinePlugin](https://webpack.js.org/plugins/define-plugin), Stark provides some global variables that are available at compilation time, which means that you can
implement some checks in your code and this will be analyzed when your application bundle is being built by Webpack.

production: false,
hmr: false,
The global variables available at compilation time are the following:

decorateModuleRef(modRef: NgModuleRef<any>): NgModuleRef<any> {
disableDebugTools();
return modRef;
},
ENV_PROVIDERS: []
- `ENV` which indicates the current environment: `"development"` or `"production"`
- `HMR` which indicates whether the Hot Module Replacement support is enabled (true/false).

### How to get target environment at compilation time?

Since Webpack defines the environment variables as global, you can use them everywhere in your code so you can, for example, determine on which environment your app is currently running
and execute some logic only on that specific environment:

```typescript
// if true, your app is running in development environment
if (ENV === "development") {
/* the code inside this block will be executed only in development */
}
```

And in the angular.json file:

To avoid Typescript compilation issues regarding these global variables, make sure that you include the typings from the stark-build package in your app `tsconfig.json`:

```text
{
"extends": "./node_modules/@nationalbankbelgium/stark-build/tsconfig.json",
"compilerOptions": {
...
"typeRoots": [
"./node_modules/@types",
"./node_modules/@nationalbankbelgium/stark-build/typings" // typings from stark-build
],
...
},
...
}
```
"dummyEnv" : {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.dummyEnv.ts"
}
]
}

### Why do you need the target environment at compilation time?

Sometimes you might need to add some logic or import some files only when your application is running in development or production.

**In this case, when Webpack builds your application, the final bundle will contain also that code and/or imports that will only be used on a specific environment.
For example, the specific code related to development will never be executed in production and yet it will be included in your production build which will increase the size of your bundle.**

This is why knowing the target environment at compilation time is useful. You can put the logic inside an if block and then such code will be tree-shaken by Webpack as it will recognize it as dead code:

```typescript
// this check is translated to "if (false)" when ENV is "production"
// allowing Webpack to identify it as dead code and so remove it
if (ENV === "development") {
/* the code inside this block will only be included in development */
}
```
23 changes: 21 additions & 2 deletions docs/WEBPACK.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Webpack Customizations

Stark-Build uses several Webpack plugins as well as some utils in order to leverage lots of functionality and customizations to your builds (DEV and PROD).
The stark-build package uses several Webpack plugins as well as some utils in order to leverage lots of functionality and customizations to your builds (DEV and PROD).

This is the list of utils and plugins used by the stark-build package:

Expand Down Expand Up @@ -41,7 +41,7 @@ The only thing you need to do is to configure the **baseHref** and **deployUrl**

Allows to customize the base url in the _index.html_ via the webpack config.

In Stark-Build, the custom base url provided to this plugin is the one you define in the **baseHref** option of your project's _angular.json_ file:
In stark-build, the custom base url provided to this plugin is the one you define in the **baseHref** option of your project's _angular.json_ file:

```json
{
Expand All @@ -64,3 +64,22 @@ In Stark-Build, the custom base url provided to this plugin is the one you defin
```

This plugin will automatically add the base tag `<base href="<custom-base-url>">` to the _index.html_ so you don't need to add it manually yourself.

#### [DefinePlugin](https://webpack.js.org/plugins/define-plugin)

Allows to define global variables that will be available during the compilation and building of the application bundles.

In this case, stark-build provides some global variables that are available at compilation time, which means that you can implement some checks in your code and this will be analyzed when your application bundle is being built by Webpack.

The global variables available at compilation time are the following:

- `ENV` which indicates the current environment: `"development"` or `"production"`
- `HMR` which indicates whether the Hot Module Replacement support is enabled (true/false).

Since the Define plugin defines those variables as global, you can use them everywhere in your code like this:

```typescript
if (ENV === "development") {
/* the code inside this block will be executed only in development */
}
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
"update:starter": "npm run clean:starter && npm run install:starter"
},
"lint-staged": {
"*.{css,js,json,pcss,scss,ts}": [
"*.{css,js,json,md,pcss,scss,ts}": [
"prettier --write",
"git add"
]
Expand Down
45 changes: 13 additions & 32 deletions packages/stark-build/config/build-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,24 @@ const fs = require("fs");
const helpers = require("./helpers");
const ngCliUtils = require("./ng-cli-utils");

const _getAngularCliAppConfig = getAngularCliAppConfig();
const angularCliAppConfig = ngCliUtils.getAngularCliAppConfig(helpers.root("angular.json"));
const ANGULAR_APP_CONFIG = {
config: _getAngularCliAppConfig,
deployUrl: _getAngularCliAppConfig.architect.build.options.deployUrl || "",
baseHref: _getAngularCliAppConfig.architect.build.options.baseHref || "/",
sourceRoot: _getAngularCliAppConfig.sourceRoot,
outputPath: _getAngularCliAppConfig.architect.build.options.outputPath
config: angularCliAppConfig,
deployUrl: angularCliAppConfig.architect.build.options.deployUrl || "",
baseHref: angularCliAppConfig.architect.build.options.baseHref || "/",
sourceRoot: angularCliAppConfig.sourceRoot,
outputPath: angularCliAppConfig.architect.build.options.outputPath
};

const DEFAULT_METADATA = {
title: "Stark Application by @NationalBankBelgium",
baseUrl: "/",
isDevServer: helpers.isWebpackDevServer(),
TITLE: "Stark Application by @NationalBankBelgium",
BASE_URL: "/",
IS_DEV_SERVER: helpers.isWebpackDevServer(),
HMR: helpers.hasProcessFlag("hot"),
AOT: process.env.BUILD_AOT || helpers.hasNpmFlag("aot"),
E2E: !!process.env.BUILD_E2E,
WATCH: helpers.hasProcessFlag("watch"),
tsConfigPath: ANGULAR_APP_CONFIG.config.architect.build.options.tsConfig,
TS_CONFIG_PATH: ANGULAR_APP_CONFIG.config.architect.build.options.tsConfig,
environment: ""
};

Expand Down Expand Up @@ -76,11 +76,11 @@ function getEnvironmentFile(environment) {
* Read the tsconfig to determine if we should prefer ES2015 modules.
* Load rxjs path aliases.
* https://github.com/ReactiveX/rxjs/blob/master/doc/pipeable-operators.md#build-and-treeshaking
* @param supportES2015 Set to true when the output of typescript is >= ES6
* @param shouldSupportES2015 Set to true when the output of typescript is >= ES6
*/
function rxjsAlias(supportES2015) {
function rxjsAlias(shouldSupportES2015) {
try {
const rxjsPathMappingImport = supportES2015 ? "rxjs/_esm2015/path-mapping" : "rxjs/_esm5/path-mapping";
const rxjsPathMappingImport = shouldSupportES2015 ? "rxjs/_esm2015/path-mapping" : "rxjs/_esm5/path-mapping";
const rxPaths = require(rxjsPathMappingImport);
return rxPaths();
} catch (e) {
Expand Down Expand Up @@ -143,25 +143,6 @@ function getApplicationAssetsConfig() {
return [];
}

function getAngularCliAppConfig() {
const applicationAngularCliConfigPath = helpers.root("angular.json");
if (fs.existsSync(applicationAngularCliConfigPath)) {
const angularCliConfig = require(applicationAngularCliConfigPath);
const cliConfig = ngCliUtils.validateAngularCLIConfig(angularCliConfig);
if (cliConfig) {
if (cliConfig.defaultProject && cliConfig.projects[cliConfig.defaultProject]) {
return cliConfig.projects[cliConfig.defaultProject];
} else {
throw new Error("Angular-cli config apps is wrong. Please adapt it to follow Angular CLI way.");
}
} else {
throw new Error("Parsing " + applicationAngularCliConfigPath + " failed. Ensure the file is valid JSON.");
}
} else {
throw new Error("angular.json is not present. Please add this at the root your project because stark-build needs this.");
}
}

/**
* Return copyWebpack config based on angular cli assets declaration
*
Expand Down
Loading

0 comments on commit 8f246f6

Please sign in to comment.