Build Angular Like An Architect (Part 2)

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) 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:
export function executeClosure(
options: ClosureBuilderSchema,
context: BuilderContext
): Observable {
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.
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.
npm i @angular-devkit/build-optimizer –save-dev

Import buildOptimizer like so.
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.
npm i glob –save-dev

Import glob into src/closure/index.ts.
import { glob } from ‘glob’;

In the optimizeBuild method, declare a new const and call it files.
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.

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.

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:
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.
export const environment = {
production: true
};

src/main.ts references this configuration in a newly scaffolded project.
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?

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.
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.
export function handleEnvironment(
options:ClosureBuilderSchema,
context: BuilderContext
): Observable<{}> {

}

If we have env defined as an option in the schema.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.
architect build_repo:closure_build –env=prod

In the method we just created we can reference the env argument on the options object.
const env = options.env ? options.env : ‘prod’;

To copy the correct environment, we can use a tool available in node called exec.
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.
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.
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!

In Part 3 of Build Angular Like An Architect we will make our build even more robust and user friendly. We’ll add progress tracking, styled logs and even write some unit tests for the build. See ya then!

Link: https://dev.to/steveblue/build-angular-like-an-architect-part-2-208l