Prerendering in Angular  - Part III
Tags: javascript, webdev, tutorial, angular
Published: 2022-10-03 10:07:47

Angular recently added package for [prerendering](https://angular.io/guide/prerendering), which allows us to create prerendered pages without building a server. Let us go through the basics of it first before we create our spin-off.

### Prerendering for browser-only applications

To run the prerender feature we have to at least install the following packages

```js
// npm packages
@angular/platform-server
@nguniversal/common
// dev dependencies
@nguniversal/builders
```

In `angular.json` setup few **targets**, one for browser build, one for server build, and one for prerender builder.

{% embed https://gist.github.com/ayyash/32b17ddcf59bf662984bc7ec2d29cbba  %}

Assuming this is only a browser platform application, and there is no express server to serve the files, and assuming the default `tsconfig.server.json` that has `server.ts` as the main file to compile, the `server.ts` need not have anything but the following:

```ts
// bare minimum server.ts, there is no need for an express server if you are not
// going to use SSR in the wild
// no need for a main.server,ts
import 'zone.js/dist/zone-node';
import { enableProdMode} from '@angular/core';
import { environment } from './src/environments/environment';

// following lines is for prerender to work
export { AppServerModule } from './src/app/app.server.module';
export { renderModule } from '@angular/platform-server';

// if you somehow use 'window' property loosely 
// (or any global properties that work only in browser)
global.window = undefined;

// if you use localStorge on the client, you might want to add this
global.localStorage = {
  key: function(index) {
    return null;
  },
  getItem: function (key) {
    return null;
  },
  setItem: function (key, value) {
  },
  clear: function () {
  },
  removeItem: function (key) {
  },
  length: 0
};

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

The `AppServerModule` is usually under `/src/app/app.server.module.ts`, and it is the default that comes with Angular CLI:

```ts
// src/app/app.server.module.ts
// basic 
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';

@NgModule({
  imports: [
    AppModule,
    ServerModule,
  ],
  providers: [
    // Add server-only providers here.
  ],
  bootstrap: [AppComponent],
})
export class AppServerModule {}
```

Now run `ng run cr:prerender`. This will generate two folders in the `host` output folder, `client` and `server`. The `client` folder will hold the static files to prerender, and it is the folder you should be deploying to your browser-only host. The `server` is not for use.

If you have SSR in place, your `server.ts` should have the `ngExpressEngine` as usual, but none of it affects the prerendering process.

That is the out-of-the-box builder. But...

> *Libraries are written for 16 personality types, and usually misses the one scenario you need.\
> ---*I said.

### Prerender builder local version

The above builder starts with the scheduling a browser and a server build with `@nguniversal/builders`. It generally is a good library, once you figure out what you're doing, but the one feature I personally wanted to have is to use it "post" build, to control which `index.html` file to serve. This helps us in our malicious intentions to [replace Angular localization package](https://garage.sekrab.com/posts/alternative-way-to-localize-in-angular) with our own multilingual single-build app. So before we build our solution, let's [open the box](https://github.com/angular/universal/tree/main/modules/builders/src/prerender) and find out if we can have our own spin-off.

-   `index.ts` runs two builders first then iterates through routes and runs a [Piscina worker](https://www.npmjs.com/package/piscina) to render every route
-   `worker.ts` uses `renderModule` exported from the server build, saves the file in a sub folder under client (with name `index.html`)
-   It also saves a copy from the original index, into `index.original.html`, this won't be necessary when we feed our own unique index file (next episode).
-   The builder uses [ora](https://www.npmjs.com/package/ora) library for logging out on console
-   It is written in `CommonJs`, and uses **dynamic imports** to import the server build
-   It takes care of `serviceWorker`. I will remove this part to simplify. (One fine Tuesday we'll dig into service workers.)

The first attempt is to reduce the schedule builder statements so that it only uses what was already built. I have created a gist with the [skimmed down version of the builder](https://gist.github.com/ayyash/2775f0c24ef09f0036095f2c0d0a3c4a), removing the parts where a build goes through `scheduleBuilder`.

> *Note: As usual, we create a subfolder for the builder, and let it have its own* `*npm*` *packages, for simplicity. If you want to run the same (or skimmed) builder code from your sub folder, the one key in* `*tsconfig*` *that made the difference was* `*"esModuleInterop": true*`

The variables needed for the worker are:

```ts
// index.ts main builder function
export async function execute(
  options: IOptions,
  context: BuilderContext,
): Promise<BuilderOutput> {
	// ...
	return _renderUniversal(
    routes, // we will pass these as an array
    context, // given in builder
    browserResult, // to replace
    serverResult, // to replace
    browserOptions, // to find out what's needed
    options.numProcesses, // hmm, okay for now
  );
}
```

The `browserResult` and `serverResult` models give us a hint to which variables we need to provide, without scheduling a build.

```ts
// the least number of properties to replace
const [platform]Result = {
  // in multiple builds for localization (Angular default behavior), this would be populated
  // in our case, it is just one
  outputPaths: [outputPath],
  
  // the following are identical, outputPath is never used
  // we need two values one for browser and one for server
  baseOutputPath: outputPath,
  outputPath: outputPath,
  
  // from BuilderOutput
  success: true
}
```

As for `browserOptions`:

```js
// browserOptions used
// to form the outPath and baseOutputPath
browserOptions.outputPath

// to read the path base of the index file
browserOptions.index

// to minifyCss and add inline critical CSS
// we can assume this to be true always
browserOptions.optimization

// the following we will not make use of
browserOptions.deployUrl // for inline critical CSS, deprecated
browserOptions.tsConfig // for guessRoutes
browserOptions.serviceWorker // for serviceWorker
browserOptions.baseHref // serviceWorker
browserOptions.ngswConfigPath // serviceWorker
```

To get the `baseOutputPath` we can use the target options as follows:

{% embed https://gist.github.com/ayyash/2ecb31ad9977e293254d26b90dde7a34 %}

The basic `_renderUniversal` function: we will simplify and assume a single language, so there will be no loop in `outputPaths`

{% embed https://gist.github.com/ayyash/49423e0b919069cd9ac7e4819c6ad191 %}

In `worker.ts` we will export the main function `PreRender`, the `RenderOptions` can be simplified

```ts
// simplified RenderOptions
export interface RenderOptions {
  indexFile: string;
  clientPath: string;
  serverBundlePath: string;
  route: string
}
```

Now the worker `PreRender` function:

{% embed https://gist.github.com/ayyash/7f1109ad7e9bd54f1a76942b2ca57271 %}

In our `schema.json` we only need the following props

```js
// schema props
{
  browserTarget,
  serverTarget,
  routes // array of routes
}
```

Running, it works as expected but fails when it references our `global` object. The solution is to add the necessary global references to the worker file (outside any scope):

```js
// worker.ts, add global references
global.window = undefined;
global.localStorage = {
  key: function (index) {
    return null;
  },
  getItem: function (key) {
    return null;
  },
  setItem: function (key, value) {
  },
  clear: function () {
  },
  removeItem: function (key) {
  },
  length: 0
}; 
```

The other needed files are in [StackBlitz prerender-builder](https://stackblitz.com/edit/angular-express-prerender) folder, including the adjustment to `angular.json`. Now we transpile (in StackBlitz that's running `npx tsc` inside our sub folder), and then run:

`ng run cr:prerender`

Checkout the output folder `host-builder` in StackBlitz. The static files were created in the client folder as expected.

### Twisting and turning

Now we need to provide our own `index.[lang].html` and create our language subfolders, in addition to passing global locale files. For that, let's meet next episode. 😴

Thank you for reading this far, I hope it was not so confusing, was it?

### RESOURCES

-   [Angular --- Prerendering Static Pages](https://angular.io/guide/prerendering)
-   [Angular universal prerender builder source code](https://github.com/angular/universal/tree/main/modules/builders/src/prerender)
-   [Piscina worker](https://www.npmjs.com/package/piscina)
-   [Skimmed down version of the builder --- Gist](https://gist.github.com/ayyash/2775f0c24ef09f0036095f2c0d0a3c4a)
-   [Ora logger](https://www.npmjs.com/package/ora)
-   [Stackblitz project](https://stackblitz.com/edit/angular-express-prerender)

### RELATED POSTS

- [Alternative way to localize in Angular](https://garage.sekrab.com/posts/alternative-way-to-localize-in-angular)