Build Angular Like An Architect (Part 2)
Tags: angular, devops, javascript, webdev
Published: 2019-04-25 19:19:11


In this section of the blog series Build Angular Like an Architect we look at optimizing a production build with angular-devkit, and round out our custom build by figuring out how to implement environments. 

### Recap

In [Build Angular Like an Architect (Part 1)](https://dev.to/steveblue/build-angular-like-an-architect-part-1-3ph2) we looked at getting started with the latest Architect API. By coding the Builder with the Architect API and RxJS we were able to extend Angular CLI with a new production build that optimizes Angular with Closure Compiler.

We ended up with a function that executes a RxJS Observable like so:

```typescript
export function executeClosure(
  options: ClosureBuilderSchema,
  context: BuilderContext
): Observable<BuilderOutput> {
  return of(context).pipe(
    concatMap( results => ngc(options, context) ),
    concatMap( results => compileMain(options, context)),
    concatMap( results => closure(options, context) ),
    mapTo({ success: true }),
    catchError(error => {
      context.reportStatus('Error: ' + error);
      return [{ success: false }];
    }),
  );
}
```


In the beginning of this section let's add more optimizations to the production bundle using a tool in @angular-devkit called `buildOptimizer`.

Create a new method called optimizeBuild that returns an RxJS Observable and add the method to the `pipe` in `executeClosure`.

```typescript
  return of(context).pipe(
    concatMap( results => ngc(options, context) ),
    concatMap( results => compileMain(options, context)),
    concatMap( results => optimizeBuild(options, context)),
    concatMap( results => closure(options, context) ),
```

Install `@angular-devkit/build-optimizer` in the build_tools directory.


```bash
npm i @angular-devkit/build-optimizer --save-dev
```

Import `buildOptimizer` like so.

```typescript
import { buildOptimizer } from '@angular-devkit/build-optimizer';
```

Essentially after the Angular Compiler runs, every component.js file in the out-tsc needs to be postprocessed with buildOptimizer. This tool removes unnecessary decorators that can bloat the bundle.

The algorithm for the script is as follows:

- list all files with extension .component.js in the out-tsc directory
- read each file in array of filenames
- call buildOptimizer, passing in content of each file
- write files to disk with the output of buildOptimizer

Let's use a handy npm package called glob to list all the files with a given extension.

Install glob in the build_tools directory.

```bash
npm i glob --save-dev
```

Import glob into src/closure/index.ts.

```javascript
import { glob } from 'glob';
```

In the `optimizeBuild` method, declare a new `const` and call it `files`.

```typescript
const files = glob.sync(normalize('out-tsc/**/*.component.js'));
```

`glob.sync` will synchronously format all files matching the glob into an array of strings. In the above example, `files` equals an array of strings that include paths to all files with the extension `.component.js`.

![](https://media.giphy.com/media/JwySbnfwZcoko/giphy.gif)

Now we have an array of filenames that require postprocessing with `buildOptimizer`. Our function `optimizeBuild` needs to return an Observable but we have an array of filenames.

Essentially `optimizeBuild` should not emit until all the files are processed, so we need to map files to an array of Observables and use a RxJS method called `forkJoin` to wait until all the Observables are done. A proceeding step in the build is to bundle the application with Closure Compiler. That task has to wait for `optimizeBuild` to complete.

```typescript

const optimizedFiles = files.map((file) => {
    return new Observable((observer) => {
        readFile(file, 'utf-8', (err, data) => {
        if (err) {
            observer.error(err);
        }
        writeFile(file, buildOptimizer({ content: data }).content, (error) => {
            if (error) {
                observer.error(error);
            }
            observer.next(file);
            observer.complete();
        });
    });
    });
});

return forkJoin(optimizedFiles);

```

Each file is read from disk with `readFile`, the contents of the file are postprocessed with `buildOptimizer` and the resulting content is written to disk with `writeFile`. The observer calls `next` and `complete` to notify `forkJoin` the asynchronous action has been performed.


If you look at the files in the out-tsc directory prior to running this optimization the files would include decorators like this one:

```javascript
AppComponent.decorators = [
{ type: Component, args: [{
            selector: 'app-root',
            templateUrl: './app.component.html',
            styleUrls: ['./app.component.css']
        },] },
];
```

Now the decorators are removed with `buildOptimizer` with you run `architect build_repo:closure_build`.

Let's move onto incorporating environments so we can replicate this feature from the default Angular CLI build.


## Handling Environments

Handling the environment configuration is much simpler than the previous exercises. First let's look at the problem.

In src/environments there are two files by default.

- environment.ts
- enviroment.prod.ts

environment.prod.ts looks like this by default.

```typescript
export const environment = {
  production: true
};
```

src/main.ts references this configuration in a newly scaffolded project.

```typescript
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}
```

Notice the environment object is always imported from ./environments/environment but we have different files per environment?

![](https://media.giphy.com/media/a2euXnuLIgVQA/giphy.gif)

The solution is quite simple.

After the AOT compiler runs and outputs JavaScript to the out-tsc directory but before the application is bundled we have to swap the files.

```bash
cp out-tsc/src/environment/environment.prod.js out-tsc/src/environment/environment.js
```

The above snippet uses the cp Unix command to copy the production environment file to the default environment.js.

After the environment.js file is replaced with the current environment, the application is bundled and all references to `environment` in the app correspond to the correct environment.


Create a new function called `handleEnvironment` and pass in the options as an argument. The function is like the others so far, it returns an Observable.

```typescript
export function handleEnvironment(
    options:ClosureBuilderSchema,
    context: BuilderContext
  ): Observable<{}> {

}
```

If we have `env` defined as an option in the schema.json.

```json
"env": {
    "type": "string",
    "description": "Environment to build for (defaults to prod)."
  }
```

We can use the same argument for running this build with the Architect CLI.

```bash
architect build_repo:closure_build --env=prod
```

In the method we just created we can reference the `env` argument on the `options` object.

```typescript
const env = options.env ? options.env : 'prod';
```

To copy the correct environment, we can use a tool available in node called `exec`.

```javascript
import { exec } from 'child_process';
```

`exec` allows you to run bash commands like you normally would in a terminal.

Functions like `exec` that come packaged with node are promised based. Luckily RxJS Observables are interoperable with Promises. We can use the `of` method packaged in `RxJS` to convert `exec` into an Observable. The finished code is below.

```javascript
export function handleEnvironment(
    options:ClosureBuilderSchema,
    context: BuilderContext
  ): Observable<{}> {

    const env = options.env ? options.env : 'prod';

    return of(exec('cp '+
                normalize('out-tsc/src/environments/environment.' + env + '.js') + ' ' +
                normalize('out-tsc/src/environments/environment.js')
             ));
}
```

Add the new method to `executeClosure` with another call to `concatMap`. It should feel like a needle and thread at this point.

```typescript
  return of(context).pipe(
    concatMap( results => ngc(options, context) ),
    concatMap( results => compileMain(options, context)),
    concatMap( results => optimizeBuild(options, context)),
    concatMap( results => handleEnvironment(options, context)),
    concatMap( results => closure(options, context) ),
```

Take a moment to reflect on the build guru you've become. All the steps are now in place for a production build!

![](https://media.giphy.com/media/3o7TKnvDNYADdLYZIQ/giphy.gif)