Angular Universal SSR loading static assets in prerender stage

1.2k Views Asked by At

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?

1

There are 1 best solutions below

0
On BEST ANSWER

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")
}