├── .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 |
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 | [](./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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------