-
Notifications
You must be signed in to change notification settings - Fork 68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
@nestjs/ng-universal not working with Angular 13 - Error [ERR_REQUIRE_ESM]: require() of ES Module #833
Comments
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. |
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 Now whenever using something that comes from This in itself makes it work but it comes at a price:
I've put my quickly hacked version below: 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: 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: 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: 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 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. |
Wow, that is so clear and elegant, that code is nice and really help for me. thank you for that @chancezeus 👍 ❤️ |
|
}), |
I am having the same problem, I also use nx:
Did you find a solution for this without rewriting stuff from the ng-universal? |
I'm submitting a...
Current behavior
Expected behavior
No TS error.
Minimal reproduction of the problem with instructions
Environment
package.json:
The text was updated successfully, but these errors were encountered: