Search code examples
node.jsangularserver-side-renderingangular-universalpre-rendering

Angular Universal SSR loading static assets in prerender stage


I'm looking for an approach to accessing assets in the /assets/ folder that is used to build the content in a component when prerendering an application. I'm using Angular 14 and the @nguniversal/express-engine package. I can't seem to get static assets to be read in the app when running npm run prerender.

I've seen the discussion at #858 however as the last comment points out this won't work when prerendering.

I have a minimal example of what I mean here: https://stackblitz.com/edit/angular-ivy-dxb32y?file=src%2Fapp%2Fapp.service.ts

You see my service turns the path into an absolute URL:

  public getContents(path: string): Observable<string> {
    if (isPlatformServer(this.platformId) && path.includes('./')) {
      path = `http://localhost:4200/${path.replace('./', '')}`
    }
    return this.http.get(path, {
      observe: 'body',
      responseType: 'text',
    });
  }

And the ssr:dev command serves this content correctly. However, under prerender I get the following error:

⠸ Prerendering 1 route(s) to C:\Users\***\preloading\dist\preloading\browser...ERROR HttpErrorResponse {
  headers: HttpHeaders {
    normalizedNames: Map(0) {},
    lazyUpdate: null,
    headers: Map(0) {}
  },
  status: 0,
  statusText: 'Unknown Error',
  url: 'http://localhost:4200/assets/file.txt',
  ok: false,
  name: 'HttpErrorResponse',
  message: 'Http failure response for http://localhost:4200/assets/file.txt: 0 Unknown Error',

I've tried a few things, such as:

  1. Turning the relative URLs into absolute URLs (https://github.com/angular/universal/issues/858) however this doesn't work during prerender
  2. Using fs to read the static assets however these node modules can't be found during the prerender stage:
if (isPlatformServer(this.platformId) && path.includes('./')) {
     import("fs")
     path = `http://localhost:4200/${path.replace('./', '')}`
   }

Gives:


✔ Browser application bundle generation complete.
⠦ Generating server application bundles (phase: sealing)...
./src/app/app.service.ts:14:8-20 - Error: Module not found: Error: Can't resolve 'fs' in 'C:\Users\***\preloading\src\app'

Error: src/app/app.service.ts:12:14 - error TS2307: Cannot find module 'fs' or its corresponding type declarations.

12       import("fs")

Any other ideas at all about what I can do?


Solution

  • So I managed to crack this using the relatively hacky solution of running both ng serve and npm run prerender using a node script:

    https://stackblitz.com/edit/angular-ivy-uy7wy9?file=prerender.js

    var error = false;
    
    function sleep(miliseconds) {
        console.log(`Sleeping for ${miliseconds} ms`);
        if (miliseconds == 0)
            return Promise.resolve();
        return new Promise(resolve => setTimeout(() => resolve(), miliseconds))
    }
    
    async function run() {
        try {
    
            console.log("Running Angular server");
            var proc = require('child_process').spawn('ng', ['serve']);
            await sleep(20000)
    
            console.log("Running prerender");
            var prerender = require('child_process').spawn('npm', ['run', 'prerender']);
            var prerenderTimeoutSeconds = 120;
            var timeoutObject;
            var timeoutResolve;
            var timeoutReject;
            var timeout = new Promise((resolve, reject) => {
                timeoutResolve = resolve;
                timeoutReject = reject;
                timeoutObject = setTimeout(() => {
                    console.log('Timed out, killing prerender');
                    try {
                        prerender.kill("SIGKILL")
                        reject(Error("Timed out running prerender"))
                    } catch (e) {
                        console.error(e)
                        reject(Error('Cannot kill prerender'));
                    }
                }, prerenderTimeoutSeconds * 1000)
            });
    
            prerender.stdout.on('data', (data) => {
                console.log(`prerender stdout: ${data}`);
            });
    
            prerender.stderr.on('data', (data) => {
                console.error(`prerender stderr: ${data}`);
            });
    
            prerender.on('close', (code) => {
                clearTimeout(timeoutObject);
                console.log(`prerender exited with code ${code}`)
                if (code === 0) {
                    timeoutResolve()
                } else {
                    timeoutReject(Error(`prerender exited with code ${code}`));
                }
            });
    
            await timeout
    
        } catch (err) {
            console.error(err);
            console.error(err.stack);
            error = true;
        } finally {
            if (proc) {
                console.log("Killing Angular server");
                var angularKilled = proc.kill("SIGKILL")
                console.log(`kill -9 on Angular success [${angularKilled}]`)
            }
        }
    }
    
    (async () => await run())();
    
    if (error) {
        throw new Error("Exception during execution")
    }