Comments (4)
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:
- 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 theforRoot
should be changed or aforRootAsync
should be added with "factory" support) - 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
) thengExpressEngine
from the compiled results meaning that it'll have to be exported from thesrc/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.
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.
from ng-universal.
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.
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)
- Add support for Angular 16 HOT 1
- Missing domino dependency HOT 1
- Vulnerable reason to stay here with ^0.13.1 and not ^0.14.0 HOT 1
- errorHandler does not includes the request to be able to respond something.
- Prerender with standalone application don't work HOT 1
- Nestjs ng-universal package dependency errors HOT 6
- Support in Angular 17 HOT 6
- Angular 13 + ng add @nestjs/ng-universal --force (Package installed failed) HOT 2
- Can't resolve 'class-transformer/storage' HOT 2
- `npm run dev:ssr` fails to run on MacOS HOT 2
- TypeError: Class constructor Type cannot be invoked without 'new' - nest runtime error when using angular library + mikro orm HOT 7
- Please make cache methods asynchronous HOT 1
- That package not work HOT 1
- minimal app with "Error: Module parse failed" HOT 2
- Would really love to use this library with latest major nestjs version (v9) HOT 1
- [FR] Add community maintainers to this package HOT 3
- Collection "@nestjs/ng-universal" cannot be resolved HOT 1
- Angular 15 ng add dependency issue 'Packages installation failed' HOT 6
- Can't start or build server application HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from ng-universal.