GithubHelp home page GithubHelp logo

Comments (4)

chancezeus avatar chancezeus commented on April 28, 2024 8

Wanting to have Angular Universal support in NestJS for a while and after quite a few failed attempts I managed to get it working.

To get it to work you'd create a helper function that uses the function constructor to generate a lazy import that won't be messed up by Typescript/Webpack (Typescript/Webpack mangle regular lazy imports to require calls) like this:

export loadEsmModule<T>(modulePath: string | URL): Promise<T> {
    return new Function('modulePath', 'return import(modulePath);')(modulePath);
}

This is similar to how @nguniversal/builders (and other @angular modules) import things when ESM is not (yet) supported (see for example: https://github.com/angular/universal/blob/main/modules/builders/src/utils/utils.ts)

Now whenever using something that comes from @angular (or @nguniversal) libraries (or depends on it), instead of using a regular import {APP_BASE_HREF} from '@angular/common'; or import {Whatever} from 'somethingThatDependsOnAngular'; you would replace that with a const {Whatever} = await loadEsmModule('somethingThatDependsOnAngular');.

This in itself makes it work but it comes at a price:

  1. Since we need an await, you'd either need to allow "top-level await" (requires NodeJS >= 16) or we need to wrap it in a async function (meaning that the forRoot should be changed or a forRootAsync should be added with "factory" support)
  2. The Angular compiler injects additional stuff into the resulting js files, this means for this to function properly, we need to explicitly "lazy import" (using the same loadEsmModule) the ngExpressEngine from the compiled results meaning that it'll have to be exported from the src/main.server.ts and then explicitly provided in the options

I've put my quickly hacked version below:
File: interfaces/angular-universal-options.interface.ts

import { AngularUniversalOptions as BaseOptions } from '@nestjs/ng-universal/dist/interfaces/angular-universal-options.interface';
import { ngExpressEngine } from '@nguniversal/express-engine';

export interface AngularUniversalOptions extends BaseOptions {
  ngExpressEngine: typeof ngExpressEngine;
}

File: utils/setup-universal.utils.ts

import { Logger } from '@nestjs/common';
import { CacheKeyByOriginalUrlGenerator } from '@nestjs/ng-universal/dist/cache/cahce-key-by-original-url.generator';
import { InMemoryCacheStorage } from '@nestjs/ng-universal/dist/cache/in-memory-cache.storage';
import { CacheKeyGenerator } from '@nestjs/ng-universal/dist/interfaces/cache-key-generator.interface';
import { CacheStorage } from '@nestjs/ng-universal/dist/interfaces/cache-storage.interface';
import * as express from 'express';
import { Express, Request } from 'express';

import { AngularUniversalOptions } from '../interfaces/angular-universal-options.interface';

const DEFAULT_CACHE_EXPIRATION_TIME = 60000; // 60 seconds

const logger = new Logger('AngularUniversalModule');

export function setupUniversal(
  app: Express,
  ngOptions: AngularUniversalOptions
) {
  const cacheOptions = getCacheOptions(ngOptions);

  app.engine('html', (_, opts, callback) => {
    const options = opts as unknown as Record<string, unknown>;
    let cacheKey: string | undefined;
    if (cacheOptions.isEnabled) {
      const cacheKeyGenerator = cacheOptions.keyGenerator;
      cacheKey = cacheKeyGenerator.generateCacheKey(options['req']);

      const cacheHtml = cacheOptions.storage.get(cacheKey);
      if (cacheHtml) {
        return callback(null, cacheHtml);
      }
    }

    ngOptions.ngExpressEngine({
      bootstrap: ngOptions.bootstrap,
      inlineCriticalCss: ngOptions.inlineCriticalCss,
      providers: [
        {
          provide: 'serverUrl',
          useValue: `${(options['req'] as Request).protocol}://${(
            options['req'] as Request
          ).get('host')}`,
        },
        ...(ngOptions.extraProviders || []),
      ],
    })(_, options, (err, html) => {
      if (err && ngOptions.errorHandler) {
        return ngOptions.errorHandler({
          err,
          html,
          renderCallback: callback,
        });
      }

      if (err) {
        logger.error(err);

        return callback(err);
      }

      if (cacheOptions.isEnabled && cacheKey) {
        cacheOptions.storage.set(cacheKey, html ?? '', cacheOptions.expiresIn);
      }

      callback(null, html);
    });
  });

  app.set('view engine', 'html');
  app.set('views', ngOptions.viewsPath);

  // Serve static files
  app.get(
    ngOptions.rootStaticPath ?? '*.*',
    express.static(ngOptions.viewsPath, {
      maxAge: 600,
    })
  );
}

type CacheOptions =
  | { isEnabled: false }
  | {
      isEnabled: true;
      storage: CacheStorage;
      expiresIn: number;
      keyGenerator: CacheKeyGenerator;
    };

export function getCacheOptions(
  ngOptions: AngularUniversalOptions
): CacheOptions {
  if (!ngOptions.cache) {
    return {
      isEnabled: false,
    };
  }

  if (typeof ngOptions.cache !== 'object') {
    return {
      isEnabled: true,
      storage: new InMemoryCacheStorage(),
      expiresIn: DEFAULT_CACHE_EXPIRATION_TIME,
      keyGenerator: new CacheKeyByOriginalUrlGenerator(),
    };
  }

  return {
    isEnabled: true,
    storage: ngOptions.cache.storage || new InMemoryCacheStorage(),
    expiresIn: ngOptions.cache.expiresIn || DEFAULT_CACHE_EXPIRATION_TIME,
    keyGenerator:
      ngOptions.cache.keyGenerator || new CacheKeyByOriginalUrlGenerator(),
  };
}

File: angular-universal.module.ts

import { DynamicModule, Inject, Module, OnModuleInit } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { ANGULAR_UNIVERSAL_OPTIONS } from '@nestjs/ng-universal/dist/angular-universal.constants';
import { existsSync } from 'fs';
import { join } from 'path';

import { loadEsmModule } from '../utils/load-esm-module';

import { angularUniversalProviders } from './angular-universal.providers';
import { AngularUniversalOptions } from './interfaces/angular-universal-options.interface';

@Module({
  providers: [...angularUniversalProviders],
})
export class AngularUniversalModule implements OnModuleInit {
  static forRoot(
    configFactory: () =>
      | AngularUniversalOptions
      | Promise<AngularUniversalOptions>
  ): DynamicModule {
    const factory = async (): Promise<AngularUniversalOptions> => {
      const options = await configFactory();

      const indexHtml = existsSync(
        join(options.viewsPath, 'index.original.html')
      )
        ? 'index.original.html'
        : 'index';

      return {
        templatePath: indexHtml,
        rootStaticPath: '*.*',
        renderPath: '*',
        ...options,
      };
    };

    return {
      module: AngularUniversalModule,
      providers: [
        {
          provide: ANGULAR_UNIVERSAL_OPTIONS,
          useFactory: factory,
        },
      ],
    };
  }

  constructor(
    @Inject(ANGULAR_UNIVERSAL_OPTIONS)
    private readonly ngOptions: AngularUniversalOptions,
    private readonly httpAdapterHost: HttpAdapterHost
  ) {}

  async onModuleInit() {
    const { APP_BASE_HREF } = await loadEsmModule<
      typeof import('@angular/common')
    >('@angular/common');

    if (!this.httpAdapterHost) {
      return;
    }

    const httpAdapter = this.httpAdapterHost.httpAdapter;
    if (!httpAdapter) {
      return;
    }

    const app = httpAdapter.getInstance();

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    app.get(this.ngOptions.renderPath, (req: any, res: any) =>
      res.render(this.ngOptions.templatePath, {
        req,
        res,
        providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
      })
    );
  }
}

File: angular-universal.providers.ts

import { Provider } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { ANGULAR_UNIVERSAL_OPTIONS } from '@nestjs/ng-universal/dist/angular-universal.constants';

import { AngularUniversalOptions } from './interfaces/angular-universal-options.interface';
import { setupUniversal } from './utils/setup-universal.utils';

export const angularUniversalProviders: Provider[] = [
  {
    provide: 'UNIVERSAL_INITIALIZER',
    useFactory: (
      host: HttpAdapterHost,
      options: AngularUniversalOptions & { template: string }
    ) =>
      host &&
      host.httpAdapter &&
      setupUniversal(host.httpAdapter.getInstance(), options),
    inject: [HttpAdapterHost, ANGULAR_UNIVERSAL_OPTIONS],
  },
];

I've done this as a new module in my project, replacing the @nestjs/ng-universal parts that gave issues.

Note: as indicated, this is just a quick "hack" to see if I could get it to work using (mostly) the existing implementation, obviously it'll need some cleanup, I posted it mainly as inspiration/poc for the devs.

Now to use it, I'd do something like:

AngularUniversalModule.forRoot(async () => {
    const angularModule = await loadEsmModule<{default: typeof import('../../src/main.server')}>(join(process.cwd(), 'dist/ProjectName/server/main.js'));
    
    return {
        bootstrap: angularModule.default.AppServerModule,
        ngExpressEngine: angularModule.default.ngExpressEngine,
        viewsPath: join(process.cwd(), 'dist/ProjectName/browser'),
    };
}),

Note: this also applies to the situation where the nestjs and angular projects are separate (for example using Nx), only thing that might change is the paths to angular sources and server/browser dist versions.

from ng-universal.

LeParadoxHD avatar LeParadoxHD commented on April 28, 2024

Looks like it's because NestJS is only compatible with CommonJS and Angular 13+ removed support for CommonJS, so this issue should be resolved once it supports ESM imports.

nestjs/nest#8736

from ng-universal.

hiepxanh avatar hiepxanh commented on April 28, 2024

Wow, that is so clear and elegant, that code is nice and really help for me. thank you for that @chancezeus 👍 ❤️

|

AngularUniversalModule.forRoot(async () => {
const angularModule = await loadEsmModule<{default: typeof import('../../src/main.server')}>(join(process.cwd(), 'dist/ProjectName/server/main.js'));

|

return {
    bootstrap: angularModule.default.AppServerModule,
    ngExpressEngine: angularModule.default.ngExpressEngine,
    viewsPath: join(process.cwd(), 'dist/ProjectName/browser'),
};

}),

from ng-universal.

florinmtsc avatar florinmtsc commented on April 28, 2024

I am having the same problem, I also use nx:

node_modules/.pnpm/@[email protected]_@[email protected]_@[email protected][email protected][email protected]/node_modules/@nx/js/src/executors/node/node-with-require-overrides.js:18
        return originalLoader.apply(this, arguments);

Did you find a solution for this without rewriting stuff from the ng-universal?

from ng-universal.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.