Authored by lea guo

nest 整合next

export const RENDER_METADATA = '@@module/render/RENDER_METADATA';
... ...
/**
* @see https://github.com/kyle-mccarthy/nest-next
*/
export * from './constants';
export * from './render.filter';
export * from './render.module';
export * from './render.service';
export * from './types';
... ...
/**
* @description This file contains snippets of code taken directly from the nextjs repo
* @see https://github.com/zeit/next.js/tree/canary/packages/next-server/server
* @copyright 2016-present ZEIT, Inc.
*
* The MIT License (MIT)
*
* Copyright (c) 2016-present ZEIT, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const internalPrefixes = [/^\/_next\//, /^\/static\//];
export function isInternalUrl(url: string): boolean {
for (const prefix of internalPrefixes) {
if (prefix.test(url)) {
return true;
}
}
return false;
}
... ...
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { IncomingMessage, ServerResponse } from 'http';
import { parse as parseUrl } from 'url';
import { RenderService } from './render.service';
import { ErrorResponse } from './types';
@Catch()
export class RenderFilter implements ExceptionFilter {
private readonly service: RenderService;
constructor(service: RenderService) {
this.service = service;
}
/**
* Nest isn't aware of how next handles routing for the build assets, let next
* handle routing for any request that isn't handled by a controller
* @param err
* @param ctx
*/
public async catch(err: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest();
const response = ctx.getResponse();
if (response && request) {
const requestHandler = this.service.getRequestHandler();
const errorRenderer = this.service.getErrorRenderer();
// these really should already always be set since it is done during the module registration
// if somehow they aren't throw an error
if (!requestHandler || !errorRenderer) {
throw new Error(
'Request and/or error renderer not set on RenderService',
);
}
const res: ServerResponse = response.res ? response.res : response;
const req: IncomingMessage = request.raw ? request.raw : request;
if (!res.headersSent && req.url) {
// check to see if the URL requested is an internal nextjs route
// if internal, the url is to some asset (ex /_next/*) that needs to be rendered by nextjs
if (this.service.isInternalUrl(req.url)) {
return requestHandler(req, res);
}
// let next handle the error
// it's possible that the err doesn't contain a status code, if this is the case treat
// it as an internal server error
res.statusCode = err && err.status ? err.status : 500;
const { pathname, query } = parseUrl(req.url, true);
const errorHandler = this.service.getErrorHandler();
if (errorHandler) {
await errorHandler(err, request, response, pathname, query);
}
if (response.sent === true || res.headersSent) {
return;
}
const serializedErr = this.serializeError(err);
return errorRenderer(serializedErr, req, res, pathname, query);
}
return;
}
// if the request and/or response are undefined (as with GraphQL) rethrow the error
throw err;
}
/**
* Serialize the error similarly to method used in Next -- parse error as Nest error type
* @param err
*/
public serializeError(err: any): ErrorResponse {
const out: ErrorResponse = {};
if (!err) {
return out;
}
if (err.stack && this.service.isDev()) {
out.stack = err.stack;
}
if (err.response && typeof err.response === 'object') {
const { statusCode, error, message } = err.response;
out.statusCode = statusCode;
out.name = error;
out.message = message;
} else if (err.message && typeof err.message === 'object') {
const { statusCode, error, message } = err.message;
out.statusCode = statusCode;
out.name = error;
out.message = message;
}
if (!out.statusCode && err.status) {
out.statusCode = err.status;
}
if (!out.message && err.message) {
out.message = err.message;
}
return out;
}
}
... ...
import { INestApplication, Module } from '@nestjs/common';
import { RenderFilter } from './render.filter';
import Server from 'next';
import { RenderService } from './render.service';
import { RendererConfig } from './types';
type INestAppliactionSubset = Pick<
INestApplication,
'getHttpAdapter' | 'useGlobalFilters'
> &
Partial<INestApplication>;
@Module({
providers: [RenderService],
})
export class RenderModule {
private app?: INestAppliactionSubset;
private server?: ReturnType<typeof Server>;
constructor(private readonly service: RenderService) {}
public register(
app: INestAppliactionSubset,
server: ReturnType<typeof Server>,
options: Partial<RendererConfig> = {},
) {
this.app = app;
this.server = server;
this.service.mergeConfig(options);
this.service.setRequestHandler(this.server.getRequestHandler());
this.service.setRenderer(this.server.render.bind(this.server));
this.service.setErrorRenderer(this.server.renderError.bind(this.server));
this.service.bindHttpServer(this.app.getHttpAdapter());
this.app.useGlobalFilters(new RenderFilter(this.service));
}
}
... ...
import {
HttpServer,
Injectable,
InternalServerErrorException,
} from '@nestjs/common';
import { ExpressAdapter } from '@nestjs/platform-express';
import { isInternalUrl } from './next-utils';
import {
ErrorHandler,
ErrorRenderer,
Renderer,
RendererConfig,
RequestHandler,
} from './types';
@Injectable()
export class RenderService {
private requestHandler?: RequestHandler;
private renderer?: Renderer;
private errorRenderer?: ErrorRenderer;
private errorHandler?: ErrorHandler;
private config: RendererConfig = {
dev: process.env.NODE_ENV !== 'production',
viewsDir: '/views',
};
/**
* Merge the default config with the config obj passed to method
* @param config
*/
public mergeConfig(config: Partial<RendererConfig>) {
if (typeof config.dev === 'boolean') {
this.config.dev = config.dev;
}
if (typeof config.viewsDir === 'string' || config.viewsDir === null) {
this.config.viewsDir = config.viewsDir;
}
}
/**
* Set the directory that Next will render pages from
* @param path
*/
public setViewsDir(path: string | null) {
this.config.viewsDir = path;
}
/**
* Get the directory that Next renders pages from
*/
public getViewsDir() {
return this.config.viewsDir;
}
/**
* Explicitly set if env is or not dev
* @param dev
*/
public setIsDev(dev: boolean) {
this.config.dev = dev;
}
/**
* Get if the env is dev
*/
public isDev(): boolean {
return this.config.dev!;
}
/**
* Set the default request handler provided by next
* @param handler
*/
public setRequestHandler(handler: RequestHandler) {
this.requestHandler = handler;
}
/**
* Get the default request handler
*/
public getRequestHandler(): RequestHandler | undefined {
return this.requestHandler;
}
/**
* Set the render function provided by next
* @param renderer
*/
public setRenderer(renderer: Renderer) {
this.renderer = renderer;
}
/**
* Get the render function provided by next
*/
public getRenderer(): Renderer | undefined {
return this.renderer;
}
/**
* Set nextjs error renderer
* @param errorRenderer
*/
public setErrorRenderer(errorRenderer: ErrorRenderer) {
this.errorRenderer = errorRenderer;
}
/**
* Get nextjs error renderer
*/
public getErrorRenderer(): ErrorRenderer | undefined {
return this.errorRenderer;
}
/**
* Set a custom error handler
* @param handler
*/
public setErrorHandler(handler: ErrorHandler) {
this.errorHandler = handler;
}
/**
* Get the custom error handler
*/
public getErrorHandler() {
return this.errorHandler;
}
/**
* Bind to the render function for the HttpServer that nest is using and override
* it to allow for next to render the page
* @param server
*/
public bindHttpServer(server: HttpServer) {
const renderer = this.getRenderer();
const getViewPath = this.getViewPath.bind(this);
server.render = (response: any, view: string, data: any) => {
const isFastify = response.request !== undefined;
const res = isFastify ? response.res : response;
const req = isFastify ? response.request.raw : response.req;
if (req && res && renderer) {
return renderer(req, res, getViewPath(view), data);
} else if (!renderer) {
throw new InternalServerErrorException(
'RenderService: renderer is not set',
);
} else if (!res) {
throw new InternalServerErrorException(
'RenderService: could not get the response',
);
} else if (!req) {
throw new InternalServerErrorException(
'RenderService: could not get the request',
);
}
throw new Error('RenderService: failed to render');
};
// and nextjs renderer to reply/response
if (server instanceof ExpressAdapter) {
server
.getInstance()
.decorateReply('render', function(view: string, data?: any) {
const res = this.res;
const req = this.request.raw;
if (!renderer) {
throw new InternalServerErrorException(
'RenderService: renderer is not set',
);
}
return renderer(req, res, getViewPath(view), data);
});
} else {
server.getInstance().use((req: any, res: any, next: () => any) => {
res.render = (view: string, data?: any) => {
if (!renderer) {
throw new InternalServerErrorException(
'RenderService: renderer is not set',
);
}
return renderer(req, res, getViewPath(view), data);
};
next();
});
}
}
/**
* Check if the URL is internal to nextjs
* @param url
*/
public isInternalUrl(url: string): boolean {
return isInternalUrl(url);
}
/**
* Format the path to the view
* @param view
*/
protected getViewPath(view: string) {
const baseDir = this.getViewsDir();
const basePath = baseDir ? baseDir : '';
return `${basePath}/${view}`;
}
}
... ...
import { ParsedUrlQuery } from 'querystring';
export type RequestHandler = (req: any, res: any, query?: any) => Promise<void>;
export type Renderer = (
req: any,
res: any,
view: string,
params?: any,
) => Promise<void>;
export type ErrorRenderer = (
err: any,
req: any,
res: any,
pathname: any,
query?: any,
) => Promise<void>;
export type ErrorHandler = (
err: any,
req: any,
res: any,
pathname: any,
query: ParsedUrlQuery,
) => Promise<any>;
export interface RendererConfig {
viewsDir: null | string;
dev: boolean;
}
export interface ErrorResponse {
name?: string;
message?: string;
stack?: any;
statusCode?: number;
}
... ...
... ... @@ -32,7 +32,7 @@
},
"devDependencies": {
"@nestjs/testing": "^6.0.0",
"@types/express": "4.16.1",
"@types/express": "^4.17.1",
"@types/jest": "24.0.11",
"@types/node": "11.13.4",
"@types/supertest": "2.0.7",
... ...
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { RenderModule } from '../nest-next';
@Module({
imports: [],
imports: [RenderModule],
controllers: [AppController],
providers: [AppService],
})
... ...
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import Next from 'next';
import { RenderModule } from '../nest-next';
import 'reflect-metadata';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
const dev = process.env.NODE_ENV !== 'production';
const app = Next({ dev });
await app.prepare();
const server = await NestFactory.create(AppModule);
const renderer = server.get(RenderModule);
renderer.register(server, app);
await server.listen(process.env.PORT || 3000);
}
bootstrap();
... ...
... ... @@ -1096,10 +1096,10 @@
"@types/node" "*"
"@types/range-parser" "*"
"@types/express@4.16.1":
version "4.16.1"
resolved "https://registry.npm.taobao.org/@types/express/download/@types/express-4.16.1.tgz#d756bd1a85c34d87eaf44c888bad27ba8a4b7cf0"
integrity sha1-11a9GoXDTYfq9EyIi60nuopLfPA=
"@types/express@^4.17.1":
version "4.17.1"
resolved "https://registry.npm.taobao.org/@types/express/download/@types/express-4.17.1.tgz#4cf7849ae3b47125a567dfee18bfca4254b88c5c"
integrity sha1-TPeEmuO0cSWlZ9/uGL/KQlS4jFw=
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "*"
... ...