angular-cli: SSR with i18n with Angular 9 not working

🐞 Bug report

What modules are related to this issue?

  • aspnetcore-engine
  • builders
  • common
  • express-engine
  • hapi-engine
  • module-map-ngfactory-loader

Is this a regression?

No, localize is new to Angular 9.

Description

The distFolder is hardcoded in ./server.ts. When the browser server assets are built with localize: true, the assets are placed in a subfolder with the locale name (eg: dist/{appName}/browser/{locale}/ and dist/{appName}/server/{locale}). Now the server can no longer find the correct directory for the browser assets and fails to render.

Is there any way server.ts can know location of the browser assets without hardcoding the path?

Thanks.

🔬 Minimal Reproduction

ng new ng-sample
ng add @angular/localize@next
ng add @nguniversal/express-engine@next

add localize: true to the options of the build and server architect

ng build --prod
ng run ng-sample:server:production
node dist/ng-sample/server/en-US/main.js

browser to http://localhost:4000

🔥 Exception or Error

Error: Failed to lookup view "index" in views directory "/code/ng-sample/dist/ng-sample/browser"
    at Function.render (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:1122933)
    at ServerResponse.render (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:1398756)
    at server.get (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:2259271)
    at Layer.handle [as handle_request] (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:1144375)
    at next (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:1131913)
    at Route.dispatch (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:1131942)
    at Layer.handle [as handle_request] (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:1144375)
    at /code/ng-sample/dist/ng-sample/server/en-US/main.js:1:2473361
    at param (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:2474870)
    at param (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:2475277)

🌍 Your Environment

Angular CLI: 9.0.0-rc.9
Node: 10.16.0
OS: darwin x64

Angular: 9.0.0-rc.9
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, localize, platform-browser
... platform-browser-dynamic, platform-server, router
Ivy Workspace: Yes

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.900.0-rc.9
@angular-devkit/build-angular     0.900.0-rc.9
@angular-devkit/build-optimizer   0.900.0-rc.9
@angular-devkit/build-webpack     0.900.0-rc.9
@angular-devkit/core              9.0.0-rc.9
@angular-devkit/schematics        9.0.0-rc.9
@ngtools/webpack                  9.0.0-rc.9
@nguniversal/builders             9.0.0-rc.0
@nguniversal/common               9.0.0-rc.0
@nguniversal/express-engine       9.0.0-rc.0
@schematics/angular               9.0.0-rc.9
@schematics/update                0.900.0-rc.9
rxjs                              6.5.4
typescript                        3.6.4
webpack                           4.41.2

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 21
  • Comments: 53 (8 by maintainers)

Commits related to this issue

Most upvoted comments

@piotrbrzuska solution worked for me.

Basically, I did the following:

server.ts:

export function app(locale) {
    const server = express();

    server.engine(
        'html',
        ngExpressEngine({
            bootstrap: AppServerModule,
        })
    );

    const distPath = join(process.cwd(), `dist/my-app/browser/${locale}`);

    //server.set('views', distPath);
    //server.set('view engine', 'html');

    server.get(
        '*.*',
        express.static(distPath, {
            maxAge: '1y',
        })
    );

    server.get('*', (req, res) => {
        res.render(join(distPath, 'index.html'), {
            req,
            providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
        });
    });

    return server;
}

export * from './src/main.server';

Then I created a separate server.run.js with this:


function app() {
    const server = express();

    ['ca', 'en', 'en-gb', 'es'].forEach((locale) => {
        const appServerModule = require(path.join(__dirname, 'dist', 'my-app', 'server', locale, 'main.js'));
        server.use(`/${locale}`, appServerModule.app(locale));
    });

    return server;
}

function run() {
    app().listen(4200, () => {
        console.log(`Node Express server listening on http://localhost:4200`);
    });
}

run();


Any update about this ?

I see this issue has been open since 2020, are there any plans to fix it?

any solution to run application (with multiple language) on single express port???

I am in the same boat: Previously I could import multiple bundles and load them dynamically based on the URL that was requested. Now I need to run one server for each language, this is quite tedious.

I am sharing ready working solution for Angular 10 on one port based on your answers 🚀🚀

Repositorium

Angular documentation is so deprecated, maybe this gonna helps someone 😉

@PowerKiKi and @schippie - thanks for the tips!

Indeed I managed to get npm run prerender to work without any hacks and with a single change in angular.json - only had to add "localize": ["en", "ab", "cd"] under “server” -> “configurations” -> “production”` (to match the regular “build” “production” configuration).

It seems that the ng add @nguniversal/express-engine schematics don’t copy over the localize value when generating the “server” configuration. I believe this can be fixed in the schematics to improve developer experience.

(Clarification: For now I’m only doing prerendering, which worked well without hacks - just by adding one localize line to angular.json like I mentioned. I haven’t fully tried SSR yet, but npm run dev:ssr seems to work too.)

Any update of this, at incoming Angular 10 ?

Hi, Is there any progress about this issue?

Same here. I adopted same setup for my production site as mentioned by @keserwan in angular/universal#1497, which is now broken.

Besides, look like this block of code is wrong. “req.baseUrl” causes app routing on server side to fail:

server.get('*', (req, res) => {
	res.render(indexHtml, {req, providers: [{provide: APP_BASE_HREF, useValue: req.baseUrl}]});
\});

I changed it as follows, and the routing works again:

const baseHref = '/en/';
server.get(baseHref + '*', (req, res) => {
	res.render(indexHtml, {req, providers: [{provide: APP_BASE_HREF, useValue: baseHref}]});
});

To add onto @marcmarcet 's workaround:

  1. It works (thank you so much!)
  2. It shouldn’t be necessary, SSR with i18n is not an exotic use case.
  3. When specifying multiple locales in angular.json, make sure to end the baseHref with a /, otherwise view-source will have the prerendered texts in the correct language, but as soon as the main.js bundle is loaded from /, the texts are replaced with the default language again.
  4. Here’s a version that hosts the default locale (e.g. en-US) on /:
const defaultLocale = "en-US";

for (const locale of ["de", defaultLocale]) {
  const appServerModule = require(path.join(__dirname, "dist", "frontend", "server", locale, "main.js"));
  server.use(locale == defaultLocale ? "/" : `/${locale}`, appServerModule.app(locale));
}
"i18n": {
    "sourceLocale": {
      "code": "en-US",
      "baseHref": "/" // <--
    },
    ...

I know this was a little bit off-topic, but I can’t be the only one struggling with this, perhaps it helps someone.

Ok, I spend about a one work day, but I’m do it.

a idea: to have files like ~/dist/app-name/server/{locale}/main.js and one main ~/server.js, which start all locales servers as modules:

var express = require('express')
const server = express();

var locals = ["en", "pl", "fr", "de"]; 
for (let idx = 0; idx < locals.length; idx++) {
    const local = locals[idx];
    var localModule = require(`./dist/app-name/server/${local}/main.js`);
    server.use('/'+local, localModule.app(local));
    idx == 0 && server.use('/', localModule.app(local)); // use first locale as main locale
}
const port = process.env.PORT || 4000;
server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
});

other things i must change is pass Locale_ID to APP_BASE_HREF in my (Browser) AppModule.

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
 // ...
  ],
  providers: [
// ....
    { provide: APP_BASE_HREF, useFactory: (locale: string) => locale, deps: [LOCALE_ID] },
// ...
  ],
  bootstrap: [AppComponent]
})
export class AppModule { 

I made it work with 2 node apps working on the server. If anybody can come up with 1 node app serving all locales that would be perfect as we were doing it with webpack.server.config previously.

You can probably use the simpler "localize": true, instead of repeating every locale. That’s what we do.

Here we never prerender, we only render live. Glad it could also work for prerendering then 👍

My solution requires no hacks and it still in use in production today with Angular 15. You might want to try that if the rest is not working for you.

Overall however the approach highlighted above in this comment #1454 (comment) is the recommended approach for build time i18n.

@alan-agius4 from my understanding, the recommended approach includes a hack from https://github.com/angular/universal/issues/1689 inside node_modules. That sounds very problematic, as the hack will get erased on npm install (unless we add more hacks).

Angular Universal and Angular i18n are two major Angular features, and I was expecting them to work together seamlessly by v15. For instance, when you run ng add @nguniversal/express-engine, the schematics should check that you’re using i18n and update the server.ts code accordingly. npm run prerender isn’t working for me either.

Could you guys please revisit this issue or recommend a workaround for Angular 15 prerendering with i18n?


A bit more context on my use-case:

  • App in 3 languages, ran ng add @nguniversal/express-engine
  • Then tried npm run prerender => error in node_modules/@nguniversal/builders/src/prerender/index.js: An unhandled exception occurred: Could not find the main bundle: dist/my-project/server/en/main.js
  • Indeed there is no such file because there isn’t an /en/ folder under server/:
    $ ls dist/my-project/server/
    269.js  426.js  562.js  793.js  875.js  50.js   623.js  868.js  main.js  3rdpartylicenses.txt  (files)
    
    $ ls dist/my-project/browser/
    en  ab  cd  (folder per locale)
    
  • The error comes from this line: https://github.com/angular/universal/blob/b5b9c1761fc647f6dc64187d443a72fcee306cf2/modules/builders/src/prerender/index.ts#L116

Update: npm run prerender worked after adding "localize": ... to angular.json under "server" -> "configurations" -> "production" (see comment below).

Here is the entire commit (minus my app very specific changes) that I used to add SSR on an Angular 10 app that already used i18n: https://gist.github.com/PowerKiKi/b8ecd4bdfb3f4d694a1370e3c57a5062

It is based on the server.run.js solution. But it automatically gets locales from angular.json (so no duplicated config). And it automatically use the proxy config that you might need for your local API.

server.ts still has its run() function in order to run yarn dev-ssr, although the app still fails because of incorrect baseHref. And it has a full configuration for pm2 where you can see that server.run.js is the main entry point (and not server.ts anymore).

And to be extra complete here is the relevant nginx configuration to proxy bots, but no humans, to the SSR.

It turns out that the problem comes from angularFire (firebase/firestore). Any data query using a rxjs pipe with take(1) in the application, cause angular universal to get stuck in a infinite loop until the server timeout… https://github.com/angular/angularfire/issues/2420