Authored by lea guo

nest 整合next

  1 +export const RENDER_METADATA = '@@module/render/RENDER_METADATA';
  1 +/**
  2 + * @see https://github.com/kyle-mccarthy/nest-next
  3 + */
  4 +export * from './constants';
  5 +export * from './render.filter';
  6 +export * from './render.module';
  7 +export * from './render.service';
  8 +export * from './types';
  1 +/**
  2 + * @description This file contains snippets of code taken directly from the nextjs repo
  3 + * @see https://github.com/zeit/next.js/tree/canary/packages/next-server/server
  4 + * @copyright 2016-present ZEIT, Inc.
  5 + *
  6 + * The MIT License (MIT)
  7 + *
  8 + * Copyright (c) 2016-present ZEIT, Inc.
  9 + *
  10 + * 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:
  11 + *
  12 + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  13 + *
  14 + * 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.
  15 + */
  16 +const internalPrefixes = [/^\/_next\//, /^\/static\//];
  17 +
  18 +export function isInternalUrl(url: string): boolean {
  19 + for (const prefix of internalPrefixes) {
  20 + if (prefix.test(url)) {
  21 + return true;
  22 + }
  23 + }
  24 +
  25 + return false;
  26 +}
  1 +import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
  2 +import { IncomingMessage, ServerResponse } from 'http';
  3 +import { parse as parseUrl } from 'url';
  4 +import { RenderService } from './render.service';
  5 +import { ErrorResponse } from './types';
  6 +
  7 +@Catch()
  8 +export class RenderFilter implements ExceptionFilter {
  9 + private readonly service: RenderService;
  10 +
  11 + constructor(service: RenderService) {
  12 + this.service = service;
  13 + }
  14 +
  15 + /**
  16 + * Nest isn't aware of how next handles routing for the build assets, let next
  17 + * handle routing for any request that isn't handled by a controller
  18 + * @param err
  19 + * @param ctx
  20 + */
  21 + public async catch(err: any, host: ArgumentsHost) {
  22 + const ctx = host.switchToHttp();
  23 + const request = ctx.getRequest();
  24 + const response = ctx.getResponse();
  25 +
  26 + if (response && request) {
  27 + const requestHandler = this.service.getRequestHandler();
  28 + const errorRenderer = this.service.getErrorRenderer();
  29 +
  30 + // these really should already always be set since it is done during the module registration
  31 + // if somehow they aren't throw an error
  32 + if (!requestHandler || !errorRenderer) {
  33 + throw new Error(
  34 + 'Request and/or error renderer not set on RenderService',
  35 + );
  36 + }
  37 +
  38 + const res: ServerResponse = response.res ? response.res : response;
  39 + const req: IncomingMessage = request.raw ? request.raw : request;
  40 +
  41 + if (!res.headersSent && req.url) {
  42 + // check to see if the URL requested is an internal nextjs route
  43 + // if internal, the url is to some asset (ex /_next/*) that needs to be rendered by nextjs
  44 + if (this.service.isInternalUrl(req.url)) {
  45 + return requestHandler(req, res);
  46 + }
  47 +
  48 + // let next handle the error
  49 + // it's possible that the err doesn't contain a status code, if this is the case treat
  50 + // it as an internal server error
  51 + res.statusCode = err && err.status ? err.status : 500;
  52 +
  53 + const { pathname, query } = parseUrl(req.url, true);
  54 +
  55 + const errorHandler = this.service.getErrorHandler();
  56 +
  57 + if (errorHandler) {
  58 + await errorHandler(err, request, response, pathname, query);
  59 + }
  60 +
  61 + if (response.sent === true || res.headersSent) {
  62 + return;
  63 + }
  64 +
  65 + const serializedErr = this.serializeError(err);
  66 +
  67 + return errorRenderer(serializedErr, req, res, pathname, query);
  68 + }
  69 +
  70 + return;
  71 + }
  72 +
  73 + // if the request and/or response are undefined (as with GraphQL) rethrow the error
  74 + throw err;
  75 + }
  76 +
  77 + /**
  78 + * Serialize the error similarly to method used in Next -- parse error as Nest error type
  79 + * @param err
  80 + */
  81 + public serializeError(err: any): ErrorResponse {
  82 + const out: ErrorResponse = {};
  83 +
  84 + if (!err) {
  85 + return out;
  86 + }
  87 +
  88 + if (err.stack && this.service.isDev()) {
  89 + out.stack = err.stack;
  90 + }
  91 +
  92 + if (err.response && typeof err.response === 'object') {
  93 + const { statusCode, error, message } = err.response;
  94 + out.statusCode = statusCode;
  95 + out.name = error;
  96 + out.message = message;
  97 + } else if (err.message && typeof err.message === 'object') {
  98 + const { statusCode, error, message } = err.message;
  99 + out.statusCode = statusCode;
  100 + out.name = error;
  101 + out.message = message;
  102 + }
  103 +
  104 + if (!out.statusCode && err.status) {
  105 + out.statusCode = err.status;
  106 + }
  107 +
  108 + if (!out.message && err.message) {
  109 + out.message = err.message;
  110 + }
  111 +
  112 + return out;
  113 + }
  114 +}
  1 +import { INestApplication, Module } from '@nestjs/common';
  2 +import { RenderFilter } from './render.filter';
  3 +import Server from 'next';
  4 +import { RenderService } from './render.service';
  5 +import { RendererConfig } from './types';
  6 +
  7 +type INestAppliactionSubset = Pick<
  8 + INestApplication,
  9 + 'getHttpAdapter' | 'useGlobalFilters'
  10 +> &
  11 + Partial<INestApplication>;
  12 +@Module({
  13 + providers: [RenderService],
  14 +})
  15 +export class RenderModule {
  16 + private app?: INestAppliactionSubset;
  17 + private server?: ReturnType<typeof Server>;
  18 +
  19 + constructor(private readonly service: RenderService) {}
  20 +
  21 + public register(
  22 + app: INestAppliactionSubset,
  23 + server: ReturnType<typeof Server>,
  24 + options: Partial<RendererConfig> = {},
  25 + ) {
  26 + this.app = app;
  27 + this.server = server;
  28 +
  29 + this.service.mergeConfig(options);
  30 + this.service.setRequestHandler(this.server.getRequestHandler());
  31 + this.service.setRenderer(this.server.render.bind(this.server));
  32 + this.service.setErrorRenderer(this.server.renderError.bind(this.server));
  33 + this.service.bindHttpServer(this.app.getHttpAdapter());
  34 +
  35 + this.app.useGlobalFilters(new RenderFilter(this.service));
  36 + }
  37 +}
  1 +import {
  2 + HttpServer,
  3 + Injectable,
  4 + InternalServerErrorException,
  5 +} from '@nestjs/common';
  6 +import { ExpressAdapter } from '@nestjs/platform-express';
  7 +import { isInternalUrl } from './next-utils';
  8 +import {
  9 + ErrorHandler,
  10 + ErrorRenderer,
  11 + Renderer,
  12 + RendererConfig,
  13 + RequestHandler,
  14 +} from './types';
  15 +
  16 +@Injectable()
  17 +export class RenderService {
  18 + private requestHandler?: RequestHandler;
  19 + private renderer?: Renderer;
  20 + private errorRenderer?: ErrorRenderer;
  21 + private errorHandler?: ErrorHandler;
  22 + private config: RendererConfig = {
  23 + dev: process.env.NODE_ENV !== 'production',
  24 + viewsDir: '/views',
  25 + };
  26 +
  27 + /**
  28 + * Merge the default config with the config obj passed to method
  29 + * @param config
  30 + */
  31 + public mergeConfig(config: Partial<RendererConfig>) {
  32 + if (typeof config.dev === 'boolean') {
  33 + this.config.dev = config.dev;
  34 + }
  35 + if (typeof config.viewsDir === 'string' || config.viewsDir === null) {
  36 + this.config.viewsDir = config.viewsDir;
  37 + }
  38 + }
  39 +
  40 + /**
  41 + * Set the directory that Next will render pages from
  42 + * @param path
  43 + */
  44 + public setViewsDir(path: string | null) {
  45 + this.config.viewsDir = path;
  46 + }
  47 +
  48 + /**
  49 + * Get the directory that Next renders pages from
  50 + */
  51 + public getViewsDir() {
  52 + return this.config.viewsDir;
  53 + }
  54 +
  55 + /**
  56 + * Explicitly set if env is or not dev
  57 + * @param dev
  58 + */
  59 + public setIsDev(dev: boolean) {
  60 + this.config.dev = dev;
  61 + }
  62 +
  63 + /**
  64 + * Get if the env is dev
  65 + */
  66 + public isDev(): boolean {
  67 + return this.config.dev!;
  68 + }
  69 +
  70 + /**
  71 + * Set the default request handler provided by next
  72 + * @param handler
  73 + */
  74 + public setRequestHandler(handler: RequestHandler) {
  75 + this.requestHandler = handler;
  76 + }
  77 +
  78 + /**
  79 + * Get the default request handler
  80 + */
  81 + public getRequestHandler(): RequestHandler | undefined {
  82 + return this.requestHandler;
  83 + }
  84 +
  85 + /**
  86 + * Set the render function provided by next
  87 + * @param renderer
  88 + */
  89 + public setRenderer(renderer: Renderer) {
  90 + this.renderer = renderer;
  91 + }
  92 +
  93 + /**
  94 + * Get the render function provided by next
  95 + */
  96 + public getRenderer(): Renderer | undefined {
  97 + return this.renderer;
  98 + }
  99 +
  100 + /**
  101 + * Set nextjs error renderer
  102 + * @param errorRenderer
  103 + */
  104 + public setErrorRenderer(errorRenderer: ErrorRenderer) {
  105 + this.errorRenderer = errorRenderer;
  106 + }
  107 +
  108 + /**
  109 + * Get nextjs error renderer
  110 + */
  111 + public getErrorRenderer(): ErrorRenderer | undefined {
  112 + return this.errorRenderer;
  113 + }
  114 +
  115 + /**
  116 + * Set a custom error handler
  117 + * @param handler
  118 + */
  119 + public setErrorHandler(handler: ErrorHandler) {
  120 + this.errorHandler = handler;
  121 + }
  122 +
  123 + /**
  124 + * Get the custom error handler
  125 + */
  126 + public getErrorHandler() {
  127 + return this.errorHandler;
  128 + }
  129 +
  130 + /**
  131 + * Bind to the render function for the HttpServer that nest is using and override
  132 + * it to allow for next to render the page
  133 + * @param server
  134 + */
  135 + public bindHttpServer(server: HttpServer) {
  136 + const renderer = this.getRenderer();
  137 + const getViewPath = this.getViewPath.bind(this);
  138 +
  139 + server.render = (response: any, view: string, data: any) => {
  140 + const isFastify = response.request !== undefined;
  141 +
  142 + const res = isFastify ? response.res : response;
  143 + const req = isFastify ? response.request.raw : response.req;
  144 +
  145 + if (req && res && renderer) {
  146 + return renderer(req, res, getViewPath(view), data);
  147 + } else if (!renderer) {
  148 + throw new InternalServerErrorException(
  149 + 'RenderService: renderer is not set',
  150 + );
  151 + } else if (!res) {
  152 + throw new InternalServerErrorException(
  153 + 'RenderService: could not get the response',
  154 + );
  155 + } else if (!req) {
  156 + throw new InternalServerErrorException(
  157 + 'RenderService: could not get the request',
  158 + );
  159 + }
  160 +
  161 + throw new Error('RenderService: failed to render');
  162 + };
  163 +
  164 + // and nextjs renderer to reply/response
  165 + if (server instanceof ExpressAdapter) {
  166 + server
  167 + .getInstance()
  168 + .decorateReply('render', function(view: string, data?: any) {
  169 + const res = this.res;
  170 + const req = this.request.raw;
  171 +
  172 + if (!renderer) {
  173 + throw new InternalServerErrorException(
  174 + 'RenderService: renderer is not set',
  175 + );
  176 + }
  177 +
  178 + return renderer(req, res, getViewPath(view), data);
  179 + });
  180 + } else {
  181 + server.getInstance().use((req: any, res: any, next: () => any) => {
  182 + res.render = (view: string, data?: any) => {
  183 + if (!renderer) {
  184 + throw new InternalServerErrorException(
  185 + 'RenderService: renderer is not set',
  186 + );
  187 + }
  188 + return renderer(req, res, getViewPath(view), data);
  189 + };
  190 +
  191 + next();
  192 + });
  193 + }
  194 + }
  195 +
  196 + /**
  197 + * Check if the URL is internal to nextjs
  198 + * @param url
  199 + */
  200 + public isInternalUrl(url: string): boolean {
  201 + return isInternalUrl(url);
  202 + }
  203 +
  204 + /**
  205 + * Format the path to the view
  206 + * @param view
  207 + */
  208 + protected getViewPath(view: string) {
  209 + const baseDir = this.getViewsDir();
  210 + const basePath = baseDir ? baseDir : '';
  211 + return `${basePath}/${view}`;
  212 + }
  213 +}
  1 +import { ParsedUrlQuery } from 'querystring';
  2 +
  3 +export type RequestHandler = (req: any, res: any, query?: any) => Promise<void>;
  4 +
  5 +export type Renderer = (
  6 + req: any,
  7 + res: any,
  8 + view: string,
  9 + params?: any,
  10 +) => Promise<void>;
  11 +
  12 +export type ErrorRenderer = (
  13 + err: any,
  14 + req: any,
  15 + res: any,
  16 + pathname: any,
  17 + query?: any,
  18 +) => Promise<void>;
  19 +
  20 +export type ErrorHandler = (
  21 + err: any,
  22 + req: any,
  23 + res: any,
  24 + pathname: any,
  25 + query: ParsedUrlQuery,
  26 +) => Promise<any>;
  27 +
  28 +export interface RendererConfig {
  29 + viewsDir: null | string;
  30 + dev: boolean;
  31 +}
  32 +
  33 +export interface ErrorResponse {
  34 + name?: string;
  35 + message?: string;
  36 + stack?: any;
  37 + statusCode?: number;
  38 +}
@@ -32,7 +32,7 @@ @@ -32,7 +32,7 @@
32 }, 32 },
33 "devDependencies": { 33 "devDependencies": {
34 "@nestjs/testing": "^6.0.0", 34 "@nestjs/testing": "^6.0.0",
35 - "@types/express": "4.16.1", 35 + "@types/express": "^4.17.1",
36 "@types/jest": "24.0.11", 36 "@types/jest": "24.0.11",
37 "@types/node": "11.13.4", 37 "@types/node": "11.13.4",
38 "@types/supertest": "2.0.7", 38 "@types/supertest": "2.0.7",
1 import { Module } from '@nestjs/common'; 1 import { Module } from '@nestjs/common';
2 import { AppController } from './app.controller'; 2 import { AppController } from './app.controller';
3 import { AppService } from './app.service'; 3 import { AppService } from './app.service';
  4 +import { RenderModule } from '../nest-next';
4 5
5 @Module({ 6 @Module({
6 - imports: [], 7 + imports: [RenderModule],
7 controllers: [AppController], 8 controllers: [AppController],
8 providers: [AppService], 9 providers: [AppService],
9 }) 10 })
1 import { NestFactory } from '@nestjs/core'; 1 import { NestFactory } from '@nestjs/core';
2 import { AppModule } from './app.module'; 2 import { AppModule } from './app.module';
  3 +import Next from 'next';
  4 +import { RenderModule } from '../nest-next';
  5 +import 'reflect-metadata';
3 6
4 async function bootstrap() { 7 async function bootstrap() {
5 - const app = await NestFactory.create(AppModule);  
6 - await app.listen(3000); 8 + const dev = process.env.NODE_ENV !== 'production';
  9 + const app = Next({ dev });
  10 +
  11 + await app.prepare();
  12 +
  13 + const server = await NestFactory.create(AppModule);
  14 +
  15 + const renderer = server.get(RenderModule);
  16 + renderer.register(server, app);
  17 +
  18 + await server.listen(process.env.PORT || 3000);
7 } 19 }
8 bootstrap(); 20 bootstrap();
@@ -1096,10 +1096,10 @@ @@ -1096,10 +1096,10 @@
1096 "@types/node" "*" 1096 "@types/node" "*"
1097 "@types/range-parser" "*" 1097 "@types/range-parser" "*"
1098 1098
1099 -"@types/express@4.16.1":  
1100 - version "4.16.1"  
1101 - resolved "https://registry.npm.taobao.org/@types/express/download/@types/express-4.16.1.tgz#d756bd1a85c34d87eaf44c888bad27ba8a4b7cf0"  
1102 - integrity sha1-11a9GoXDTYfq9EyIi60nuopLfPA= 1099 +"@types/express@^4.17.1":
  1100 + version "4.17.1"
  1101 + resolved "https://registry.npm.taobao.org/@types/express/download/@types/express-4.17.1.tgz#4cf7849ae3b47125a567dfee18bfca4254b88c5c"
  1102 + integrity sha1-TPeEmuO0cSWlZ9/uGL/KQlS4jFw=
1103 dependencies: 1103 dependencies:
1104 "@types/body-parser" "*" 1104 "@types/body-parser" "*"
1105 "@types/express-serve-static-core" "*" 1105 "@types/express-serve-static-core" "*"