├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── test.yaml ├── .gitignore ├── .npmignore ├── .prettierrc ├── CHANGES.md ├── LICENSE ├── README.md ├── docs ├── error-filter-sequence.mmd └── out │ ├── error-filter-sequence-sm.png │ └── error-filter-sequence.png ├── examples ├── basic │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── components │ │ ├── post-preview.tsx │ │ └── sidebar.tsx │ ├── next-env.d.ts │ ├── nodemon.json │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ └── views │ │ │ ├── about.tsx │ │ │ ├── blog │ │ │ ├── [slug].tsx │ │ │ └── index.tsx │ │ │ └── home.tsx │ ├── src │ │ ├── app.controller.ts │ │ ├── application.module.ts │ │ ├── blog │ │ │ ├── blog.controller.ts │ │ │ └── blog.service.ts │ │ └── main.ts │ ├── tsconfig.json │ ├── tsconfig.server.json │ ├── tslint.json │ └── types.ts ├── monorepo │ ├── .gitignore │ ├── README.md │ ├── dto │ │ ├── package.json │ │ ├── src │ │ │ ├── AboutPage.ts │ │ │ └── IndexPage.ts │ │ └── tsconfig.json │ ├── package.json │ ├── server │ │ ├── nodemon.json │ │ ├── package.json │ │ ├── src │ │ │ ├── app.controller.ts │ │ │ ├── application.module.ts │ │ │ └── main.ts │ │ └── tsconfig.json │ └── ui │ │ ├── next-env.d.ts │ │ ├── package.json │ │ ├── pages │ │ ├── about.tsx │ │ └── index.tsx │ │ └── tsconfig.json └── passthrough │ ├── .gitignore │ ├── README.md │ ├── next-env.d.ts │ ├── nodemon.json │ ├── package.json │ ├── pages │ └── index.tsx │ ├── public │ └── github-icon-32.png │ ├── src │ ├── app.controller.ts │ ├── application.module.ts │ └── main.ts │ ├── tsconfig.json │ ├── tsconfig.server.json │ └── tslint.json ├── index.d.ts ├── index.js ├── index.ts ├── lib ├── constants.ts ├── index.ts ├── next-utils.ts ├── render.filter.ts ├── render.module.ts ├── render.service.ts ├── types.ts └── vendor │ └── next │ ├── LICENSE │ ├── escape-regexp.ts │ ├── interpolate-dynamic-path.ts │ ├── is-dynamic.ts │ ├── remove-trailing-slash.ts │ └── route-regex.ts ├── package.json ├── run_tests.sh ├── tests ├── .env.example ├── docker-compose.yaml ├── e2e │ ├── .gitignore │ ├── package.json │ ├── playwright.config.ts │ └── specs │ │ ├── gsp-dynamic.spec.ts │ │ ├── gsp.spec.ts │ │ ├── gssp-dynamic.spec.ts │ │ └── gssp.spec.ts ├── test-app │ ├── .gitignore │ ├── Dockerfile │ ├── Dockerfile.dev │ ├── nest-cli.json │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── src │ │ ├── pages │ │ │ ├── [id].tsx │ │ │ ├── about │ │ │ │ └── [...all].tsx │ │ │ ├── blog-posts │ │ │ │ ├── [slug].tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ └── server │ │ │ ├── app.controller.ts │ │ │ ├── app.module.ts │ │ │ ├── main.ts │ │ │ └── params.interceptor.ts │ ├── tsconfig.json │ └── tsconfig.server.json └── wait-until ├── tsconfig.json └── tslint.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'plugin:@typescript-eslint/recommended', 9 | 'prettier', 10 | 'prettier/@typescript-eslint', 11 | 'plugin:promise/recommended', 12 | ], 13 | globals: { 14 | Atomics: 'readonly', 15 | SharedArrayBuffer: 'readonly', 16 | }, 17 | parser: '@typescript-eslint/parser', 18 | parserOptions: { 19 | ecmaFeatures: { 20 | jsx: true, 21 | }, 22 | ecmaVersion: 2018, 23 | sourceType: 'module', 24 | project: './tsconfig.json', 25 | }, 26 | plugins: ['@typescript-eslint', 'prettier', 'promise'], 27 | rules: { 28 | 'no-unused-vars': [ 29 | 'error', 30 | { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, 31 | ], 32 | '@typescript-eslint/no-unused-vars': [ 33 | 'error', 34 | { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, 35 | ], 36 | '@typescript-eslint/no-explicit-any': 0, 37 | '@typescript-eslint/ban-ts-ignore': 0, 38 | '@typescript-eslint/camelcase': [ 39 | 'error', 40 | { ignoreDestructuring: true, properties: 'never' }, 41 | ], 42 | '@typescript-eslint/explicit-function-return-type': 0, 43 | '@typescript-eslint/no-floating-promises': 1, 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **To Reproduce** 17 | 18 | Repository URL: 19 | 20 | **Version** 21 | - next.js: 22 | - nest: 23 | - nest-next: 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [master] 7 | pull_request_target: 8 | branches: [master] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | ref: ${{ github.event.pull_request.head.sha }} 18 | 19 | - name: Install dependencies 20 | run: npm install 21 | 22 | - name: Build 23 | run: npm run build 24 | 25 | - name: Upload dist directory 26 | uses: actions/upload-artifact@v3 27 | with: 28 | name: nest-next-dist 29 | path: dist/ 30 | 31 | e2e: 32 | needs: build 33 | timeout-minutes: 10 34 | runs-on: ubuntu-latest 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | node_version: [14] 39 | next_version: [10, 11, 12, 13] 40 | service: ['test-app', 'test-app-dev'] 41 | 42 | steps: 43 | - uses: actions/checkout@v3 44 | with: 45 | fetch-depth: 0 46 | ref: ${{ github.event.pull_request.head.sha }} 47 | 48 | - name: Download dist directory 49 | uses: actions/download-artifact@v3 50 | with: 51 | name: nest-next-dist 52 | path: tests/test-app/nest-next-dist 53 | 54 | - name: Start service 55 | working-directory: tests 56 | run: | 57 | docker-compose up -d --build ${{ matrix.service }} \ 58 | && bash wait-until "curl -sLf -o /dev/null http://localhost:3000/api/health/ping/" 300 59 | env: 60 | NEXT_VERSION: ${{ matrix.next_version }} 61 | NODE_VERSION: ${{ matrix.node_version }} 62 | 63 | - name: Install dependencies 64 | working-directory: tests/e2e 65 | run: npm install && npx playwright install --with-deps 66 | 67 | - name: Run tests 68 | working-directory: tests/e2e 69 | run: npx playwright test 70 | 71 | - name: Upload report 72 | if: always() 73 | uses: actions/upload-artifact@v3 74 | with: 75 | name: playwright-report 76 | path: tests/e2e/playwright-report/ 77 | 78 | - name: Display service logs 79 | if: always() 80 | run: docker logs tests_${{ matrix.service }}_1 81 | 82 | - name: Stop containers 83 | if: always() 84 | working-directory: tests 85 | run: docker-compose down 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | yarn-error.log 4 | *.DS_Store 5 | *.lock 6 | *-lock.json 7 | .env 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib 2 | index.ts 3 | package-lock.json 4 | tslint.json 5 | tsconfig.json 6 | yarn-error.log 7 | yarn.lock 8 | docs 9 | examples 10 | .github 11 | CHANGES.md 12 | .prettierrc 13 | .eslintrc.js 14 | 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## [0.7.0] - 2019-04-24 4 | 5 | ### Changed 6 | - Renamed RegisterOptions interface to RendererConfig and moved to the types file. 7 | 8 | ### Added 9 | - Added dev option to new RendererConfig along with getters and setters to the RenderService. 10 | 11 | ### Fixed 12 | - Transformed the error from nest to the interface next expects. Previously server side errors passed to the frontend resulted in an invariant violation in React. 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present Kyle McCarthy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Nest-Next

3 |

Render Module to add Nextjs support for Nestjs.

4 | npm PRs Welcome GitHub license

5 | 6 |
7 | 8 | > nest-next provides a nestjs module to integrate next.js into a nest.js application, it allows the rendering of next.js pages via nestjs controllers and providing initial props to the page as well. 9 | 10 | 11 | 12 | ### Table of Contents 13 | 14 | - [Table of Contents](#table-of-contents) 15 | - [Installation](#installation) 16 | - [Peer Dependencies](#peer-dependencies) 17 | - [Usage](#usage) 18 | - [Configuration](#configuration) 19 | - [Views/Pages Folder](#viewspages-folder) 20 | - [Dev Mode](#dev-mode) 21 | - [tsconfig.json](#tsconfigjson) 22 | - [Pass-through 404s](#pass-through-404s) 23 | - [Rendering Pages](#rendering-pages) 24 | - [Rendering the initial props](#rendering-the-initial-props) 25 | - [Handling Errors](#handling-errors) 26 | - [Custom error handler](#custom-error-handler) 27 | - [ErrorHandler Typedef](#errorhandler-typedef) 28 | - [Setting ErrorHandler](#setting-errorhandler) 29 | - [Error Flow (Diagram)](#error-flow-diagram) 30 | - [Examples folder structure](#examples-folder-structure) 31 | - [Basic Setup](#basic-setup) 32 | - [Monorepo](#monorepo) 33 | - [Known Issues](#known-issues) 34 | - [Contributing](#contributing) 35 | - [License](#license) 36 | 37 | 38 | 39 | ### Installation 40 | 41 | ```bash 42 | yarn add nest-next 43 | 44 | # or 45 | 46 | npm install nest-next 47 | ``` 48 | 49 | ### Peer Dependencies 50 | 51 | - `react` 52 | - `react-dom` 53 | - `next` 54 | 55 | if you are using next.js with typescript which most likely is the case, you will need to also install the typescript types for react and react-dom. 56 | 57 | ### Usage 58 | 59 | Import the RenderModule into your application's root module and call the forRootAsync method. 60 | 61 | ```typescript 62 | import { Module } from '@nestjs/common'; 63 | import Next from 'next'; 64 | import { RenderModule } from 'nest-next'; 65 | 66 | @Module({ 67 | imports: [ 68 | RenderModule.forRootAsync(Next({ dev: process.env.NODE_ENV !== 'production' })), 69 | ... 70 | ], 71 | ... 72 | }) 73 | export class AppModule {} 74 | ``` 75 | 76 | ### Configuration 77 | 78 | The RenderModule accepts configuration options as the second argument in the forRootAsync method. 79 | 80 | ```typescript 81 | interface RenderOptions { 82 | viewsDir: null | string; 83 | dev: boolean; 84 | } 85 | ``` 86 | 87 | #### Views/Pages Folder 88 | 89 | By default the the renderer will serve pages from the `/pages/views` dir. Due to limitations with 90 | Next, the `/pages` directory is not configurable, but the directory within the `/pages` directory is configurable. 91 | 92 | The `viewsDir` option determines the folder inside of `pages` to render from. By default the value is `/views` but this can be changed or set to null to render from the root of `pages`. 93 | 94 | #### Dev Mode 95 | 96 | By default the dev mode will be set to `true` unless the option is overwritten. Currently the dev mode determines how the errors should be serialized before being sent to next. 97 | 98 | #### tsconfig.json 99 | 100 | Next 9 added [built-in zero-config typescript support](https://nextjs.org/blog/next-9#built-in-zero-config-typescript-support). This change is great in general, but next requires specific settings in the tsconfig which are incompatible with what are needed for the server. However, these settings can easily be overridden in the `tsconfig.server.json` file. 101 | 102 | If you are having issues with unexpected tokens, files not emitting when building for production, warnings about `allowJs` and `declaration` not being used together, and other typescript related errors; see the `tsconfig.server.json` [file in the example project](/examples/basic/tsconfig.server.json) for the full config. 103 | 104 | #### Pass-through 404s 105 | 106 | Instead of having Nest handle the response for requests that 404, they can be forwarded to Next's request handler. 107 | 108 | ```typescript 109 | RenderModule.forRootAsync( 110 | Next({ 111 | dev: process.env.NODE_ENV !== 'production', 112 | dir: resolve(__dirname, '..'), 113 | }), 114 | { passthrough404: true, viewsDir: null }, 115 | ), 116 | ``` 117 | 118 | See [this discussion](https://github.com/kyle-mccarthy/nest-next/issues/38#issuecomment-647867509) for more context. 119 | 120 | ### Rendering Pages 121 | 122 | The `RenderModule` overrides the Express/Fastify render. To render a page in your controller import 123 | the Render decorator from `@nestjs/common` and add it to the method that will render the page. The 124 | path for the page is relative to the `/pages` directory. 125 | 126 | ```typescript 127 | import { Controller, Get, Render } from '@nestjs/common'; 128 | 129 | @Controller() 130 | export class AppController { 131 | @Get() 132 | @Render('Index') 133 | public index() { 134 | // initial props 135 | return { 136 | title: 'Next with Nest', 137 | }; 138 | } 139 | } 140 | ``` 141 | 142 | Additionally, the render function is made available on the res object. 143 | 144 | ```typescript 145 | @Controller() 146 | export class AppController { 147 | @Get() 148 | public index(@Res() res: RenderableResponse) { 149 | res.render('Index', { 150 | title: 'Next with Nest', 151 | }); 152 | } 153 | } 154 | ``` 155 | 156 | The render function takes in the view, as well as the initial props passed to the page. 157 | 158 | ```typescript 159 | render = (view: string, initialProps?: any) => any; 160 | ``` 161 | 162 | ### Rendering the initial props 163 | 164 | The initial props sent to the next.js view page can be accessed from the context's query method inside the getInitialProps method. 165 | 166 | ```typescript 167 | import { NextPage, NextPageContext } from 'next'; 168 | 169 | // The component's props type 170 | type PageProps = { 171 | title: string; 172 | }; 173 | 174 | // extending the default next context type 175 | type PageContext = NextPageContext & { 176 | query: PageProps; 177 | }; 178 | 179 | // react component 180 | const Page: NextPage = ({ title }) => { 181 | return ( 182 |
183 |

{title}

184 |
185 | ); 186 | }; 187 | 188 | // assigning the initial props to the component's props 189 | Page.getInitialProps = (ctx: PageContext) => { 190 | return { 191 | title: ctx.query.title, 192 | }; 193 | }; 194 | 195 | export default Page; 196 | ``` 197 | 198 | ### Handling Errors 199 | 200 | By default, errors will be handled and rendered with next's error renderer, which uses the [customizable](https://nextjs.org/docs/#custom-error-handling) \_error page. Additionally, errors can be intercepted by setting your own error handler. 201 | 202 | #### Custom error handler 203 | 204 | A custom error handler can be set to override or enhance the default behavior. This can be used for things such as logging the error or rendering a different response. 205 | 206 | In your custom error handler you have the option of just intercepting and inspecting the error, or sending your own response. If a response is sent from the error handler, the request is considered done and the error won't be forwarded to next's error renderer. If a response is not sent in the error handler, after the handler returns the error is forwarded to the error renderer. See the request flow below for visual explanation. 207 | 208 | ##### ErrorHandler Typedef 209 | 210 | ```typescript 211 | export type ErrorHandler = ( 212 | err: any, 213 | req: any, 214 | res: any, 215 | pathname: any, 216 | query: ParsedUrlQuery, 217 | ) => Promise; 218 | ``` 219 | 220 | ##### Setting ErrorHandler 221 | 222 | You can set the error handler by getting the RenderService from nest's container. 223 | 224 | ```typescript 225 | // in main.ts file after registering the RenderModule 226 | 227 | const main() => { 228 | ... 229 | 230 | // get the RenderService 231 | const service = server.get(RenderService); 232 | 233 | service.setErrorHandler(async (err, req, res) => { 234 | // send JSON response 235 | res.send(err.response); 236 | }); 237 | 238 | ... 239 | } 240 | 241 | ``` 242 | 243 | ##### Error Flow (Diagram) 244 | 245 | _The image is linked to a larger version_ 246 | 247 | [![error filter sequence diagram](./docs/out/error-filter-sequence-sm.png)](./docs/out/error-filter-sequence.png) 248 | 249 | ### Examples folder structure 250 | 251 | Fully setup projects can be viewed in the [examples folder](/examples) 252 | 253 | #### Basic Setup 254 | 255 | Next renders pages from the pages directory. The Nest source code can remain in the default `/src` folder 256 | 257 | /src 258 | /main.ts 259 | /app.module.ts 260 | /app.controller.ts 261 | /pages 262 | /views 263 | /Index.jsx 264 | /components 265 | ... 266 | babel.config.js 267 | next.config.js 268 | nodemon.json 269 | tsconfig.json 270 | tsconfig.server.json 271 | 272 | #### Monorepo 273 | 274 | Next renders pages from the pages directory in the "ui" subproject. The Nest project is in the "server" folder. 275 | In order to make the properties type safe between the "ui" and "server" projects, there is a folder called "dto" 276 | outside of both projects. Changes in it during "dev" runs trigger recompilation of both projects. 277 | 278 | /server 279 | /src 280 | /main.ts 281 | /app.module.ts 282 | /app.controller.ts 283 | nodemon.json 284 | tsconfig.json 285 | ... 286 | /ui 287 | /pages 288 | /index.tsx 289 | /about.tsx 290 | next-env.d.ts 291 | tsconfig.json 292 | ... 293 | /dto 294 | /src 295 | /AboutPage.ts 296 | /IndexPage.ts 297 | package.json 298 | 299 | To run this project, the "ui" and "server" project must be built, in any order. The "dto" project will be implicitly built by the "server" project. After both of those builds, the "server" project can be started in either dev or production mode. 300 | 301 | It is important that "ui" references to "dto" refer to the TypeScript files (.ts files in the "src" folder), and NOT the declaration files (.d.ts files in the "dist" folder), due to how Next not being compiled in the same fashion as the server. 302 | 303 | ### Known issues 304 | 305 | Currently Next ["catch all routes"](https://nextjs.org/docs/routing/dynamic-routes#catch-all-routes) pages do not work correctly. See issue [#101](https://github.com/kyle-mccarthy/nest-next/issues/101) for details. 306 | 307 | ### Contributing 308 | 309 | To contribute make sure your changes pass the test suite. To run test suite `docker`, `docker-compose` are required. Run `npm run test` to run tests. Playwright will be installed with required packages. To run tests in Next development mode run `npm run test-dev`. You can also specify `NODE_VERSION` and major `NEXT_VERSION` variables to run tests in specific environments. 310 | 311 | ### License 312 | 313 | MIT License 314 | 315 | Copyright (c) 2018-present Kyle McCarthy 316 | 317 | Permission is hereby granted, free of charge, to any person obtaining a copy 318 | of this software and associated documentation files (the "Software"), to deal 319 | in the Software without restriction, including without limitation the rights 320 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 321 | copies of the Software, and to permit persons to whom the Software is 322 | furnished to do so, subject to the following conditions: 323 | 324 | The above copyright notice and this permission notice shall be included in all 325 | copies or substantial portions of the Software. 326 | 327 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 328 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 329 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 330 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 331 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 332 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 333 | SOFTWARE. 334 | -------------------------------------------------------------------------------- /docs/error-filter-sequence.mmd: -------------------------------------------------------------------------------- 1 | graph TB 2 | NoRteReg[Error handled by RenderFilter] 3 | 4 | NoRteReg --> HdrsSnt{Headers sent?} 5 | 6 | HdrsSnt --> |T| FIN 7 | 8 | HdrsSnt --> |F| IntlNxtRoute{Internal
nextjs URL?} 9 | 10 | IntlNxtRoute --> |T| NxtReqHndlr[Use nextjs requestHandler] 11 | NxtReqHndlr --> FIN 12 | 13 | IntlNxtRoute --> |F| ErrHndlrReg{errorHandler
registered?} 14 | ErrHndlrReg --> |T| ErrHndlrCall[Call error Handler] 15 | 16 | ErrHndlrCall --> ErrHndlrRes{Error handler
sent response?} 17 | 18 | ErrHndlrRes --> |T| FIN 19 | 20 | ErrHndlrRes --> |F| NxtErrRndr[Use nextjs errorRenderer] 21 | NxtErrRndr --> FIN 22 | 23 | ErrHndlrReg --> |F| NxtErrRndr 24 | NxtErrRndr --> FIN[Done] 25 | 26 | -------------------------------------------------------------------------------- /docs/out/error-filter-sequence-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyle-mccarthy/nest-next/caddbba4a63ba40aa138b3bb63a321cb208fe30f/docs/out/error-filter-sequence-sm.png -------------------------------------------------------------------------------- /docs/out/error-filter-sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyle-mccarthy/nest-next/caddbba4a63ba40aa138b3bb63a321cb208fe30f/docs/out/error-filter-sequence.png -------------------------------------------------------------------------------- /examples/basic/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | settings: { 8 | react: { 9 | version: 'detect', 10 | }, 11 | }, 12 | extends: [ 13 | 'plugin:react/recommended', 14 | 'plugin:@typescript-eslint/recommended', 15 | 'prettier', 16 | 'prettier/@typescript-eslint', 17 | ], 18 | globals: { 19 | Atomics: 'readonly', 20 | SharedArrayBuffer: 'readonly', 21 | }, 22 | parser: '@typescript-eslint/parser', 23 | parserOptions: { 24 | ecmaFeatures: { 25 | jsx: true, 26 | }, 27 | ecmaVersion: 2018, 28 | sourceType: 'module', 29 | project: './tsconfig.json', 30 | tsconfigRootDir: __dirname, 31 | }, 32 | plugins: ['react', '@typescript-eslint', 'prettier'], 33 | rules: { 34 | 'react/prop-types': 'off', 35 | 'react/react-in-jsx-scope': 'off', 36 | 'no-unused-vars': 0, 37 | '@typescript-eslint/no-unused-vars': [ 38 | 'error', 39 | { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, 40 | ], 41 | '@typescript-eslint/no-explicit-any': 0, 42 | '@typescript-eslint/ban-ts-ignore': 0, 43 | '@typescript-eslint/explicit-function-return-type': [ 44 | 'warn', 45 | { 46 | allowExpressions: true, 47 | allowTypedFunctionExpressions: true, 48 | allowHigherOrderFunctions: true, 49 | }, 50 | ], 51 | '@typescript-eslint/no-floating-promises': 1, 52 | }, 53 | }; 54 | 55 | -------------------------------------------------------------------------------- /examples/basic/.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | .yalc 3 | node_modules 4 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # NEST-NEXT AGGREGATED EXAMPLE 2 | 3 | This example demonstrates how to use nest-next to add server side rendering to [nest](https://github.com/nestjs/nest) with [next.js](https://github.com/zeit/next.js/). 4 | 5 | All needed components are expected to be under one project, one repo, with this folder being the root of the repo. 6 | 7 | > You need to build the project first -------------------------------------------------------------------------------- /examples/basic/components/post-preview.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { IPost } from '../types'; 3 | import Link from 'next/link'; 4 | 5 | interface Props { 6 | post: IPost; 7 | } 8 | 9 | const excerpt = (content: string[]): string => { 10 | if (Array.isArray(content) && content.length > 0) { 11 | const e = content[0]; 12 | return `${e.substr(0, 125)}...`; 13 | } 14 | return 'No preview available...'; 15 | }; 16 | 17 | const PostPreview: FC = ({ post }) => { 18 | return ( 19 |
20 |

{post.title}

21 |

{excerpt(post.content)}

22 | 29 | 40 | View 41 | 42 | 43 |
44 | ); 45 | }; 46 | 47 | export default PostPreview; 48 | -------------------------------------------------------------------------------- /examples/basic/components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import Link from 'next/link'; 3 | 4 | const Sidebar: FC = () => { 5 | return ( 6 |
7 |
8 | 9 | 16 | EXAMPLE APP 17 | 18 | 19 |
20 |
    21 |
  • 22 | 23 | Home 24 | 25 |
  • 26 |
  • 27 | 28 | About 29 | 30 |
  • 31 |
  • 32 | 33 | Blog 34 | 35 |
  • 36 |
37 |
38 | ); 39 | }; 40 | 41 | export default Sidebar; 42 | -------------------------------------------------------------------------------- /examples/basic/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/basic/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src/**/*"], 3 | "ext": "ts,tsx", 4 | "execMap": { 5 | "ts": "ts-node --project tsconfig.server.json" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestnext8", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "nodemon src/main.ts", 8 | "build:clean": "rimraf .next", 9 | "build:ui": "npx next build", 10 | "build:server": "npx tsc --project tsconfig.server.json && npx babel .next/production-server -d .next/production-server --extensions \".js\"", 11 | "build": "yarn build:clean && yarn build:ui && yarn build:server", 12 | "start": "cross-env NODE_ENV=production node .next/production-server/src/main.js" 13 | }, 14 | "dependencies": { 15 | "@nestjs/common": "^8.4.4", 16 | "@nestjs/core": "^8.4.4", 17 | "@nestjs/platform-express": "^8.4.4", 18 | "cross-env": "^7.0.3", 19 | "nest-next": "^9.6.0", 20 | "next": "12.1.5", 21 | "react": "18.0.0", 22 | "react-dom": "18.0.0", 23 | "reflect-metadata": "^0.1.13", 24 | "rxjs": "^7.5.5" 25 | }, 26 | "devDependencies": { 27 | "@babel/cli": "^7.17.6", 28 | "@babel/core": "^7.17.9", 29 | "@types/node": "^17.0.23", 30 | "@types/react": "^18.0.4", 31 | "@typescript-eslint/eslint-plugin": "^5.19.0", 32 | "@typescript-eslint/parser": "^5.19.0", 33 | "eslint": "^8.13.0", 34 | "eslint-config-prettier": "^8.5.0", 35 | "eslint-plugin-prettier": "^4.0.0", 36 | "nodemon": "^2.0.15", 37 | "prettier": "^2.6.2", 38 | "rimraf": "^3.0.0", 39 | "ts-node": "^10.7.0", 40 | "typescript": "^4.6.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/basic/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import type { AppProps /*, AppContext */ } from 'next/app'; 3 | import Sidebar from '../components/sidebar'; 4 | 5 | const MyApp: FC = ({ Component, pageProps }) => { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 | ); 16 | }; 17 | 18 | export default MyApp; 19 | -------------------------------------------------------------------------------- /examples/basic/pages/views/about.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { NextPage } from 'next'; 3 | 4 | const Page: NextPage = () => { 5 | return
About Page!
; 6 | }; 7 | 8 | export default Page; 9 | -------------------------------------------------------------------------------- /examples/basic/pages/views/blog/[slug].tsx: -------------------------------------------------------------------------------- 1 | import { NextPage, NextPageContext } from 'next'; 2 | import { IPost } from '../../../types'; 3 | import { BlogService } from '../../../src/blog/blog.service'; 4 | 5 | interface Props { 6 | post: IPost; 7 | source: string; 8 | } 9 | 10 | interface SSProps { 11 | post: IPost | null; 12 | source: string; 13 | } 14 | 15 | const Post: NextPage = ({ post: { title, content }, source }) => { 16 | return ( 17 |
18 |

{title}

19 |
20 | {content.map((block, index) => ( 21 |

{block}

22 | ))} 23 |
24 |
25 | this page was rendered on the {source} 26 |
27 |
28 | ); 29 | }; 30 | 31 | // When the page was rendered server side the ctx.query will contain the data 32 | // returned by the controller's method. When the page was rendered on the client 33 | // side, the ctx.query will only contain the query params for the url. 34 | // 35 | // To better understand why this happens, reference the following next 36 | // documentation about how getServerSideProps only runs on the server: 37 | // https://nextjs.org/docs/basic-features/data-fetching#only-runs-on-server-side 38 | export function getServerSideProps(ctx: NextPageContext) { 39 | const post = ctx.query.post || null; 40 | 41 | const props: SSProps = { 42 | source: 'server', 43 | post: post as any, 44 | }; 45 | 46 | if (!props.post) { 47 | const service = new BlogService(); 48 | props.post = service.find(ctx.query.slug as string); 49 | props.source = 'client'; 50 | } 51 | 52 | if (props.post === null) { 53 | // https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering 54 | // return object with notFound equal to true for 404 error 55 | return { 56 | notFound: true, 57 | }; 58 | } 59 | 60 | return { props }; 61 | } 62 | 63 | export default Post; 64 | -------------------------------------------------------------------------------- /examples/basic/pages/views/blog/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { NextPage, NextPageContext } from 'next'; 3 | import { IPost } from '../../../types'; 4 | import PostPreview from '../../../components/post-preview'; 5 | import { BlogService } from '../../../src/blog/blog.service'; 6 | 7 | interface Props { 8 | posts: IPost[]; 9 | source: string; 10 | } 11 | 12 | const Blog: NextPage = ({ posts, source }) => { 13 | return ( 14 |
15 |

blog

16 |
17 | {posts.map((post) => ( 18 | 19 | ))} 20 |
21 |
22 | this page was rendered on the {source} 23 |
24 |
25 | ); 26 | }; 27 | 28 | // When the page was rendered server side the ctx.query will contain the data 29 | // returned by the controller's method. When the page was rendered on the client 30 | // side, the ctx.query will only contain the query params for the url. 31 | // 32 | // To better understand why this happens, reference the following next 33 | // documentation about how getServerSideProps only runs on the server: 34 | // https://nextjs.org/docs/basic-features/data-fetching#only-runs-on-server-side 35 | export async function getServerSideProps(ctx: NextPageContext) { 36 | const props: Props = { 37 | source: 'server', 38 | posts: ctx.query.posts as any, 39 | }; 40 | 41 | if (!Array.isArray(props.posts)) { 42 | const service = new BlogService(); 43 | props.posts = service.all(); 44 | props.source = 'client'; 45 | } 46 | 47 | return { props }; 48 | } 49 | 50 | export default Blog; 51 | -------------------------------------------------------------------------------- /examples/basic/pages/views/home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { NextPage, NextPageContext } from 'next'; 3 | 4 | interface Props { 5 | query: { name?: string }; 6 | } 7 | 8 | const Home: NextPage = ({ query }) => { 9 | const greetName = query.name ? query.name : 'World'; 10 | 11 | return ( 12 |
13 |
Hello, {greetName}!
14 |
15 | ); 16 | }; 17 | 18 | export async function getServerSideProps(ctx: NextPageContext) { 19 | const query = { 20 | name: ctx.query.name || null, 21 | }; 22 | return { props: { query } }; 23 | } 24 | 25 | export default Home; 26 | -------------------------------------------------------------------------------- /examples/basic/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query, Render } from '@nestjs/common'; 2 | 3 | @Controller() 4 | export class AppController { 5 | @Render('home') 6 | @Get() 7 | public index(@Query('name') name?: string) { 8 | return { name }; 9 | } 10 | 11 | @Render('about') 12 | @Get('/about') 13 | public about() { 14 | return {}; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/basic/src/application.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RenderModule } from 'nest-next'; 3 | import Next from 'next'; 4 | import { AppController } from './app.controller'; 5 | import { BlogController } from './blog/blog.controller'; 6 | import { BlogService } from './blog/blog.service'; 7 | 8 | @Module({ 9 | imports: [ 10 | RenderModule.forRootAsync( 11 | Next({ 12 | dev: process.env.NODE_ENV !== 'production', 13 | conf: { useFilesystemPublicRoutes: false }, 14 | }), 15 | ), 16 | ], 17 | controllers: [AppController, BlogController], 18 | providers: [BlogService], 19 | }) 20 | export class AppModule {} 21 | -------------------------------------------------------------------------------- /examples/basic/src/blog/blog.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | NotFoundException, 5 | Param, 6 | Render, 7 | } from '@nestjs/common'; 8 | import { BlogService } from './blog.service'; 9 | 10 | @Controller('/blog') 11 | export class BlogController { 12 | constructor(private service: BlogService) {} 13 | 14 | @Render('blog') 15 | @Get() 16 | public index() { 17 | return { posts: this.service.all() }; 18 | } 19 | 20 | @Render('blog/[slug]') 21 | @Get(':slug') 22 | public get(@Param('slug') slug: string) { 23 | const post = this.service.find(slug); 24 | 25 | if (post === null) { 26 | throw new NotFoundException(); 27 | } 28 | 29 | return { post }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/basic/src/blog/blog.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@nestjs/common'; 2 | import { IPost } from '../../types'; 3 | 4 | const POSTS: Record = { 5 | 'first-post': { 6 | title: 'First Post!', 7 | slug: 'first-post', 8 | content: [ 9 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent sed suscipit quam, sit amet feugiat ligula. Nunc sit amet velit vestibulum, mattis orci gravida, aliquam velit. Donec eget lectus nec ipsum suscipit gravida et et odio. Morbi hendrerit dui scelerisque, imperdiet ligula in, ornare risus. Aliquam blandit sem risus, a ornare orci finibus ut. Maecenas interdum lacus arcu, nec finibus nibh semper quis. Vivamus venenatis pharetra ligula, eget semper justo finibus et. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nullam finibus accumsan elit, et ornare nulla accumsan id. Cras nec leo sed ex egestas malesuada. Nullam a bibendum libero. Cras ullamcorper massa sed odio euismod vulputate. Nullam at ullamcorper dolor. Maecenas et fermentum arcu. Sed interdum nunc neque, eu consectetur ex commodo finibus. Nunc interdum aliquam purus, eu lobortis enim semper et.', 10 | 'Ut sed dolor odio. Mauris cursus aliquet tortor, a posuere mi elementum in. Morbi sed efficitur mauris. Donec sed nulla efficitur, finibus massa ut, aliquet elit. Praesent eu mattis velit. Fusce sodales tincidunt mi, ut placerat turpis lobortis eu. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nam at scelerisque lacus, ut commodo leo. Morbi vitae iaculis arcu. Donec finibus erat sed tristique feugiat. Morbi lorem tellus, elementum et facilisis eu, egestas fringilla eros. In quis arcu aliquam, ornare nulla malesuada, convallis massa. Donec tellus neque, tempor eu porttitor at, malesuada eget tellus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Quisque vel pellentesque elit. Morbi semper purus velit, a pulvinar eros blandit vel.', 11 | ], 12 | }, 13 | 'second-post': { 14 | title: 'Second Post!', 15 | slug: 'second-post', 16 | content: [ 17 | 'Nulla sed purus ullamcorper, volutpat leo ac, blandit sem. Aenean efficitur ante rhoncus, lobortis est nec, consequat nisl. Fusce quis semper ligula, eget commodo magna. In tincidunt nisl sed dui ornare, nec pulvinar nibh laoreet. Suspendisse lobortis elit at nunc egestas fermentum. Etiam leo dui, fermentum ac nulla et, hendrerit varius arcu. Quisque porttitor congue mattis. Mauris non lorem suscipit turpis dictum porttitor. Nullam eget blandit felis. Duis eu erat ac mauris egestas placerat. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.', 18 | 'Etiam vel tellus sollicitudin, laoreet quam id, dignissim eros. Suspendisse dapibus tempor magna eget eleifend. Morbi molestie arcu id sagittis tristique. Suspendisse luctus id velit et elementum. Cras gravida sodales quam vel iaculis. Cras aliquet ex a placerat tincidunt. Fusce at ligula urna. Pellentesque id sapien lacus. Nullam eleifend ultrices tortor a hendrerit. Vivamus cursus leo eget tortor porttitor finibus. Quisque at quam gravida, aliquam orci ut, volutpat enim. Vivamus sit amet lobortis lacus. In aliquet consectetur diam vitae lacinia. Suspendisse ultrices malesuada turpis ac congue. Pellentesque vestibulum, nulla nec mollis euismod, sapien ipsum lobortis tortor, nec pellentesque sem nulla gravida diam.', 19 | ], 20 | }, 21 | }; 22 | 23 | export class BlogService { 24 | public all(): IPost[] { 25 | return Object.values(POSTS); 26 | } 27 | 28 | public find(slug: string): IPost | null { 29 | return POSTS[slug] || null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/basic/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './application.module'; 3 | 4 | async function bootstrap() { 5 | const server = await NestFactory.create(AppModule); 6 | 7 | await server.listen(3000); 8 | } 9 | 10 | bootstrap(); 11 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "jsx": "preserve", 6 | "strict": true, 7 | "pretty": true, 8 | "noImplicitAny": true, 9 | "alwaysStrict": true, 10 | "noImplicitReturns": true, 11 | "noImplicitThis": true, 12 | "outDir": ".next", 13 | "moduleResolution": "node", 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true, 18 | "sourceMap": true, 19 | "skipLibCheck": true, 20 | "lib": [ 21 | "es2017", 22 | "dom" 23 | ], 24 | "baseUrl": ".", 25 | "typeRoots": [ 26 | "node_modules/@types", 27 | "./typings" 28 | ], 29 | "allowJs": true, 30 | "forceConsistentCasingInFileNames": true, 31 | "noEmit": true, 32 | "resolveJsonModule": true, 33 | "isolatedModules": true, 34 | "incremental": true 35 | }, 36 | "include": [ 37 | "./pages/**/*", 38 | "./src/**/*", 39 | "./ui/**/*", 40 | "types.ts" 41 | ], 42 | "exclude": [ 43 | "node_modules" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": ".next/production-server", 5 | "declaration": true, 6 | "target": "es6", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "isolatedModules": false, 12 | "noEmit": false, 13 | "allowJs": false 14 | }, 15 | "include": ["./src/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /examples/basic/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-react", 5 | "tslint-config-prettier", 6 | "tslint-react-hooks" 7 | ], 8 | "rules": { 9 | "quotemark": [true, "single", "jsx-double"], 10 | "semicolon": [true, "always"], 11 | "trailing-comma": [true], 12 | "object-literal-sort-keys": false, 13 | "no-unused-variable": true, 14 | "interface-name": false, 15 | "no-empty-interface": false, 16 | "jsx-no-multiline-js": false, 17 | "jsx-no-target-blank": false, 18 | "no-shadowed-variable": [true, { "temporalDeadZone": false }], 19 | "member-ordering": [true, { "order": "fields-first" }], 20 | "react-hooks-nesting": "warning" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/basic/types.ts: -------------------------------------------------------------------------------- 1 | export interface IPost { 2 | title: string, 3 | slug: string, 4 | content: string[] 5 | } -------------------------------------------------------------------------------- /examples/monorepo/.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /examples/monorepo/README.md: -------------------------------------------------------------------------------- 1 | # NEST-NEXT SEGREGATED EXAMPLE 2 | 3 | This example demonstrates how to use nest-next to add server side rendering to [nest](https://github.com/nestjs/nest) with [next.js](https://github.com/zeit/next.js/). 4 | 5 | All needed components are expected to be in a mono repo with multiple projects, with this folder being the root of the repo. 6 | 7 | To run this project, the "ui" and "server" project must be built, in any order. The "dto" project will be implicitly built by the "server" project. After both of those builds, the "server" project can be started in either dev or production mode. 8 | 9 | It is important that "ui" references to "dto" refer to the TypeScript files (.ts files in the "src" folder), and NOT the declaration files (.d.ts files in the "dist" folder), due to how Next not being compiled in the same fashion as the server. 10 | -------------------------------------------------------------------------------- /examples/monorepo/dto/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-nest-example-advanced-dto", 3 | "version": "1.0.0", 4 | "description": "The DTO portion of an advanced example using nest-next", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "rimraf": "^3.0.0", 8 | "typescript": "^3.7.3" 9 | }, 10 | "scripts": { 11 | "clean": "rimraf ./dist", 12 | "build": "tsc -b" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/monorepo/dto/src/AboutPage.ts: -------------------------------------------------------------------------------- 1 | export interface AboutProps { 2 | message: string; 3 | } 4 | -------------------------------------------------------------------------------- /examples/monorepo/dto/src/IndexPage.ts: -------------------------------------------------------------------------------- 1 | export interface IndexProps { 2 | message: string; 3 | } 4 | -------------------------------------------------------------------------------- /examples/monorepo/dto/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "sourceMap": true, 6 | "composite": true, 7 | "incremental": true, 8 | "declaration": true, 9 | "strict": true, 10 | "rootDir": "./src", 11 | "outDir": "./dist", 12 | "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo" 13 | }, 14 | "exclude": ["node_modules", "dist"], 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /examples/monorepo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-next-example-advanced", 3 | "version": "1.0.0", 4 | "description": "Root of an advanced example using nest-next", 5 | "license": "MIT", 6 | "dependencies": { 7 | "react": "^16.12.0", 8 | "react-dom": "^16.12.0" 9 | }, 10 | "devDependencies": { 11 | "@types/react": "^16.9.49" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/monorepo/server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src/**/*", "../dto/src/**/*"], 3 | "ext": "ts,tsx", 4 | "exec": "ts-node ./src/main.ts" 5 | } 6 | -------------------------------------------------------------------------------- /examples/monorepo/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-next-example-advanced-server", 3 | "version": "1.0.0", 4 | "description": "The server portion of an advanced example using nest-next.", 5 | "main": "dist/main.js", 6 | "types": "src/main.ts", 7 | "license": "MIT", 8 | "dependencies": { 9 | "@nestjs/common": "^7.6.5", 10 | "@nestjs/core": "^7.6.5", 11 | "@nestjs/platform-fastify": "^7.6.5", 12 | "cross-env": "^6.0.3", 13 | "nest-next": "^9.1.0", 14 | "next": "^9.1.6", 15 | "nodemon": "^2.0.2", 16 | "reflect-metadata": "^0.1.13", 17 | "rxjs": "^6.5.3", 18 | "ts-node": "^8.5.4" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^12.12.21", 22 | "@types/react-dom": "^16.9.4", 23 | "rimraf": "^3.0.0", 24 | "typescript": "^3.7.3" 25 | }, 26 | "scripts": { 27 | "clean": "rimraf ./dist", 28 | "build": "tsc -b", 29 | "start": "cross-env NODE_ENV=production node ./dist/main.js", 30 | "dev": "nodemon" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/monorepo/server/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Render } from '@nestjs/common'; 2 | import { AboutProps } from '../../dto/dist/AboutPage'; 3 | import { IndexProps } from '../../dto/dist/IndexPage'; 4 | 5 | @Controller() 6 | export class AppController { 7 | @Render('index') 8 | @Get() 9 | public index(): IndexProps { 10 | return { message: 'from server' }; 11 | } 12 | 13 | @Render('about') 14 | @Get('/about') 15 | public about(): AboutProps { 16 | return { message: 'server' }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/monorepo/server/src/application.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RenderModule } from 'nest-next'; 3 | import Next from 'next'; 4 | import { resolve } from 'path'; 5 | import { AppController } from './app.controller'; 6 | 7 | @Module({ 8 | controllers: [AppController], 9 | imports: [ 10 | RenderModule.forRootAsync( 11 | Next({ 12 | dev: process.env.NODE_ENV !== 'production', 13 | dir: resolve(__dirname, '../../ui'), 14 | }), 15 | { 16 | viewsDir: '' 17 | } 18 | ), 19 | ], 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /examples/monorepo/server/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; 3 | import 'reflect-metadata'; 4 | import { AppModule } from './application.module'; 5 | 6 | async function bootstrap() { 7 | const server = await NestFactory.create(AppModule, new FastifyAdapter()); 8 | 9 | await server.listen(3000); 10 | } 11 | 12 | bootstrap(); 13 | -------------------------------------------------------------------------------- /examples/monorepo/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "sourceMap": true, 6 | "outDir": "./dist", 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "incremental": true, 13 | "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo", 14 | "rootDir": "./src", 15 | "typeRoots": [ 16 | "./node_modules/@types" 17 | ] 18 | }, 19 | "exclude": ["node_modules", "./dist"], 20 | "include": ["./src/**/*.ts"], 21 | "files": ["./src/main.ts"], 22 | "references": [ 23 | { 24 | "path": "../dto" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/monorepo/ui/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /examples/monorepo/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-next-example-advanced-ui", 3 | "version": "1.0.0", 4 | "description": "The UI portion of an advanced example using nest-next", 5 | "license": "MIT", 6 | "dependencies": { 7 | "next": "^9.1.6" 8 | }, 9 | "devDependencies": { 10 | "@types/node": "^12.12.21", 11 | "rimraf": "^3.0.0", 12 | "typescript": "^3.7.3" 13 | }, 14 | "scripts": { 15 | "clean": "rimraf .next", 16 | "dev": "next", 17 | "build": "next build", 18 | "start": "next start" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/monorepo/ui/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next'; 2 | import Link from 'next/link'; 3 | 4 | import { AboutProps } from '../../dto/src/AboutPage'; 5 | 6 | const AboutPage: NextPage = props => ( 7 |
8 |

9 | This is about the {props.message} page.{' '} 10 | 11 | Go back to the home page 12 | 13 |

14 |
15 | ); 16 | AboutPage.getInitialProps = async context => { 17 | if (context.req) { 18 | return (context.query as unknown) as AboutProps; 19 | } 20 | return { message: 'client' }; 21 | }; 22 | export default AboutPage; 23 | -------------------------------------------------------------------------------- /examples/monorepo/ui/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next'; 2 | import Link from 'next/link'; 3 | 4 | import { IndexProps } from '../../dto/src/IndexPage'; 5 | 6 | const IndexPage: NextPage = props => ( 7 |
8 |

9 | Hello {props.message}.{' '} 10 | 11 | About us 12 | 13 |

14 |
15 | ); 16 | IndexPage.getInitialProps = async context => { 17 | if (context.req) { 18 | return (context.query as unknown) as IndexProps; 19 | } 20 | return { message: 'from client' }; 21 | }; 22 | 23 | export default IndexPage; 24 | -------------------------------------------------------------------------------- /examples/monorepo/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve" 20 | }, 21 | "exclude": [ 22 | "node_modules" 23 | ], 24 | "include": [ 25 | "next-env.d.ts", 26 | "**/*.ts", 27 | "**/*.tsx" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /examples/passthrough/.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | .yalc 3 | node_modules 4 | -------------------------------------------------------------------------------- /examples/passthrough/README.md: -------------------------------------------------------------------------------- 1 | # NEST-NEXT PASSTHROUGH EXAMPLE 2 | 3 | This example demonstrates how to pass through all requests not handled by Nest to Next. 4 | 5 | -------------------------------------------------------------------------------- /examples/passthrough/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/passthrough/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src/**/*"], 3 | "ext": "ts,tsx", 4 | "execMap": { 5 | "ts": "ts-node --project tsconfig.server.json" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/passthrough/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passthrough", 3 | "version": "1.0.0", 4 | "description": "nest-next passthrough example", 5 | "main": "index.js", 6 | "author": "Kyle McCarthy", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "nodemon src/main.ts", 10 | "build:clean": "rimraf .next", 11 | "build:ui": "npx next build", 12 | "build:server": "npx tsc --project tsconfig.server.json && npx babel .next/production-server -d .next/production-server --extensions \".js\"", 13 | "build": "yarn build:clean && yarn build:ui && yarn build:server", 14 | "start": "cross-env NODE_ENV=production node .next/production-server/main.js" 15 | }, 16 | "dependencies": { 17 | "@nestjs/common": "^8.4.4", 18 | "@nestjs/core": "^8.4.4", 19 | "@nestjs/platform-express": "^8.4.4", 20 | "nest-next": "9.6.0", 21 | "next": "^12.1.5", 22 | "react": "^18.0.0", 23 | "react-dom": "^18.0.0", 24 | "reflect-metadata": "^0.1.13", 25 | "rxjs": "^7.5.5" 26 | }, 27 | "devDependencies": { 28 | "@babel/cli": "^7.17.6", 29 | "@babel/core": "^7.17.9", 30 | "@types/node": "^17.0.23", 31 | "@types/react": "^18.0.4", 32 | "rimraf": "^3.0.2", 33 | "ts-node": "^10.7.0", 34 | "typescript": "^4.6.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/passthrough/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage, GetStaticProps } from 'next'; 2 | 3 | interface Body { 4 | message: string; 5 | } 6 | 7 | interface Props { 8 | data: Body | null; 9 | error: string | null; 10 | } 11 | 12 | const Page: NextPage = ({ data, error }) => { 13 | return ( 14 |
15 |

nest-next passthrough example

16 | 17 |

static props: {data && data.message ? data.message : error}

18 | 19 |

20 | 24 | Github 31 | View Repo 32 | 33 |

34 |
35 | ); 36 | }; 37 | 38 | // https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation 39 | export const getStaticProps: GetStaticProps = async () => { 40 | let data: any | null = null; 41 | let error: string | null = null; 42 | 43 | try { 44 | const res = await fetch('http://localhost:3000/data'); 45 | 46 | if (res.ok) { 47 | data = await res.json(); 48 | } else { 49 | error = res.statusText; 50 | } 51 | } catch (e) { 52 | error = e.message; 53 | } 54 | 55 | return { props: { data, error } }; 56 | }; 57 | 58 | export default Page; 59 | -------------------------------------------------------------------------------- /examples/passthrough/public/github-icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyle-mccarthy/nest-next/caddbba4a63ba40aa138b3bb63a321cb208fe30f/examples/passthrough/public/github-icon-32.png -------------------------------------------------------------------------------- /examples/passthrough/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query, Render } from '@nestjs/common'; 2 | 3 | @Controller() 4 | export class AppController { 5 | @Get('/data') 6 | public index() { 7 | return { message: 'Data from AppController' }; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/passthrough/src/application.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RenderModule } from 'nest-next'; 3 | import Next from 'next'; 4 | import { AppController } from './app.controller'; 5 | import { resolve } from 'path'; 6 | 7 | @Module({ 8 | imports: [ 9 | RenderModule.forRootAsync( 10 | Next({ 11 | dev: process.env.NODE_ENV !== 'production', 12 | dir: resolve(__dirname, '..'), 13 | }), 14 | { passthrough404: true, viewsDir: null }, 15 | ), 16 | ], 17 | controllers: [AppController], 18 | }) 19 | export class AppModule {} 20 | -------------------------------------------------------------------------------- /examples/passthrough/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './application.module'; 3 | 4 | async function bootstrap() { 5 | const server = await NestFactory.create(AppModule); 6 | 7 | await server.listen(3000); 8 | } 9 | 10 | bootstrap(); 11 | -------------------------------------------------------------------------------- /examples/passthrough/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "jsx": "preserve", 6 | "strict": true, 7 | "pretty": true, 8 | "noImplicitAny": true, 9 | "alwaysStrict": true, 10 | "noImplicitReturns": true, 11 | "noImplicitThis": true, 12 | "outDir": ".next", 13 | "moduleResolution": "node", 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true, 18 | "sourceMap": true, 19 | "skipLibCheck": true, 20 | "lib": [ 21 | "es2017", 22 | "dom" 23 | ], 24 | "baseUrl": ".", 25 | "typeRoots": [ 26 | "node_modules/@types", 27 | "./typings" 28 | ], 29 | "allowJs": true, 30 | "forceConsistentCasingInFileNames": true, 31 | "noEmit": true, 32 | "resolveJsonModule": true, 33 | "isolatedModules": true, 34 | "incremental": true 35 | }, 36 | "include": [ 37 | "./pages/**/*", 38 | "./src/**/*", 39 | "./ui/**/*" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /examples/passthrough/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": ".next/production-server", 5 | "declaration": true, 6 | "target": "es6", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "isolatedModules": false, 12 | "noEmit": false, 13 | "allowJs": false 14 | }, 15 | "include": ["./src/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /examples/passthrough/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-react", 5 | "tslint-config-prettier", 6 | "tslint-react-hooks" 7 | ], 8 | "rules": { 9 | "quotemark": [true, "single", "jsx-double"], 10 | "semicolon": [true, "always"], 11 | "trailing-comma": [true], 12 | "object-literal-sort-keys": false, 13 | "no-unused-variable": true, 14 | "interface-name": false, 15 | "no-empty-interface": false, 16 | "jsx-no-multiline-js": false, 17 | "jsx-no-target-blank": false, 18 | "no-shadowed-variable": [true, { "temporalDeadZone": false }], 19 | "member-ordering": [true, { "order": "fields-first" }], 20 | "react-hooks-nesting": "warning" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist'; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 4 | } 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | __export(require("./dist")); 7 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './dist'; 2 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const RENDER_METADATA = '@@module/render/RENDER_METADATA'; 2 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './render.filter'; 3 | export * from './render.module'; 4 | export * from './render.service'; 5 | export * from './types'; 6 | -------------------------------------------------------------------------------- /lib/next-utils.ts: -------------------------------------------------------------------------------- 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 | } 27 | -------------------------------------------------------------------------------- /lib/render.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentsHost, 3 | Catch, 4 | ExceptionFilter, 5 | HttpStatus, 6 | } from '@nestjs/common'; 7 | import { IncomingMessage, ServerResponse } from 'http'; 8 | import { parse as parseUrl } from 'url'; 9 | import { RenderService } from './render.service'; 10 | import { ErrorResponse } from './types'; 11 | 12 | @Catch() 13 | export class RenderFilter implements ExceptionFilter { 14 | private readonly service: RenderService; 15 | 16 | constructor(service: RenderService) { 17 | this.service = service; 18 | } 19 | 20 | /** 21 | * Nest isn't aware of how next handles routing for the build assets, let next 22 | * handle routing for any request that isn't handled by a controller 23 | * @param err 24 | * @param host 25 | */ 26 | public async catch(err: any, host: ArgumentsHost) { 27 | const ctx = host.switchToHttp(); 28 | const request = ctx.getRequest(); 29 | const response = ctx.getResponse(); 30 | 31 | if (response && request) { 32 | const requestHandler = this.service.getRequestHandler(); 33 | const errorRenderer = this.service.getErrorRenderer(); 34 | 35 | // these really should already always be set since it is done during the module registration 36 | // if somehow they aren't throw an error 37 | if (!requestHandler || !errorRenderer) { 38 | throw new Error( 39 | 'Request and/or error renderer not set on RenderService', 40 | ); 41 | } 42 | 43 | const isFastify = !!response.res; 44 | 45 | const res: ServerResponse = isFastify ? response.res : response; 46 | const req: IncomingMessage = isFastify ? request.raw : request; 47 | 48 | if (!res.headersSent && req.url) { 49 | // check to see if the URL requested is an internal nextjs route 50 | // if internal, the url is to some asset (ex /_next/*) that needs to be rendered by nextjs 51 | if (this.service.isInternalUrl(req.url)) { 52 | if (isFastify) { 53 | response.sent = true; 54 | } 55 | return requestHandler(req, res); 56 | } 57 | 58 | // let next handle the error 59 | // it's possible that the err doesn't contain a status code, if this is the case treat 60 | // it as an internal server error 61 | res.statusCode = err && err.status ? err.status : 500; 62 | 63 | const { pathname, query } = parseUrl(req.url, true); 64 | 65 | const errorHandler = this.service.getErrorHandler(); 66 | 67 | if (errorHandler) { 68 | await errorHandler(err, request, response, pathname, query); 69 | } 70 | 71 | if (response.sent === true || res.headersSent) { 72 | return; 73 | } 74 | 75 | const serializedErr = this.serializeError(err); 76 | 77 | if (isFastify) { 78 | response.sent = true; 79 | } 80 | 81 | if (res.statusCode === HttpStatus.NOT_FOUND) { 82 | if (this.service.passthroughNotFoundErrors()) { 83 | return requestHandler(req, res); 84 | } 85 | 86 | return errorRenderer(null, req, res, pathname, { 87 | ...query, 88 | [Symbol.for('Error')]: serializedErr, 89 | }); 90 | } 91 | 92 | return errorRenderer(serializedErr, req, res, pathname, query); 93 | } 94 | 95 | return; 96 | } 97 | 98 | // if the request and/or response are undefined (as with GraphQL) rethrow the error 99 | throw err; 100 | } 101 | 102 | /** 103 | * Serialize the error similarly to method used in Next -- parse error as Nest error type 104 | * @param err 105 | */ 106 | public serializeError(err: any): ErrorResponse { 107 | const out: ErrorResponse = {}; 108 | 109 | if (!err) { 110 | return out; 111 | } 112 | 113 | if (err.stack && this.service.isDev()) { 114 | out.stack = err.stack; 115 | } 116 | 117 | if (err.response && typeof err.response === 'object') { 118 | const { statusCode, error, message } = err.response; 119 | out.statusCode = statusCode; 120 | out.name = error; 121 | out.message = message; 122 | } else if (err.message && typeof err.message === 'object') { 123 | const { statusCode, error, message } = err.message; 124 | out.statusCode = statusCode; 125 | out.name = error; 126 | out.message = message; 127 | } 128 | 129 | if (!out.statusCode && err.status) { 130 | out.statusCode = err.status; 131 | } 132 | 133 | if (!out.message && err.message) { 134 | out.message = err.message; 135 | } 136 | 137 | return out; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lib/render.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | import { ApplicationConfig, HttpAdapterHost } from '@nestjs/core'; 3 | 4 | import Server from 'next'; 5 | import type { DynamicRoutes } from 'next/dist/server/router'; 6 | 7 | import { RenderFilter } from './render.filter'; 8 | import { RenderService } from './render.service'; 9 | 10 | import type { RendererConfig } from './types'; 11 | 12 | @Module({ 13 | providers: [RenderService], 14 | }) 15 | export class RenderModule { 16 | /** 17 | * Registers this module with a Next app at the root of the Nest app. 18 | * 19 | * @param next The Next app to register. 20 | * @param options Options for the RenderModule. 21 | */ 22 | public static async forRootAsync( 23 | next: ReturnType, 24 | options: Partial = {}, 25 | ): Promise { 26 | if (typeof next.prepare === 'function') { 27 | await next.prepare(); 28 | } 29 | 30 | const nextConfig = (next as any).nextConfig; 31 | const nextServer = (next as any).server; 32 | 33 | const basePath = nextConfig 34 | ? nextConfig.basePath 35 | : nextServer.nextConfig.basePath; 36 | 37 | const dynamicRoutes = (nextServer.dynamicRoutes as DynamicRoutes).map( 38 | (route) => route.page, 39 | ); 40 | 41 | const config: Partial = { 42 | basePath, 43 | dynamicRoutes, 44 | ...options, 45 | }; 46 | 47 | return { 48 | exports: [RenderService], 49 | module: RenderModule, 50 | providers: [ 51 | { 52 | inject: [HttpAdapterHost], 53 | provide: RenderService, 54 | useFactory: (nestHost: HttpAdapterHost): RenderService => { 55 | return RenderService.init( 56 | config, 57 | next.getRequestHandler(), 58 | next.render.bind(next), 59 | next.renderError.bind(next), 60 | nestHost.httpAdapter, 61 | ); 62 | }, 63 | }, 64 | { 65 | inject: [ApplicationConfig, RenderService], 66 | provide: RenderFilter, 67 | useFactory: ( 68 | nestConfig: ApplicationConfig, 69 | service: RenderService, 70 | ) => { 71 | const filter = new RenderFilter(service); 72 | nestConfig.addGlobalFilter(filter); 73 | 74 | return filter; 75 | }, 76 | }, 77 | ], 78 | }; 79 | } 80 | 81 | constructor( 82 | private readonly httpAdapterHost: HttpAdapterHost, 83 | private readonly applicationConfig: ApplicationConfig, 84 | private readonly service: RenderService, 85 | ) {} 86 | 87 | /** 88 | * Register the RenderModule. 89 | * 90 | * @deprecated Use RenderModule.forRootAsync() when importing the module, and remove this post init call. 91 | * @param _app Previously, the Nest app. Now ignored. 92 | * @param next The Next app. 93 | * @param options Options for the RenderModule. 94 | */ 95 | public register( 96 | _app: any, 97 | next: ReturnType, 98 | options: Partial = {}, 99 | ) { 100 | console.error( 101 | 'RenderModule.register() is deprecated and will be removed in a future release.', 102 | ); 103 | console.error( 104 | 'Please use RenderModule.forRootAsync() when importing the module, and remove this post init call.', 105 | ); 106 | 107 | if (!this.service.isInitialized()) { 108 | this.service.mergeConfig(options); 109 | this.service.setRequestHandler(next.getRequestHandler()); 110 | this.service.setRenderer(next.render.bind(next)); 111 | this.service.setErrorRenderer(next.renderError.bind(next)); 112 | this.service.bindHttpServer(this.httpAdapterHost.httpAdapter); 113 | this.applicationConfig.useGlobalFilters(new RenderFilter(this.service)); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/render.service.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { HttpServer, InternalServerErrorException } from '@nestjs/common'; 3 | import { ParsedUrlQuery } from 'querystring'; 4 | import { isInternalUrl } from './next-utils'; 5 | import { 6 | ErrorHandler, 7 | ErrorRenderer, 8 | RenderableResponse, 9 | Renderer, 10 | RendererConfig, 11 | RequestHandler, 12 | } from './types'; 13 | 14 | import { getNamedRouteRegex } from './vendor/next/route-regex'; 15 | import { interpolateDynamicPath } from './vendor/next/interpolate-dynamic-path'; 16 | import { isDynamicRoute } from './vendor/next/is-dynamic'; 17 | 18 | export class RenderService { 19 | public static init( 20 | config: Partial, 21 | handler: RequestHandler, 22 | renderer: Renderer, 23 | errorRenderer: ErrorRenderer, 24 | server: HttpServer, 25 | ): RenderService { 26 | const self = new RenderService(); 27 | self.mergeConfig(config); 28 | self.setRequestHandler(handler); 29 | self.setRenderer(renderer); 30 | self.setErrorRenderer(errorRenderer); 31 | self.bindHttpServer(server); 32 | return self; 33 | } 34 | 35 | private initialized = false; 36 | private requestHandler?: RequestHandler; 37 | private renderer?: Renderer; 38 | private errorRenderer?: ErrorRenderer; 39 | private errorHandler?: ErrorHandler; 40 | private config: RendererConfig = { 41 | dev: process.env.NODE_ENV !== 'production', 42 | passthrough404: false, 43 | viewsDir: '/views', 44 | }; 45 | private dynamicRouteRegexes = new Map< 46 | string, 47 | ReturnType 48 | >(); 49 | 50 | /** 51 | * Merge the default config with the config obj passed to method 52 | * @param config 53 | */ 54 | public mergeConfig(config: Partial) { 55 | if (typeof config.dev === 'boolean') { 56 | this.config.dev = config.dev; 57 | } 58 | if (typeof config.viewsDir === 'string' || config.viewsDir === null) { 59 | this.config.viewsDir = config.viewsDir; 60 | } 61 | if (typeof config.passthrough404 === 'boolean') { 62 | this.config.passthrough404 = config.passthrough404; 63 | } 64 | if (typeof config.basePath === 'string') { 65 | this.config.basePath = config.basePath; 66 | } 67 | if (config.dynamicRoutes?.length) { 68 | this.initializeDynamicRouteRegexes(config.dynamicRoutes); 69 | } 70 | } 71 | 72 | /** 73 | * Set the directory that Next will render pages from 74 | * @param path 75 | */ 76 | public setViewsDir(path: string | null) { 77 | this.config.viewsDir = path; 78 | } 79 | 80 | /** 81 | * Get the directory that Next renders pages from 82 | */ 83 | public getViewsDir() { 84 | return this.config.viewsDir; 85 | } 86 | 87 | /** 88 | * Explicitly set if env is or not dev 89 | * @param dev 90 | */ 91 | public setIsDev(dev: boolean) { 92 | this.config.dev = dev; 93 | } 94 | 95 | /** 96 | * Get if the env is dev 97 | */ 98 | public isDev(): boolean { 99 | return this.config.dev!; 100 | } 101 | 102 | /** 103 | * Should 404 errors passthrough to Next 104 | */ 105 | public passthroughNotFoundErrors(): boolean { 106 | return !!this.config.passthrough404; 107 | } 108 | 109 | /** 110 | * Set the default request handler provided by next 111 | * @param handler 112 | */ 113 | public setRequestHandler(handler: RequestHandler) { 114 | this.requestHandler = handler; 115 | } 116 | 117 | /** 118 | * Get the default request handler 119 | */ 120 | public getRequestHandler(): RequestHandler | undefined { 121 | return this.requestHandler; 122 | } 123 | 124 | /** 125 | * Set the render function provided by next 126 | * @param renderer 127 | */ 128 | public setRenderer(renderer: Renderer) { 129 | this.renderer = renderer; 130 | } 131 | 132 | /** 133 | * Get the render function provided by next 134 | */ 135 | public getRenderer(): Renderer | undefined { 136 | return this.renderer; 137 | } 138 | 139 | /** 140 | * Set nextjs error renderer 141 | * @param errorRenderer 142 | */ 143 | public setErrorRenderer(errorRenderer: ErrorRenderer) { 144 | this.errorRenderer = errorRenderer; 145 | } 146 | 147 | /** 148 | * Get nextjs error renderer 149 | */ 150 | public getErrorRenderer(): ErrorRenderer | undefined { 151 | return this.errorRenderer; 152 | } 153 | 154 | /** 155 | * Set a custom error handler 156 | * @param handler 157 | */ 158 | public setErrorHandler(handler: ErrorHandler) { 159 | this.errorHandler = handler; 160 | } 161 | 162 | /** 163 | * Get the custom error handler 164 | */ 165 | public getErrorHandler(): ErrorHandler | undefined { 166 | return this.errorHandler; 167 | } 168 | 169 | /** 170 | * Check if the URL is internal to nextjs 171 | * @param url 172 | */ 173 | public isInternalUrl(url: string): boolean { 174 | if (this.config.basePath && url.startsWith(this.config.basePath)) { 175 | return isInternalUrl(url.replace(this.config.basePath, '')); 176 | } 177 | 178 | return isInternalUrl(url); 179 | } 180 | 181 | public initializeDynamicRouteRegexes(routes: string[] = []) { 182 | for (const route of routes) { 183 | const pathname = this.getNormalizedPath(route); 184 | 185 | this.dynamicRouteRegexes.set(route, getNamedRouteRegex(pathname)); 186 | } 187 | } 188 | 189 | /** 190 | * Check if the service has been initialized by the module 191 | */ 192 | public isInitialized(): boolean { 193 | return this.initialized; 194 | } 195 | 196 | /** 197 | * Bind to the render function for the HttpServer that nest is using and override 198 | * it to allow for next to render the page 199 | * @param server 200 | */ 201 | public bindHttpServer(server: HttpServer) { 202 | if (this.initialized) { 203 | throw new Error('RenderService: already initialized'); 204 | } 205 | 206 | this.initialized = true; 207 | const renderer = this.getRenderer(); 208 | const getViewPath = this.getViewPath.bind(this); 209 | 210 | server.render = (response: any, view: string, data: any) => { 211 | const isFastify = response.request !== undefined; 212 | 213 | const res = isFastify ? response.res : response; 214 | const req = isFastify ? response.request.raw : response.req; 215 | 216 | if (req && res && renderer) { 217 | if (isFastify) { 218 | response.sent = true; 219 | } 220 | return renderer(req, res, getViewPath(view, req.params), data); 221 | } else if (!renderer) { 222 | throw new InternalServerErrorException( 223 | 'RenderService: renderer is not set', 224 | ); 225 | } else if (!res) { 226 | throw new InternalServerErrorException( 227 | 'RenderService: could not get the response', 228 | ); 229 | } else if (!req) { 230 | throw new InternalServerErrorException( 231 | 'RenderService: could not get the request', 232 | ); 233 | } 234 | 235 | throw new Error('RenderService: failed to render'); 236 | }; 237 | 238 | let isFastifyAdapter = false; 239 | try { 240 | const { FastifyAdapter } = require('@nestjs/platform-fastify'); 241 | isFastifyAdapter = server instanceof FastifyAdapter; 242 | } catch (e) { 243 | // Failed to load @nestjs/platform-fastify probably. Assume not fastify. 244 | } 245 | 246 | // and nextjs renderer to reply/response 247 | if (isFastifyAdapter) { 248 | server 249 | .getInstance() 250 | .decorateReply('render', function ( 251 | view: string, 252 | data?: ParsedUrlQuery, 253 | ) { 254 | const res = this.res; 255 | const req = this.request.raw; 256 | 257 | if (!renderer) { 258 | throw new InternalServerErrorException( 259 | 'RenderService: renderer is not set', 260 | ); 261 | } 262 | 263 | this.sent = true; 264 | 265 | return renderer(req, res, getViewPath(view, req.params), data); 266 | } as RenderableResponse['render']); 267 | } else { 268 | server.getInstance().use((req: any, res: any, next: () => any) => { 269 | res.render = ((view: string, data?: ParsedUrlQuery) => { 270 | if (!renderer) { 271 | throw new InternalServerErrorException( 272 | 'RenderService: renderer is not set', 273 | ); 274 | } 275 | 276 | return renderer(req, res, getViewPath(view, req.params), data); 277 | }) as RenderableResponse['render']; 278 | 279 | next(); 280 | }); 281 | } 282 | } 283 | 284 | public getNormalizedPath(view: string) { 285 | const basePath = this.getViewsDir() ?? ''; 286 | const denormalizedPath = [basePath, view].join( 287 | view.startsWith('/') ? '' : '/', 288 | ); 289 | 290 | const pathname = path.posix.normalize(denormalizedPath); 291 | 292 | return pathname; 293 | } 294 | 295 | /** 296 | * Format the path to the view including path parameters interpolation 297 | * Copied Next.js code is used for interpolation 298 | * @param view 299 | * @param params 300 | */ 301 | protected getViewPath(view: string, params: ParsedUrlQuery) { 302 | const pathname = this.getNormalizedPath(view); 303 | 304 | if (!isDynamicRoute(pathname)) { 305 | return pathname; 306 | } 307 | 308 | const regex = this.dynamicRouteRegexes.get(pathname); 309 | 310 | if (!regex) { 311 | console.warn( 312 | `RenderService: view ${view} is dynamic and has no route regex`, 313 | ); 314 | } 315 | 316 | return interpolateDynamicPath(pathname, params, regex); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQuery } from 'querystring'; 2 | 3 | export type RequestHandler = (req: any, res: any, query?: any) => Promise; 4 | 5 | export type Renderer = ( 6 | req: any, 7 | res: any, 8 | view: string, 9 | params?: DataType, 10 | ) => Promise; 11 | 12 | export interface RenderableResponse { 13 | render(view: string, data?: DataType): ReturnType>; 14 | } 15 | 16 | export type ErrorRenderer = ( 17 | err: any, 18 | req: any, 19 | res: any, 20 | pathname: any, 21 | query?: any, 22 | ) => Promise; 23 | 24 | export type ErrorHandler = ( 25 | err: any, 26 | req: any, 27 | res: any, 28 | pathname: any, 29 | query: ParsedUrlQuery, 30 | ) => Promise; 31 | 32 | export interface RendererConfig { 33 | viewsDir: null | string; 34 | basePath?: string; 35 | dev: boolean; 36 | passthrough404?: boolean; 37 | dynamicRoutes?: string[]; 38 | } 39 | 40 | export interface ErrorResponse { 41 | name?: string; 42 | message?: string; 43 | stack?: any; 44 | statusCode?: number; 45 | } 46 | -------------------------------------------------------------------------------- /lib/vendor/next/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Vercel, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /lib/vendor/next/escape-regexp.ts: -------------------------------------------------------------------------------- 1 | // regexp is based on https://github.com/sindresorhus/escape-string-regexp 2 | const reHasRegExp = /[|\\{}()[\]^$+*?.-]/; 3 | const reReplaceRegExp = /[|\\{}()[\]^$+*?.-]/g; 4 | 5 | export function escapeStringRegexp(str: string) { 6 | // see also: https://github.com/lodash/lodash/blob/2da024c3b4f9947a48517639de7560457cd4ec6c/escapeRegExp.js#L23 7 | if (reHasRegExp.test(str)) { 8 | return str.replace(reReplaceRegExp, '\\$&'); 9 | } 10 | return str; 11 | } 12 | -------------------------------------------------------------------------------- /lib/vendor/next/interpolate-dynamic-path.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQuery } from 'querystring'; 2 | import { getNamedRouteRegex } from './route-regex'; 3 | 4 | export function interpolateDynamicPath( 5 | pathname: string, 6 | params: ParsedUrlQuery, 7 | defaultRouteRegex?: ReturnType | undefined, 8 | ) { 9 | if (!defaultRouteRegex) return pathname; 10 | 11 | for (const param of Object.keys(defaultRouteRegex.groups)) { 12 | const { optional, repeat } = defaultRouteRegex.groups[param]; 13 | let builtParam = `[${repeat ? '...' : ''}${param}]`; 14 | 15 | if (optional) { 16 | builtParam = `[${builtParam}]`; 17 | } 18 | 19 | const paramIdx = pathname!.indexOf(builtParam); 20 | 21 | if (paramIdx > -1) { 22 | let paramValue: string; 23 | const value = params[param]; 24 | 25 | if (Array.isArray(value)) { 26 | paramValue = value.map((v) => v && encodeURIComponent(v)).join('/'); 27 | } else if (value) { 28 | paramValue = encodeURIComponent(value); 29 | } else { 30 | paramValue = ''; 31 | } 32 | 33 | pathname = 34 | pathname.slice(0, paramIdx) + 35 | paramValue + 36 | pathname.slice(paramIdx + builtParam.length); 37 | } 38 | } 39 | 40 | return pathname; 41 | } 42 | -------------------------------------------------------------------------------- /lib/vendor/next/is-dynamic.ts: -------------------------------------------------------------------------------- 1 | // Identify /[param]/ in route string 2 | const TEST_ROUTE = /\/\[[^/]+?\](?=\/|$)/; 3 | 4 | export function isDynamicRoute(route: string): boolean { 5 | return TEST_ROUTE.test(route); 6 | } 7 | -------------------------------------------------------------------------------- /lib/vendor/next/remove-trailing-slash.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes the trailing slash for a given route or page path. Preserves the 3 | * root page. Examples: 4 | * - `/foo/bar/` -> `/foo/bar` 5 | * - `/foo/bar` -> `/foo/bar` 6 | * - `/` -> `/` 7 | */ 8 | export function removeTrailingSlash(route: string) { 9 | return route.replace(/\/$/, '') || '/'; 10 | } 11 | -------------------------------------------------------------------------------- /lib/vendor/next/route-regex.ts: -------------------------------------------------------------------------------- 1 | import { escapeStringRegexp } from './escape-regexp'; 2 | import { removeTrailingSlash } from './remove-trailing-slash'; 3 | 4 | export interface Group { 5 | pos: number; 6 | repeat: boolean; 7 | optional: boolean; 8 | } 9 | 10 | export interface RouteRegex { 11 | groups: { [groupName: string]: Group }; 12 | re: RegExp; 13 | } 14 | 15 | /** 16 | * Parses a given parameter from a route to a data structure that can be used 17 | * to generate the parametrized route. Examples: 18 | * - `[...slug]` -> `{ name: 'slug', repeat: true, optional: true }` 19 | * - `[foo]` -> `{ name: 'foo', repeat: false, optional: true }` 20 | * - `bar` -> `{ name: 'bar', repeat: false, optional: false }` 21 | */ 22 | function parseParameter(param: string) { 23 | const optional = param.startsWith('[') && param.endsWith(']'); 24 | if (optional) { 25 | param = param.slice(1, -1); 26 | } 27 | const repeat = param.startsWith('...'); 28 | if (repeat) { 29 | param = param.slice(3); 30 | } 31 | return { key: param, repeat, optional }; 32 | } 33 | 34 | function getParametrizedRoute(route: string) { 35 | const segments = removeTrailingSlash(route).slice(1).split('/'); 36 | const groups: { [groupName: string]: Group } = {}; 37 | let groupIndex = 1; 38 | return { 39 | parameterizedRoute: segments 40 | .map((segment) => { 41 | if (segment.startsWith('[') && segment.endsWith(']')) { 42 | const { key, optional, repeat } = parseParameter( 43 | segment.slice(1, -1), 44 | ); 45 | groups[key] = { pos: groupIndex++, repeat, optional }; 46 | return repeat ? (optional ? '(?:/(.+?))?' : '/(.+?)') : '/([^/]+?)'; 47 | } else { 48 | return `/${escapeStringRegexp(segment)}`; 49 | } 50 | }) 51 | .join(''), 52 | groups, 53 | }; 54 | } 55 | 56 | /** 57 | * From a normalized route this function generates a regular expression and 58 | * a corresponding groups object intended to be used to store matching groups 59 | * from the regular expression. 60 | */ 61 | export function getRouteRegex(normalizedRoute: string): RouteRegex { 62 | const { parameterizedRoute, groups } = getParametrizedRoute(normalizedRoute); 63 | return { 64 | re: new RegExp(`^${parameterizedRoute}(?:/)?$`), 65 | groups: groups, 66 | }; 67 | } 68 | 69 | /** 70 | * Builds a function to generate a minimal routeKey using only a-z and minimal 71 | * number of characters. 72 | */ 73 | function buildGetSafeRouteKey() { 74 | let routeKeyCharCode = 97; 75 | let routeKeyCharLength = 1; 76 | 77 | return () => { 78 | let routeKey = ''; 79 | for (let i = 0; i < routeKeyCharLength; i++) { 80 | routeKey += String.fromCharCode(routeKeyCharCode); 81 | routeKeyCharCode++; 82 | 83 | if (routeKeyCharCode > 122) { 84 | routeKeyCharLength++; 85 | routeKeyCharCode = 97; 86 | } 87 | } 88 | return routeKey; 89 | }; 90 | } 91 | 92 | function getNamedParametrizedRoute(route: string) { 93 | const segments = removeTrailingSlash(route).slice(1).split('/'); 94 | const getSafeRouteKey = buildGetSafeRouteKey(); 95 | const routeKeys: { [named: string]: string } = {}; 96 | return { 97 | namedParameterizedRoute: segments 98 | .map((segment) => { 99 | if (segment.startsWith('[') && segment.endsWith(']')) { 100 | const { key, optional, repeat } = parseParameter( 101 | segment.slice(1, -1), 102 | ); 103 | // replace any non-word characters since they can break 104 | // the named regex 105 | let cleanedKey = key.replace(/\W/g, ''); 106 | let invalidKey = false; 107 | 108 | // check if the key is still invalid and fallback to using a known 109 | // safe key 110 | if (cleanedKey.length === 0 || cleanedKey.length > 30) { 111 | invalidKey = true; 112 | } 113 | if (!isNaN(parseInt(cleanedKey.slice(0, 1)))) { 114 | invalidKey = true; 115 | } 116 | 117 | if (invalidKey) { 118 | cleanedKey = getSafeRouteKey(); 119 | } 120 | 121 | routeKeys[cleanedKey] = key; 122 | return repeat 123 | ? optional 124 | ? `(?:/(?<${cleanedKey}>.+?))?` 125 | : `/(?<${cleanedKey}>.+?)` 126 | : `/(?<${cleanedKey}>[^/]+?)`; 127 | } else { 128 | return `/${escapeStringRegexp(segment)}`; 129 | } 130 | }) 131 | .join(''), 132 | routeKeys, 133 | }; 134 | } 135 | 136 | /** 137 | * This function extends `getRouteRegex` generating also a named regexp where 138 | * each group is named along with a routeKeys object that indexes the assigned 139 | * named group with its corresponding key. 140 | */ 141 | export function getNamedRouteRegex(normalizedRoute: string) { 142 | const result = getNamedParametrizedRoute(normalizedRoute); 143 | return { 144 | ...getRouteRegex(normalizedRoute), 145 | namedRegex: `^${result.namedParameterizedRoute}(?:/)?$`, 146 | routeKeys: result.routeKeys, 147 | }; 148 | } 149 | 150 | /** 151 | * Generates a named regexp. 152 | * This is intended to be using for build time only. 153 | */ 154 | export function getNamedMiddlewareRegex( 155 | normalizedRoute: string, 156 | options: { 157 | catchAll?: boolean; 158 | }, 159 | ) { 160 | const { parameterizedRoute } = getParametrizedRoute(normalizedRoute); 161 | const { catchAll = true } = options; 162 | if (parameterizedRoute === '/') { 163 | const catchAllRegex = catchAll ? '.*' : ''; 164 | return { 165 | namedRegex: `^/${catchAllRegex}$`, 166 | }; 167 | } 168 | 169 | const { namedParameterizedRoute } = 170 | getNamedParametrizedRoute(normalizedRoute); 171 | const catchAllGroupedRegex = catchAll ? '(?:(/.*)?)' : ''; 172 | return { 173 | namedRegex: `^${namedParameterizedRoute}${catchAllGroupedRegex}$`, 174 | }; 175 | } 176 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-next", 3 | "version": "10.1.0", 4 | "description": "Nestjs module to allow for server side rendering with Nextjs", 5 | "author": "Kyle McCarthy", 6 | "license": "MIT", 7 | "keywords": [ 8 | "nestjs", 9 | "nextjs" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/kyle-mccarthy/nest-next.git" 14 | }, 15 | "scripts": { 16 | "build": "npx del-cli ./dist --force && tsc -p tsconfig.json", 17 | "prepublish": "yarn run build", 18 | "publish-public": "yarn publish --access public", 19 | "lint": "node ./node_modules/eslint/bin/eslint.js lib/**", 20 | "test": "bash run_tests.sh test-app", 21 | "test-dev": "bash run_tests.sh test-app-dev" 22 | }, 23 | "devDependencies": { 24 | "@nestjs/common": "^8.4.4", 25 | "@nestjs/core": "^8.4.4", 26 | "@nestjs/platform-fastify": "^8.4.4", 27 | "@types/node": "^17.0.23", 28 | "@typescript-eslint/eslint-plugin": "^5.19.0", 29 | "@typescript-eslint/parser": "^5.19.0", 30 | "del-cli": "^4.0.1", 31 | "eslint": "^8.13.0", 32 | "eslint-config-prettier": "^8.5.0", 33 | "eslint-plugin-prettier": "^4.0.0", 34 | "eslint-plugin-promise": "^6.0.0", 35 | "mermaid.cli": "^0.5", 36 | "next": "^12.1.5", 37 | "prettier": "^2.6.2", 38 | "reflect-metadata": "^0.1", 39 | "tslint": "^6.1.3", 40 | "tslint-config-prettier": "^1.18", 41 | "typescript": "^4.6.3" 42 | }, 43 | "dependencies": { 44 | "rxjs": "^7.5.5" 45 | }, 46 | "peerDependencies": { 47 | "@nestjs/common": ">=6.4", 48 | "@nestjs/core": ">=6.4", 49 | "next": ">=9" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm run build && cp -r dist tests/test-app/nest-next-dist 4 | 5 | (cd tests; NODE_VERSION=${NODE_VERSION:-14} NEXT_VERSION=${NEXT_VERSION:-12} docker-compose up -d --build $1) 6 | 7 | (cd tests/e2e; npm install; npx playwright install; npx playwright test) 8 | -------------------------------------------------------------------------------- /tests/.env.example: -------------------------------------------------------------------------------- 1 | NODE_VERSION=14 2 | NEXT_VERSION=11 3 | -------------------------------------------------------------------------------- /tests/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | 3 | services: 4 | test-app: 5 | build: 6 | context: test-app 7 | dockerfile: Dockerfile 8 | args: 9 | - NODE_VERSION=${NODE_VERSION} 10 | - NEXT_VERSION=${NEXT_VERSION} 11 | ports: 12 | - 3000:3000 13 | 14 | test-app-dev: 15 | build: 16 | context: test-app 17 | dockerfile: Dockerfile.dev 18 | args: 19 | - NODE_VERSION=${NODE_VERSION} 20 | - NEXT_VERSION=${NEXT_VERSION} 21 | ports: 22 | - 3000:3000 23 | -------------------------------------------------------------------------------- /tests/e2e/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /playwright/.cache/ 5 | -------------------------------------------------------------------------------- /tests/e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": {}, 7 | "keywords": [], 8 | "author": "", 9 | "license": "ISC", 10 | "devDependencies": { 11 | "@playwright/test": "^1.27.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/e2e/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { PlaywrightTestConfig } from '@playwright/test'; 2 | 3 | const config: PlaywrightTestConfig = { 4 | use: { 5 | baseURL: 'http://localhost:3000', 6 | screenshot: 'only-on-failure', 7 | trace: 'on-first-retry', 8 | ignoreHTTPSErrors: true, 9 | }, 10 | retries: 1, 11 | reporter: [['html', { open: process.env.CI ? 'never' : 'on-failure' }]], 12 | quiet: true, 13 | fullyParallel: true, 14 | forbidOnly: Boolean(process.env.CI), 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /tests/e2e/specs/gsp-dynamic.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('getStaticProps dynamic pages', () => { 4 | test('page 1', async ({ page }) => { 5 | await page.goto('/1'); 6 | 7 | await expect(page.locator('h1')).toHaveText('1'); 8 | }); 9 | 10 | test('page 2', async ({ page }) => { 11 | await page.goto('/2'); 12 | 13 | await expect(page.locator('h1')).toHaveText('2'); 14 | }); 15 | 16 | test('fallback page 3', async ({ page }) => { 17 | await page.goto('/3'); 18 | 19 | await expect(page.locator('h1')).toHaveText('3'); 20 | }); 21 | 22 | test('should navigate to home', async ({ page }) => { 23 | await page.goto('/1'); 24 | 25 | await page.locator('a').click(); 26 | 27 | await expect(page.locator('h1')).toHaveText('HOME'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/e2e/specs/gsp.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('getStaticProps pages', () => { 4 | test('homepage', async ({ page }) => { 5 | await page.goto('/'); 6 | 7 | await expect(page.locator('h1')).toHaveText('HOME'); 8 | }); 9 | 10 | test('should navigate to 1', async ({ page }) => { 11 | await page.goto('/'); 12 | 13 | await page.locator('a').click(); 14 | 15 | await expect(page.locator('h1')).toHaveText('1'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/e2e/specs/gssp-dynamic.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('getServerSideProps dynamic pages', () => { 4 | test('blog post "some-post"', async ({ page }) => { 5 | await page.goto('/blog-posts/some-post'); 6 | 7 | await expect(page.locator('h1')).toHaveText('some-post'); 8 | }); 9 | 10 | test('blog post "other-post"', async ({ page }) => { 11 | await page.goto('/blog-posts/other-post'); 12 | 13 | await expect(page.locator('h1')).toHaveText('other-post'); 14 | }); 15 | 16 | test('should navigate to blog-posts', async ({ page }) => { 17 | await page.goto('/blog-posts/some-post'); 18 | 19 | await page.locator('a').click(); 20 | 21 | await expect(page.locator('h1')).toHaveText('FALLBACK BLOG POSTS'); 22 | }); 23 | 24 | // FIXME nested slug paths [...slug] dont work - see https://github.com/kyle-mccarthy/nest-next/issues/101 25 | test.skip('any about page', async ({ page }) => { 26 | await page.goto('/about/any-page/nested'); 27 | 28 | await expect(page.locator('h1')).toHaveText('ALL ABOUT'); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/e2e/specs/gssp.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('getServerSideProps pages', () => { 4 | test('blog posts', async ({ page }) => { 5 | await page.goto('/blog-posts'); 6 | 7 | await expect(page.locator('h1')).toHaveText('BLOG POSTS'); 8 | }); 9 | 10 | test('should navigate to blog post "some-post"', async ({ page }) => { 11 | await page.goto('/blog-posts'); 12 | 13 | await page.locator('a').click(); 14 | 15 | await expect(page.locator('h1')).toHaveText('some-post'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/test-app/.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | dist 4 | nest-next-dist 5 | -------------------------------------------------------------------------------- /tests/test-app/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NODE_VERSION=14 2 | 3 | FROM node:$NODE_VERSION 4 | 5 | WORKDIR /app 6 | 7 | COPY . . 8 | 9 | RUN npm install 10 | 11 | ARG NEXT_VERSION=12 12 | ENV NEXT_VERSION=${NEXT_VERSION} 13 | RUN npm install next@$NEXT_VERSION --save 14 | RUN if [ "${NEXT_VERSION}" -ge "13" ]; then npm install react@18 react-dom@18 @types/react@18 @types/react-dom@18; fi 15 | 16 | RUN npm run build 17 | 18 | ENTRYPOINT ["npm", "run", "start"] 19 | -------------------------------------------------------------------------------- /tests/test-app/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | ARG NODE_VERSION=14 2 | 3 | FROM node:$NODE_VERSION 4 | 5 | WORKDIR /app 6 | 7 | COPY . . 8 | 9 | RUN npm install 10 | 11 | ARG NEXT_VERSION=12 12 | ENV NEXT_VERSION=${NEXT_VERSION} 13 | RUN npm install next@$NEXT_VERSION --save 14 | RUN if [ "${NEXT_VERSION}" -ge "13" ]; then npm install react@18 react-dom@18 @types/react@18 @types/react-dom@18; fi 15 | 16 | ENTRYPOINT ["npm", "run", "start:dev"] 17 | -------------------------------------------------------------------------------- /tests/test-app/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src/server", 4 | "entryFile": "main" 5 | } 6 | -------------------------------------------------------------------------------- /tests/test-app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /tests/test-app/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | basePath: process.env.BASE_PATH, 3 | }; 4 | -------------------------------------------------------------------------------- /tests/test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-app", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "Alexey Yakovlev ", 6 | "private": true, 7 | "license": "MIT", 8 | "scripts": { 9 | "prebuild": "rm -rf dist", 10 | "build": "yarn build:next && yarn build:nest", 11 | "build:next": "next build", 12 | "build:nest": "nest build --path ./tsconfig.server.json", 13 | "start": "node dist/main.js", 14 | "start:next": "next dev", 15 | "start:dev": "NODE_ENV=development nest start --watch --path tsconfig.server.json" 16 | }, 17 | "dependencies": { 18 | "@nestjs/common": "^8.0.0", 19 | "@nestjs/core": "^8.0.0", 20 | "@nestjs/platform-express": "^8.0.0", 21 | "next": "^11.1.4", 22 | "react": "17.0.2", 23 | "react-dom": "17.0.2", 24 | "reflect-metadata": "^0.1.13", 25 | "rxjs": "^7.2.0" 26 | }, 27 | "devDependencies": { 28 | "@nestjs/cli": "^8.0.0", 29 | "@nestjs/schematics": "^8.0.0", 30 | "@nestjs/testing": "^8.0.0", 31 | "@types/express": "^4.17.13", 32 | "@types/node": "^16.0.0", 33 | "@types/react": "17.0.2", 34 | "@types/react-dom": "17.0.2", 35 | "ts-loader": "^9.2.3", 36 | "ts-node": "^10.0.0", 37 | "tsconfig-paths": "^3.10.1", 38 | "typescript": "^4.3.5", 39 | "webpack": "^5.46.0", 40 | "webpack-node-externals": "^3.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/test-app/src/pages/[id].tsx: -------------------------------------------------------------------------------- 1 | import { GetStaticPaths, GetStaticProps } from 'next'; 2 | import Link from 'next/link'; 3 | import { FC } from 'react'; 4 | 5 | type TPostProps = { 6 | id: string; 7 | }; 8 | 9 | const Post: FC = ({ id }) => { 10 | return ( 11 |
12 |

{id}

13 |
14 | TO HOME 15 |
16 |
17 | ); 18 | }; 19 | 20 | export const getStaticPaths: GetStaticPaths = async () => { 21 | const paths = [1, 2].map((id) => ({ params: { id: String(id) } })); 22 | 23 | return { paths, fallback: true }; 24 | }; 25 | 26 | export const getStaticProps: GetStaticProps = async (ctx) => { 27 | const id = ctx.params!.id; 28 | 29 | return { props: { id } }; 30 | }; 31 | 32 | export default Post; 33 | -------------------------------------------------------------------------------- /tests/test-app/src/pages/about/[...all].tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next'; 2 | import { FC } from 'react'; 3 | 4 | type TAboutAllProps = { 5 | query: any; 6 | }; 7 | 8 | const AboutAll: FC = ({ query }) => { 9 | return ( 10 |
11 |

ALL ABOUT

12 |
{query}
13 |
14 | ); 15 | }; 16 | 17 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 18 | return { props: { query: ctx.query } }; 19 | }; 20 | 21 | export default AboutAll; 22 | -------------------------------------------------------------------------------- /tests/test-app/src/pages/blog-posts/[slug].tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next'; 2 | import Link from 'next/link'; 3 | import { FC } from 'react'; 4 | 5 | type TBlogPostProps = { 6 | slug: string; 7 | }; 8 | 9 | const BlogPost: FC = ({ slug }) => { 10 | return ( 11 |
12 |

{slug}

13 |
14 | TO BLOG POSTS 15 |
16 |
17 | ); 18 | }; 19 | 20 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 21 | const slug = ctx.query.slug; 22 | 23 | return { props: { slug } }; 24 | }; 25 | 26 | export default BlogPost; 27 | -------------------------------------------------------------------------------- /tests/test-app/src/pages/blog-posts/index.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next'; 2 | import Link from 'next/link'; 3 | import { FC } from 'react'; 4 | 5 | type TBlogPostsProps = { 6 | title: string; 7 | }; 8 | 9 | const BlogPosts: FC = ({ title }) => { 10 | return ( 11 |
12 |

{title}

13 |
14 | TO BLOG POST "some-post" 15 |
16 |
17 | ); 18 | }; 19 | 20 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 21 | // GSSP will get query from controller only on first load (not on navigation) 22 | return { props: { title: ctx.query.title || 'FALLBACK BLOG POSTS' } }; 23 | }; 24 | 25 | export default BlogPosts; 26 | -------------------------------------------------------------------------------- /tests/test-app/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { GetStaticProps } from 'next'; 2 | import Link from 'next/link'; 3 | import { FC } from 'react'; 4 | 5 | type THomeProps = { title: string }; 6 | 7 | const Home: FC = ({ title }) => { 8 | return ( 9 |
10 |

{title}

11 |
12 | TO 1 13 |
14 |
15 | ); 16 | }; 17 | 18 | export const getStaticProps: GetStaticProps = async () => { 19 | return { props: { title: 'HOME' } }; 20 | }; 21 | 22 | export default Home; 23 | -------------------------------------------------------------------------------- /tests/test-app/src/server/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Param, 5 | ParseIntPipe, 6 | Render, 7 | UseInterceptors, 8 | } from '@nestjs/common'; 9 | import { ParamsInterceptor } from './params.interceptor'; 10 | 11 | @Controller() 12 | export class AppController { 13 | constructor() {} 14 | 15 | @Get('/') 16 | @Render('index') 17 | @UseInterceptors(ParamsInterceptor) 18 | home() { 19 | return {}; 20 | } 21 | 22 | @Get('/about/**') 23 | @Render('about/[...all]') 24 | @UseInterceptors(ParamsInterceptor) 25 | aboutAll() { 26 | return {}; 27 | } 28 | 29 | @Get('/blog-posts') 30 | @Render('blog-posts') 31 | @UseInterceptors(ParamsInterceptor) 32 | blogPosts() { 33 | return { title: 'BLOG POSTS' }; 34 | } 35 | 36 | @Get('/blog-posts/:slug') 37 | @Render('blog-posts/[slug]') 38 | @UseInterceptors(ParamsInterceptor) 39 | public blogPost() { 40 | return {}; 41 | } 42 | 43 | @Get('/:id') 44 | @Render('[id]') 45 | @UseInterceptors(ParamsInterceptor) 46 | public post() { 47 | return {}; 48 | } 49 | 50 | @Get('/api/health/ping') 51 | public ping() { 52 | return 'pong'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/test-app/src/server/app.module.ts: -------------------------------------------------------------------------------- 1 | import Next from 'next'; 2 | 3 | import { Module } from '@nestjs/common'; 4 | 5 | // @ts-expect-error 6 | import { RenderModule } from '../nest-next-dist/render.module'; 7 | 8 | import { AppController } from './app.controller'; 9 | 10 | @Module({ 11 | imports: [ 12 | RenderModule.forRootAsync( 13 | Next({ dev: process.env.NODE_ENV === 'development' }), 14 | { 15 | viewsDir: null, 16 | }, 17 | ), 18 | ], 19 | controllers: [AppController], 20 | providers: [], 21 | }) 22 | export class AppModule {} 23 | -------------------------------------------------------------------------------- /tests/test-app/src/server/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(process.env.PORT || '3000'); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /tests/test-app/src/server/params.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NestInterceptor, 4 | ExecutionContext, 5 | CallHandler, 6 | } from '@nestjs/common'; 7 | import { Request } from 'express'; 8 | import { Observable } from 'rxjs'; 9 | import { map } from 'rxjs/operators'; 10 | 11 | @Injectable() 12 | export class ParamsInterceptor implements NestInterceptor { 13 | intercept(context: ExecutionContext, next: CallHandler): Observable { 14 | const request = context.switchToHttp().getRequest() as Request; 15 | 16 | return next.handle().pipe( 17 | map((data) => { 18 | return { 19 | ...request.query, 20 | ...request.params, 21 | ...data, 22 | }; 23 | }), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/test-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "lib": ["dom", "dom.iterable", "esnext"], 16 | "allowJs": true, 17 | "strict": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "noEmit": true, 20 | "esModuleInterop": true, 21 | "moduleResolution": "node", 22 | "resolveJsonModule": true, 23 | "isolatedModules": true, 24 | "jsx": "preserve" 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /tests/test-app/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false 5 | }, 6 | "include": [ 7 | "./src/server/**/*.ts", 8 | "./src/shared/**/*.ts", 9 | "./@types/**/*.d.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tests/wait-until: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # https://github.com/nickjj/wait-until 3 | 4 | command="${1}" 5 | timeout="${2:-30}" 6 | 7 | i=1 8 | until eval "${command}" 9 | do 10 | ((i++)) 11 | 12 | if [ "${i}" -gt "${timeout}" ]; then 13 | echo "command was never successful, aborting due to ${timeout}s timeout!" 14 | exit 1 15 | fi 16 | 17 | sleep 1 18 | done -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "strict": true, 5 | "pretty": true, 6 | "declaration": true, 7 | "noImplicitAny": true, 8 | "alwaysStrict": true, 9 | "removeComments": true, 10 | "noImplicitThis": false, 11 | 12 | "outDir": "./dist", 13 | "moduleResolution": "node", 14 | 15 | "allowSyntheticDefaultImports": true, 16 | "esModuleInterop": true, 17 | 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true, 20 | 21 | "noLib": false, 22 | "target": "es6", 23 | "skipLibCheck": true, 24 | 25 | "rootDir": "./lib", 26 | "typeRoots": ["node_modules/@types"] 27 | }, 28 | "include": ["lib/**/*"], 29 | "exclude": ["node_modules", "example"] 30 | } 31 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-config-prettier"], 3 | "rules": { 4 | "no-implicit-dependencies": { 5 | "options": [ 6 | "dev" 7 | ] 8 | }, 9 | "interface-name": false, 10 | "only-arrow-functions": false, 11 | "no-submodule-imports": false 12 | } 13 | } 14 | --------------------------------------------------------------------------------