├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── nodejs.yml │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg ├── pre-commit └── pre-push ├── .lintstagedrc ├── .npmignore ├── .npmrc ├── .releaserc ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── example ├── basic │ ├── app.controller.ts │ ├── app.module.ts │ ├── main.ts │ └── views │ │ ├── layouts │ │ └── main.tsx │ │ ├── my-context.tsx │ │ └── my-view.tsx ├── graphql │ ├── app.controller.ts │ ├── app.module.ts │ ├── main.ts │ └── views │ │ ├── layouts │ │ └── main.tsx │ │ └── my-view.tsx └── multiple-modules │ ├── app.module.ts │ ├── feature1 │ ├── feature1.controller.ts │ ├── feature1.module.ts │ └── views │ │ └── my-view.tsx │ ├── feature2 │ ├── feature2.controller.ts │ ├── feature2.module.ts │ └── views │ │ └── my-view.tsx │ └── main.ts ├── index.ts ├── jest.config.js ├── package.json ├── renovate.json ├── src ├── index.ts ├── tsx-views.constants.ts ├── tsx-views.interface.ts ├── tsx-views.middleware.ts ├── tsx-views.middlware.test.ts ├── tsx-views.module.ts └── tsx-views.service.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | *.js 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@heise'], 3 | rules: { 4 | 'node/no-missing-import': 'off', 5 | 'unicorn/import-style': 'off', 6 | 'unicorn/no-null': 'off', 7 | 'unicorn/prevent-abbreviations': 'off', 8 | }, 9 | env: { 10 | node: true, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | env: 5 | CI: true 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Use Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: "16.x" 18 | 19 | - name: Install dependencies 20 | run: yarn --frozen-lockfile 21 | 22 | - name: Run lint 23 | run: yarn lint 24 | 25 | - name: Run tests 26 | run: yarn test --coverage 27 | 28 | - name: Run build 29 | run: yarn build 30 | 31 | - name: Coveralls 32 | uses: coverallsapp/github-action@master 33 | with: 34 | github-token: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_run: 8 | workflows: ["Tests"] 9 | types: [completed] 10 | 11 | jobs: 12 | release: 13 | name: Release 14 | runs-on: ubuntu-18.04 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 16 22 | 23 | - name: Install dependencies 24 | run: yarn --frozen-lockfile 25 | 26 | - name: Build 27 | run: yarn build 28 | 29 | - name: Release 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 33 | run: yarn semantic-release 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname $0)/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname $0)/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname $0)/_/husky.sh" 3 | 4 | yarn lint 5 | yarn test 6 | yarn test:e2e 7 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,tsx}": [ 3 | "eslint --fix" 4 | ], 5 | "*.{html,json}": [ 6 | "prettier --write" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs 2 | __mocks__ 3 | __tests__ 4 | .* 5 | *.old 6 | *.log 7 | *.tgz 8 | **/*.test.js 9 | **/*.ts 10 | **/example 11 | coverage 12 | renovate.json 13 | *.config.js 14 | node_modules 15 | tsconfig*.json 16 | !dist/**/* 17 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["master"], 3 | "repositoryUrl": "git@github.com:pmb0/nestjs-tsx-views.git", 4 | "plugins": [ 5 | "@semantic-release/commit-analyzer", 6 | "@semantic-release/release-notes-generator", 7 | ["@semantic-release/changelog", { 8 | "changelogFile": "CHANGELOG.md" 9 | }], 10 | "@semantic-release/npm", 11 | ["@semantic-release/git", { 12 | "assets": ["package.json", "CHANGELOG.md"], 13 | "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}" 14 | }] 15 | ], 16 | "fail": false, 17 | "success": false 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "editorconfig.editorconfig", 5 | ], 6 | "unwantedRecommendations": [] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.organizeImports": true, 4 | "source.fixAll": true 5 | }, 6 | "editor.defaultFormatter": "esbenp.prettier-vscode", 7 | "editor.formatOnSave": true, 8 | "editor.formatOnSaveMode": "file", 9 | "editor.semanticHighlighting.enabled": true, 10 | "npm.packageManager": "yarn", 11 | "typescript.tsdk": "node_modules/typescript/lib", 12 | "typescript.referencesCodeLens.enabled": true, 13 | "typescript.implementationsCodeLens.enabled": true, 14 | "typescript.updateImportsOnFileMove.enabled": "always", 15 | "eslint.nodePath": "node_modules/eslint", 16 | "eslint.format.enable": true, 17 | "eslint.packageManager": "yarn", 18 | "eslint.lintTask.options": "--cache .", 19 | "[javascript]": { 20 | "editor.rulers": [80], 21 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 22 | }, 23 | "[javascriptreact]": { 24 | "editor.rulers": [80], 25 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 26 | }, 27 | "[typescript]": { 28 | "editor.rulers": [80], 29 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 30 | }, 31 | "[typescriptreact]": { 32 | "editor.rulers": [80], 33 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.2.6](https://github.com/pmb0/nestjs-tsx-views/compare/v1.2.5...v1.2.6) (2023-03-12) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * peer deps ([f365da6](https://github.com/pmb0/nestjs-tsx-views/commit/f365da6c76b750502ace11d60c8c7c4cfb6a47d9)) 7 | * upgrade deps ([90d0195](https://github.com/pmb0/nestjs-tsx-views/commit/90d01956519ceb85e0a1c8bb3588f91471318872)) 8 | 9 | ## [1.2.5](https://github.com/pmb0/nestjs-tsx-views/compare/v1.2.4...v1.2.5) (2021-08-20) 10 | 11 | 12 | ### Bug Fixes 13 | 14 | * upgrade deps ([9fee742](https://github.com/pmb0/nestjs-tsx-views/commit/9fee74245758919446ab800d4bdf8d6b0964257b)) 15 | * **deps:** support NestJS ^8 ([279f74e](https://github.com/pmb0/nestjs-tsx-views/commit/279f74e432da4988f067a6541d75f58f85fd2e28)) 16 | 17 | ## [1.2.4](https://github.com/pmb0/nestjs-tsx-views/compare/v1.2.3...v1.2.4) (2021-08-03) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * upgrade deps ([1bffc23](https://github.com/pmb0/nestjs-tsx-views/commit/1bffc233b8658ab36f41f97d0746d628c1aea480)) 23 | 24 | ## [1.2.3](https://github.com/pmb0/nestjs-tsx-views/compare/v1.2.2...v1.2.3) (2021-08-02) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * export * from express-tsx-views ([1b8bb4a](https://github.com/pmb0/nestjs-tsx-views/commit/1b8bb4af1666ee42e18ef6307e7b73b9e3f4e3a3)) 30 | 31 | ## [1.2.2](https://github.com/pmb0/nestjs-tsx-views/compare/v1.2.1...v1.2.2) (2021-07-31) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * upgrade deps ([1b12157](https://github.com/pmb0/nestjs-tsx-views/commit/1b121579de075ada4129249ce5ddaedac19c2b14)) 37 | 38 | ## [1.2.1](https://github.com/pmb0/nestjs-tsx-views/compare/v1.2.0...v1.2.1) (2021-07-31) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * add missing export ([2816cec](https://github.com/pmb0/nestjs-tsx-views/commit/2816cec7eaa70ddef252a3cb9fafaf70c3fb9f85)) 44 | * upgrade deps ([e722496](https://github.com/pmb0/nestjs-tsx-views/commit/e722496f23c6b5a9af059d6a611bf61222d69e67)) 45 | 46 | # [1.2.0](https://github.com/pmb0/nestjs-tsx-views/compare/v1.1.0...v1.2.0) (2021-07-31) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * upgrade deps ([0b7ad5c](https://github.com/pmb0/nestjs-tsx-views/commit/0b7ad5ce6ca60d02e91c5e716c3cf735096d1e57)) 52 | 53 | 54 | ### Features 55 | 56 | * support React contexts ([84c239f](https://github.com/pmb0/nestjs-tsx-views/commit/84c239f0e6c4a50dc4742cb57791e1c9083836c6)) 57 | 58 | # [1.1.0](https://github.com/pmb0/nestjs-tsx-views/compare/v1.0.7...v1.1.0) (2021-06-20) 59 | 60 | 61 | ### Features 62 | 63 | * support GraphQL queries ([fe7077f](https://github.com/pmb0/nestjs-tsx-views/commit/fe7077ff1bfddb2f9ada6bdf701eea336b40d306)) 64 | 65 | ## [1.0.7](https://github.com/pmb0/nestjs-tsx-views/compare/v1.0.6...v1.0.7) (2021-05-24) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * add missing peerDependency `react@^17` ([4fa5680](https://github.com/pmb0/nestjs-tsx-views/commit/4fa5680685545e5f0da411557500a67f61fb5547)) 71 | * cleanup dependencies ([7c79d11](https://github.com/pmb0/nestjs-tsx-views/commit/7c79d112b35d03fd87f4e2ed909b9f60a6a1276f)) 72 | 73 | ## [1.0.6](https://github.com/pmb0/nestjs-tsx-views/compare/v1.0.5...v1.0.6) (2021-05-22) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * **deps:** update dependency @types/react to v17.0.6 ([b6e638e](https://github.com/pmb0/nestjs-tsx-views/commit/b6e638e5bf687c9c1c123c472c73d55b0f05697c)) 79 | 80 | ## [1.0.5](https://github.com/pmb0/nestjs-tsx-views/compare/v1.0.4...v1.0.5) (2021-05-05) 81 | 82 | 83 | ### Bug Fixes 84 | 85 | * **deps:** update dependency @types/react to v17.0.5 ([a29406b](https://github.com/pmb0/nestjs-tsx-views/commit/a29406bfcb83314c628c2f77f918bd9ee68d40b4)) 86 | 87 | ## [1.0.4](https://github.com/pmb0/nestjs-tsx-views/compare/v1.0.3...v1.0.4) (2021-05-03) 88 | 89 | 90 | ### Bug Fixes 91 | 92 | * **deps:** update dependency @types/react to v17.0.4 ([67c2ad5](https://github.com/pmb0/nestjs-tsx-views/commit/67c2ad50bb59f904207a50cec67753090a3f2d97)) 93 | 94 | ## [1.0.3](https://github.com/pmb0/nestjs-tsx-views/compare/v1.0.2...v1.0.3) (2021-03-07) 95 | 96 | 97 | ### Bug Fixes 98 | 99 | * **deps:** update dependency @types/react to v17.0.3 ([439f3c6](https://github.com/pmb0/nestjs-tsx-views/commit/439f3c6fea81177dd5afd3385290c12de48d7ec4)) 100 | 101 | ## [1.0.2](https://github.com/pmb0/nestjs-tsx-views/compare/v1.0.1...v1.0.2) (2021-02-12) 102 | 103 | 104 | ### Bug Fixes 105 | 106 | * **deps:** update dependency @types/react to v17.0.2 ([32dc2a7](https://github.com/pmb0/nestjs-tsx-views/commit/32dc2a761bdf25eed45872919e441fc01d11852a)) 107 | 108 | ## [1.0.1](https://github.com/pmb0/nestjs-tsx-views/compare/v1.0.0...v1.0.1) (2021-02-03) 109 | 110 | 111 | ### Bug Fixes 112 | 113 | * **deps:** update dependency @types/react to v17.0.1 ([d2c4d48](https://github.com/pmb0/nestjs-tsx-views/commit/d2c4d4874e2e06898c407d6e40077f50a687da8a)) 114 | 115 | # 1.0.0 (2020-12-13) 116 | 117 | 118 | ### Bug Fixes 119 | 120 | * upgrade deps ([1afde02](https://github.com/pmb0/nestjs-tsx-views/commit/1afde02f8492b191f6644af504d0ae9e542d0734)) 121 | 122 | 123 | ### Features 124 | 125 | * add synchronous `register` method ([b0d99b8](https://github.com/pmb0/nestjs-tsx-views/commit/b0d99b89bef248b0f107c8292707903878eeeff3)) 126 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 pmb0 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 |

nestjs-tsx-views

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | React SSR module for NestJS MVC 14 |

15 |
16 | 17 | # Example 18 | 19 | Controller: 20 | 21 | ```ts 22 | import { Controller, Get, Render } from "@nestjs/common"; 23 | import { MyViewProps } from "./views/my-view"; 24 | 25 | @Controller() 26 | export class AppController { 27 | @Get() 28 | @Render("my-view") 29 | index(): MyViewProps { 30 | return { name: "world" }; 31 | } 32 | } 33 | ``` 34 | 35 | `views/my-view.tsx`: 36 | 37 | ```tsx 38 | import React, { ReactElement } from "react"; 39 | import { MainLayout } from "./layouts/main"; 40 | 41 | export interface MyViewProps { 42 | name: string; 43 | title: string; 44 | } 45 | 46 | const MyView = ({ name, ...props }: MyViewProps): ReactElement => ( 47 |
Hello {name}
48 | ); 49 | 50 | export default MyView; 51 | ``` 52 | 53 | # Highlights 54 | 55 | - Fast, since the JSX/TSX files do not have to be transpiled on-the-fly with every request 56 | - Separate NestJS modules can use their own views directories (see [multi module example](https://github.com/pmb0/nestjs-tsx-views/blob/master/example/multiple-modules)) 57 | - Works with compiled files (`.js` / `node`) and uncompiled files (`.tsx` / `ts-node`, `ts-jest`, ...) 58 | - Provides React contexts 59 | - Supports execution of GraphQL queries from JSX components 60 | 61 | # Table of contents 62 | 63 | - [Example](#example) 64 | - [Usage](#usage) 65 | - [Synchronous configuration](#synchronous-configuration) 66 | - [Asynchronous configuration](#asynchronous-configuration) 67 | - [React Context](#react-context) 68 | - [GraphQL](#graphql) 69 | - [Configuration](#configuration) 70 | - [License](#license) 71 | 72 | # Usage 73 | 74 | ```sh 75 | $ npm install --save nestjs-tsx-views 76 | ``` 77 | 78 | Import the module with `TsxViewsModule.register(...)` or `TsxViewsModule.registerAsync(...)`. 79 | 80 | ## Synchronous configuration 81 | 82 | Use `TsxViewsModule.register()`. Available options are described in the [TsxViewsModuleOptions interface](#configuration). 83 | 84 | ```ts 85 | @Module({ 86 | imports: [ 87 | TsxViewsModule.register({ 88 | viewsDirectory: resolve(__dirname, "./views"), 89 | prettify: true, 90 | forRoutes: [AppController], 91 | }), 92 | ], 93 | }) 94 | export class MyModule {} 95 | ``` 96 | 97 | ## Asynchronous configuration 98 | 99 | If you want to use retrieve you [TSX views options](#configuration) dynamically, use `TsxViewsModule.registerAsync()`. Use `useFactory` and `inject` to import your dependencies. Example using the `ConfigService`: 100 | 101 | ```ts 102 | @Module({ 103 | imports: [ 104 | TsxViewsModule.registerAsync({ 105 | useFactory: (config: ConfigService) => ({ 106 | viewsDirectory: resolve(__dirname, './views'), 107 | prettify: config.get('PRETTIFY_HTML' 108 | ) 109 | forRoutes: [AppController], 110 | }), 111 | inject: [ConfigService], 112 | }), 113 | ], 114 | }) 115 | export class MyModule {} 116 | ``` 117 | 118 | ## React Context 119 | 120 | 1. Define a React context: 121 | 122 | ```tsx 123 | import { createContext } from 'react' 124 | 125 | export interface MyContextProps { 126 | name: string 127 | } 128 | 129 | export const MyContext = createContext 130 | ``` 131 | 132 | 2. Set the context in your controller (or provider): 133 | 134 | ```ts 135 | @Controller() 136 | export class AppController { 137 | constructor(private readonly ssr: TsxViewsService) {} 138 | 139 | @Get() 140 | @Render("my-view") 141 | index() { 142 | this.#ssr.addContext(MyContext, { name: "My context data" }); 143 | 144 | return {}; 145 | } 146 | } 147 | ``` 148 | 149 | 3. Use it somewhere in your component: 150 | 151 | ```tsx 152 | import { useContext } from "react"; 153 | import { MyContext } from "./my-context"; 154 | 155 | export function MyComponent() { 156 | const { name } = useContext(MyContext); 157 | return Hallo, {name}!; 158 | } 159 | ``` 160 | 161 | ## GraphQL 162 | 163 | This module supports the execution of GraphQL queries from the TSX template. For this purpose `graphql`, `@apollo/client` and `cross-fetch` have to be installed separately: 164 | 165 | ```sh 166 | $ npm install --save @apollo/client cross-fetch graphql 167 | ``` 168 | 169 | See `example/graphql/app.module.ts` for a working example of how to configure the NestJS module. View example: 170 | 171 | ```ts 172 | // example/graphql/views/my-view.tsx 173 | 174 | export interface Film { 175 | id: string; 176 | title: string; 177 | releaseDate: string; 178 | } 179 | 180 | export interface AllFilms { 181 | allFilms: { 182 | films: Film[]; 183 | }; 184 | } 185 | 186 | const MY_QUERY = gql` 187 | query AllFilms { 188 | allFilms { 189 | films { 190 | id 191 | title 192 | releaseDate 193 | } 194 | } 195 | } 196 | `; 197 | 198 | export interface MyViewProps { 199 | name: string; 200 | title: string; 201 | } 202 | 203 | const MyView = (props: MyViewProps): ReactElement => { 204 | const { data, error } = useQuery(MY_QUERY); 205 | 206 | if (error) { 207 | throw error; 208 | } 209 | 210 | return ( 211 | 212 |

Films:

213 | {data?.allFilms.films.map((film) => ( 214 |
    215 | {film.title} ({new Date(film.releaseDate).getFullYear()}) 216 |
217 | ))} 218 |
219 | ); 220 | }; 221 | 222 | export default MyView; 223 | ``` 224 | 225 | ## Configuration 226 | 227 | nestjs-tsx-views can be configured with the following options: 228 | 229 | ```ts 230 | export interface TsxViewsModuleOptions extends ReactViewsOptions { 231 | /** 232 | * The directory where your views (`.tsx` files) are stored. Must be 233 | * specified. 234 | */ 235 | viewsDirectory: string; 236 | 237 | /** 238 | * [Doctype](https://developer.mozilla.org/en-US/docs/Glossary/Doctype) to 239 | * be used. */ 240 | doctype?: string; 241 | 242 | /** 243 | * If activated, the generated HTML string is formatted using 244 | * [prettier](https://github.com/prettier/prettier) 245 | */ 246 | prettify?: boolean; 247 | 248 | /** 249 | * With this optional function the rendered HTML document can be modified. For 250 | * this purpose a function must be defined which gets the HTML `string` as 251 | * argument. The function returns a modified version of the HTML string as 252 | * `string`. 253 | */ 254 | transform?: (html: string) => string | Promise; 255 | 256 | /** 257 | * Excludes routes from the currently processed middleware. 258 | * 259 | * @param {(string | RouteInfo)[]} routes 260 | * @returns {MiddlewareConfigProxy} 261 | */ 262 | exclude?: (string | RouteInfo)[]; 263 | 264 | /** 265 | * Attaches passed either routes or controllers to the currently configured middleware. 266 | * If you pass a class, Nest would attach middleware to every path defined within this controller. 267 | * 268 | * @param {(string | Type | RouteInfo)[]} routes 269 | * @returns {MiddlewareConsumer} 270 | */ 271 | forRoutes?: (string | Type | RouteInfo)[]; 272 | } 273 | ``` 274 | 275 | # License 276 | 277 | nestjs-tsx-views is distributed under the MIT license. [See LICENSE](./LICENSE) for details. 278 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /example/basic/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Render } from '@nestjs/common' 2 | import { TsxViewsService } from '../../src/tsx-views.service' 3 | import { MyContext } from './views/my-context' 4 | import { MyViewProps } from './views/my-view' 5 | 6 | @Controller() 7 | export class AppController { 8 | #ssr: TsxViewsService 9 | 10 | constructor(ssr: TsxViewsService) { 11 | this.#ssr = ssr 12 | } 13 | 14 | @Get() 15 | @Render('my-view') 16 | index(): MyViewProps { 17 | this.#ssr.addContext(MyContext, { name: 'My context data' }) 18 | 19 | return { title: 'my title', name: 'world' } 20 | } 21 | 22 | /** 23 | * This route is excluded in AppModule setup 24 | */ 25 | @Get('/throws-exception') 26 | @Render('my-view') 27 | throwException(): MyViewProps { 28 | return { title: 'my title', name: 'world' } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/basic/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { resolve } from 'path' 3 | import { TsxViewsModule } from '../../src' 4 | import { AppController } from './app.controller' 5 | 6 | @Module({ 7 | imports: [ 8 | TsxViewsModule.registerAsync({ 9 | useFactory: () => ({ 10 | viewsDirectory: resolve(__dirname, './views'), 11 | prettify: true, 12 | exclude: ['/throws-exception'], 13 | forRoutes: [AppController], 14 | }), 15 | }), 16 | ], 17 | controllers: [AppController], 18 | }) 19 | export class AppModule {} 20 | -------------------------------------------------------------------------------- /example/basic/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core' 2 | import { AppModule } from './app.module' 3 | 4 | const DEFAULT_PORT = 3000 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule) 8 | await app.listenAsync(process.env.PORT || DEFAULT_PORT) 9 | } 10 | 11 | void bootstrap() 12 | -------------------------------------------------------------------------------- /example/basic/views/layouts/main.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, ReactNode } from 'react' 2 | 3 | export interface MainLayoutProps { 4 | title: string 5 | children: ReactNode 6 | } 7 | 8 | export const MainLayout = ({ 9 | title, 10 | children, 11 | }: MainLayoutProps): ReactElement => ( 12 | 13 | 14 | {title} 15 | 16 | 17 |

{title}

18 | {children} 19 | 20 | 21 | ) 22 | -------------------------------------------------------------------------------- /example/basic/views/my-context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | export interface MyContextProps { 4 | name: string 5 | } 6 | 7 | export const MyContext = createContext(undefined) 8 | -------------------------------------------------------------------------------- /example/basic/views/my-view.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useContext } from 'react' 2 | import { MainLayout } from './layouts/main' 3 | import { MyContext } from './my-context' 4 | 5 | export interface MyViewProps { 6 | name: string 7 | title: string 8 | } 9 | 10 | const MyView = ({ name, ...props }: MyViewProps): ReactElement => { 11 | const myContext = useContext(MyContext) 12 | return ( 13 | 14 |
Hello {name}
15 |
Hello context: {myContext?.name}
16 |
17 | ) 18 | } 19 | 20 | export default MyView 21 | -------------------------------------------------------------------------------- /example/graphql/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Render } from '@nestjs/common' 2 | import { MyViewProps } from './views/my-view' 3 | 4 | @Controller() 5 | export class AppController { 6 | @Get() 7 | @Render('my-view') 8 | index(): MyViewProps { 9 | return { title: 'my title', name: 'world' } 10 | } 11 | 12 | /** 13 | * This route is excluded in AppModule setup 14 | */ 15 | @Get('/throws-exception') 16 | @Render('my-view') 17 | throwException(): MyViewProps { 18 | return { title: 'my title', name: 'world' } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/graphql/app.module.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client' 2 | import { Module } from '@nestjs/common' 3 | import { fetch } from 'cross-fetch' 4 | import { ApolloRenderMiddleware } from 'express-tsx-views/dist/apollo' 5 | import { resolve } from 'path' 6 | import { TsxViewsModule } from '../../src' 7 | import { AppController } from './app.controller' 8 | 9 | @Module({ 10 | imports: [ 11 | TsxViewsModule.registerAsync({ 12 | useFactory: (apollo: ApolloClient) => ({ 13 | viewsDirectory: resolve(__dirname, './views'), 14 | prettify: true, 15 | exclude: ['/throws-exception'], 16 | forRoutes: [AppController], 17 | middlewares: [new ApolloRenderMiddleware(apollo)], 18 | }), 19 | inject: [ApolloClient], 20 | extraProviders: [ 21 | { 22 | provide: ApolloClient, 23 | useValue: new ApolloClient({ 24 | ssrMode: true, 25 | link: createHttpLink({ 26 | uri: 'https://swapi-graphql.netlify.app/.netlify/functions/index', 27 | fetch, 28 | }), 29 | cache: new InMemoryCache(), 30 | }), 31 | }, 32 | ], 33 | }), 34 | ], 35 | controllers: [AppController], 36 | }) 37 | export class AppModule {} 38 | -------------------------------------------------------------------------------- /example/graphql/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core' 2 | import { AppModule } from './app.module' 3 | 4 | const DEFAULT_PORT = 3000 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule) 8 | await app.listenAsync(process.env.PORT || DEFAULT_PORT) 9 | } 10 | 11 | void bootstrap() 12 | -------------------------------------------------------------------------------- /example/graphql/views/layouts/main.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, ReactNode } from 'react' 2 | 3 | export interface MainLayoutProps { 4 | title: string 5 | children: ReactNode 6 | } 7 | 8 | export const MainLayout = ({ 9 | title, 10 | children, 11 | }: MainLayoutProps): ReactElement => ( 12 | 13 | 14 | {title} 15 | 16 | 17 |

{title}

18 | {children} 19 | 20 | 21 | ) 22 | -------------------------------------------------------------------------------- /example/graphql/views/my-view.tsx: -------------------------------------------------------------------------------- 1 | import { gql, useQuery } from '@apollo/client' 2 | import React, { ReactElement } from 'react' 3 | import { MainLayout } from './layouts/main' 4 | 5 | export interface Film { 6 | id: string 7 | title: string 8 | releaseDate: string 9 | } 10 | 11 | export interface AllFilms { 12 | allFilms: { 13 | films: Film[] 14 | } 15 | } 16 | 17 | const MY_QUERY = gql` 18 | query AllFilms { 19 | allFilms { 20 | films { 21 | id 22 | title 23 | releaseDate 24 | } 25 | } 26 | } 27 | ` 28 | 29 | export interface MyViewProps { 30 | name: string 31 | title: string 32 | } 33 | 34 | const MyView = (props: MyViewProps): ReactElement => { 35 | const { data, error } = useQuery(MY_QUERY) 36 | 37 | if (error) { 38 | throw error 39 | } 40 | 41 | return ( 42 | 43 |

Films:

44 | {data?.allFilms.films.map((film) => ( 45 |
    46 | {film.title} ({new Date(film.releaseDate).getFullYear()}) 47 |
48 | ))} 49 |
50 | ) 51 | } 52 | 53 | export default MyView 54 | -------------------------------------------------------------------------------- /example/multiple-modules/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { Feature1Module } from './feature1/feature1.module' 3 | import { Feature2Module } from './feature2/feature2.module' 4 | 5 | @Module({ 6 | imports: [Feature1Module, Feature2Module], 7 | }) 8 | export class AppModule {} 9 | -------------------------------------------------------------------------------- /example/multiple-modules/feature1/feature1.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Render } from '@nestjs/common' 2 | import { MyViewProps } from './views/my-view' 3 | 4 | @Controller('feature1') 5 | export class Feature1Controller { 6 | @Get() 7 | @Render('my-view') 8 | index(): MyViewProps { 9 | return { name: 'Feature 1' } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/multiple-modules/feature1/feature1.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { resolve } from 'path' 3 | import { TsxViewsModule } from '../../../src/tsx-views.module' 4 | import { Feature1Controller } from './feature1.controller' 5 | 6 | @Module({ 7 | imports: [ 8 | TsxViewsModule.register({ 9 | viewsDirectory: resolve(__dirname, './views'), 10 | prettify: true, 11 | 12 | forRoutes: [Feature1Controller], 13 | }), 14 | ], 15 | controllers: [Feature1Controller], 16 | }) 17 | export class Feature1Module {} 18 | -------------------------------------------------------------------------------- /example/multiple-modules/feature1/views/my-view.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react' 2 | 3 | export interface MyViewProps { 4 | name: string 5 | } 6 | 7 | const MyView = (): ReactElement => ( 8 |
9 |

Feature1

10 |

With prettified output

11 |
12 | ) 13 | 14 | export default MyView 15 | -------------------------------------------------------------------------------- /example/multiple-modules/feature2/feature2.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Render } from '@nestjs/common' 2 | import { MyViewProps } from './views/my-view' 3 | 4 | @Controller('feature2') 5 | export class Feature2Controller { 6 | @Get() 7 | @Render('my-view') 8 | index(): MyViewProps { 9 | return { name: 'Feature 2' } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/multiple-modules/feature2/feature2.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { resolve } from 'path' 3 | import { TsxViewsModule } from '../../../src/tsx-views.module' 4 | import { Feature2Controller } from './feature2.controller' 5 | 6 | @Module({ 7 | imports: [ 8 | TsxViewsModule.register({ 9 | viewsDirectory: resolve(__dirname, './views'), 10 | forRoutes: [Feature2Controller], 11 | }), 12 | ], 13 | controllers: [Feature2Controller], 14 | }) 15 | export class Feature2Module {} 16 | -------------------------------------------------------------------------------- /example/multiple-modules/feature2/views/my-view.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react' 2 | 3 | export interface MyViewProps { 4 | name: string 5 | } 6 | 7 | const MyView = (): ReactElement => ( 8 |
9 |

Feature2

10 |

With minified output

11 |
12 | ) 13 | 14 | export default MyView 15 | -------------------------------------------------------------------------------- /example/multiple-modules/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core' 2 | import { AppModule } from './app.module' 3 | 4 | const DEFAULT_PORT = 3000 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule) 8 | await app.listenAsync(process.env.PORT || DEFAULT_PORT) 9 | } 10 | 11 | void bootstrap() 12 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './src' 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable toplevel/no-toplevel-side-effect */ 2 | // For a detailed explanation regarding each configuration property, visit: 3 | // https://jestjs.io/docs/en/configuration.html 4 | 5 | module.exports = { 6 | // All imported modules in your tests should be mocked automatically 7 | // automock: false, 8 | 9 | // Stop running tests after `n` failures 10 | // bail: 0, 11 | 12 | // Respect "browser" field in package.json when resolving modules 13 | // browser: false, 14 | 15 | // The directory where Jest should store its cached dependency information 16 | // cacheDirectory: "/private/var/folders/3z/vt4dq0w132b_kg4gm2s7j33c0000gn/T/jest_dx", 17 | 18 | // Automatically clear mock calls and instances between every test 19 | clearMocks: true, 20 | 21 | // Indicates whether the coverage information should be collected while executing the test 22 | // collectCoverage: false, 23 | 24 | // An array of glob patterns indicating a set of files for which coverage information should be collected 25 | collectCoverageFrom: ['./src/**'], 26 | 27 | // The directory where Jest should output its coverage files 28 | coverageDirectory: 'coverage', 29 | 30 | // An array of regexp pattern strings used to skip coverage collection 31 | coveragePathIgnorePatterns: ['.dto.ts$'], 32 | 33 | // A list of reporter names that Jest uses when writing coverage reports 34 | coverageReporters: ['text', 'lcov'], 35 | 36 | // An object that configures minimum threshold enforcement for coverage results 37 | // coverageThreshold: undefined, 38 | 39 | // A path to a custom dependency extractor 40 | // dependencyExtractor: undefined, 41 | 42 | // Make calling deprecated APIs throw helpful error messages 43 | // errorOnDeprecated: false, 44 | 45 | // Force coverage collection from ignored files using an array of glob patterns 46 | // forceCoverageMatch: [], 47 | 48 | // A path to a module which exports an async function that is triggered once before all test suites 49 | // globalSetup: undefined, 50 | 51 | // A path to a module which exports an async function that is triggered once after all test suites 52 | // globalTeardown: undefined, 53 | 54 | // A set of global variables that need to be available in all test environments 55 | globals: { 56 | 'ts-jest': { 57 | tsconfig: 'tsconfig.json', 58 | }, 59 | }, 60 | 61 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 62 | // maxWorkers: "50%", 63 | 64 | // An array of directory names to be searched recursively up from the requiring module's location 65 | // moduleDirectories: [ 66 | // "node_modules" 67 | // ], 68 | 69 | // An array of file extensions your modules use 70 | moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'], 71 | 72 | // A map from regular expressions to module names that allow to stub out resources with a single module 73 | // moduleNameMapper: {}, 74 | 75 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 76 | // modulePathIgnorePatterns: [], 77 | 78 | // Activates notifications for test results 79 | // notify: false, 80 | 81 | // An enum that specifies notification mode. Requires { notify: true } 82 | // notifyMode: "failure-change", 83 | 84 | // A preset that is used as a base for Jest's configuration 85 | // preset: undefined, 86 | 87 | // Run tests from one or more projects 88 | // projects: undefined, 89 | 90 | // Use this configuration option to add custom reporters to Jest 91 | // reporters: undefined, 92 | 93 | // Automatically reset mock state between every test 94 | // resetMocks: false, 95 | 96 | // Reset the module registry before running each individual test 97 | // resetModules: false, 98 | 99 | // A path to a custom resolver 100 | // resolver: undefined, 101 | 102 | // Automatically restore mock state between every test 103 | // restoreMocks: false, 104 | 105 | // The root directory that Jest should scan for tests and modules within 106 | // rootDir: undefined, 107 | 108 | // A list of paths to directories that Jest should use to search for files in 109 | // roots: [ 110 | // "" 111 | // ], 112 | 113 | // Allows you to use a custom runner instead of Jest's default test runner 114 | // runner: "jest-runner", 115 | 116 | // The paths to modules that run some code to configure or set up the testing environment before each test 117 | // setupFiles: [], 118 | 119 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 120 | // setupFilesAfterEnv: [], 121 | 122 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 123 | // snapshotSerializers: [], 124 | 125 | // The test environment that will be used for testing 126 | testEnvironment: 'node', 127 | 128 | // Options that will be passed to the testEnvironment 129 | // testEnvironmentOptions: {}, 130 | 131 | // Adds a location field to test results 132 | // testLocationInResults: false, 133 | 134 | // The glob patterns Jest uses to detect test files 135 | // testMatch: [ 136 | // "**/__tests__/**/*.[jt]s?(x)", 137 | // "**/?(*.)+(spec|test).[tj]s?(x)" 138 | // ], 139 | 140 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 141 | testPathIgnorePatterns: ['/dist/'], 142 | 143 | // The regexp pattern or array of patterns that Jest uses to detect test files 144 | // testRegex: [], 145 | 146 | // This option allows the use of a custom results processor 147 | // testResultsProcessor: undefined, 148 | 149 | // This option allows use of a custom test runner 150 | // testRunner: "jasmine2", 151 | 152 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 153 | // testURL: "http://localhost", 154 | 155 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 156 | // timers: "real", 157 | 158 | // A map from regular expressions to paths to transformers 159 | transform: { 160 | '^.+\\.(ts|tsx)$': 'ts-jest', 161 | }, 162 | 163 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 164 | // transformIgnorePatterns: [ 165 | // "/node_modules/" 166 | // ], 167 | 168 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 169 | // unmockedModulePathPatterns: undefined, 170 | 171 | // Indicates whether each individual test should be reported during the run 172 | // verbose: undefined, 173 | 174 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 175 | // watchPathIgnorePatterns: [], 176 | 177 | // Whether to use watchman for file crawling 178 | // watchman: true, 179 | } 180 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-tsx-views", 3 | "version": "1.2.6", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "description": "Server-side JSX/TSX rendering for your NestJS application 🚀", 7 | "author": "Philipp Busse", 8 | "bugs": { 9 | "url": "https://github.com/pmb0/nestjs-tsx-views/issues" 10 | }, 11 | "homepage": "https://github.com/pmb0/nestjs-tsx-views#readme", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/pmb0/nestjs-tsx-views.git" 15 | }, 16 | "keywords": [ 17 | "nestjs", 18 | "react", 19 | "typescript", 20 | "express", 21 | "ssr", 22 | "template-engine" 23 | ], 24 | "devDependencies": { 25 | "@apollo/client": "^3.3.20", 26 | "@commitlint/cli": "^13.0.0", 27 | "@commitlint/config-conventional": "^13.0.0", 28 | "@heise/eslint-config": "^20.0.7", 29 | "@nestjs/common": "^9.3.9", 30 | "@nestjs/config": "^2.3.1", 31 | "@nestjs/core": "^9.3.9", 32 | "@nestjs/platform-express": "^9.3.9", 33 | "@nestjs/schedule": "^2.2.0", 34 | "@nestjs/schematics": "^9.0.4", 35 | "@nestjs/testing": "^9.3.9", 36 | "@semantic-release/changelog": "^6.0.0", 37 | "@semantic-release/commit-analyzer": "^9.0.1", 38 | "@semantic-release/git": "^10.0.0", 39 | "@semantic-release/npm": "^8.0.1", 40 | "@semantic-release/release-notes-generator": "^10.0.2", 41 | "@types/express": "^4.17.8", 42 | "@types/jest": "^27.0.0", 43 | "@types/node": "^16", 44 | "@types/react": "^18.0.28", 45 | "cross-fetch": "^3.1.4", 46 | "express-tsx-views": "^1.3.1", 47 | "graphql": "^15.5.1", 48 | "husky": "^7.0.0", 49 | "jest": "^27.0.0", 50 | "lint-staged": "^11.0.0", 51 | "pinst": "^2.1.1", 52 | "react": "^18.2.0", 53 | "react-dom": "^18.2.0", 54 | "reflect-metadata": "^0.1.13", 55 | "rxjs": "^7.0.0", 56 | "semantic-release": "^18.0.0", 57 | "ts-jest": "^27.0.0", 58 | "ts-node": "^10.1.0", 59 | "typescript": "^4.0.0" 60 | }, 61 | "peerDependencies": { 62 | "@nestjs/common": "*", 63 | "@nestjs/core": "*", 64 | "express": "^4.17.1", 65 | "express-tsx-views": ">= 1.3.1", 66 | "react": "*", 67 | "reflect-metadata": "*" 68 | }, 69 | "scripts": { 70 | "build:test": "tsc --noEmit", 71 | "build": "tsc --build tsconfig.build.json", 72 | "clean": "rimraf dist", 73 | "lint": "eslint --cache .", 74 | "prebuild": "yarn clean", 75 | "start:basic": "nodemon --exec ts-node --files example/basic/main.ts", 76 | "start:graphql": "nodemon --exec ts-node --files example/graphql/main.ts", 77 | "start:multiple-modules": "nodemon --exec ts-node --files example/multiple-modules/main.ts", 78 | "test": "jest", 79 | "_postinstall": "is-ci || husky install", 80 | "prepublish": "pinst --disable", 81 | "postpublish": "pinst --enable" 82 | }, 83 | "dependencies": {} 84 | } 85 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | ":separateMajorReleases", 5 | ":combinePatchMinorReleases", 6 | ":ignoreUnstable", 7 | ":prImmediately", 8 | ":renovatePrefix", 9 | ":semanticCommits", 10 | ":semanticPrefixFixDepsChoreOthers", 11 | ":updateNotScheduled", 12 | ":ignoreModulesAndTests", 13 | "group:monorepos", 14 | "group:recommended", 15 | "helpers:disableTypesNodeMajor" 16 | ], 17 | "rangeStrategy": "update-lockfile", 18 | "packageRules": [ 19 | { 20 | "depTypeList": ["devDependencies"], 21 | "extends": ["schedule:weekly"], 22 | "automerge": true 23 | } 24 | ], 25 | "automerge": true, 26 | "major": { 27 | "automerge": false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'express-tsx-views' 2 | export * from './tsx-views.constants' 3 | export * from './tsx-views.interface' 4 | export * from './tsx-views.module' 5 | export * from './tsx-views.service' 6 | -------------------------------------------------------------------------------- /src/tsx-views.constants.ts: -------------------------------------------------------------------------------- 1 | export const TSX_VIEWS_OPTIONS = 'TSX_VIEWS_OPTIONS' 2 | -------------------------------------------------------------------------------- /src/tsx-views.interface.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata, Provider, Type } from '@nestjs/common' 2 | import { Controller, RouteInfo } from '@nestjs/common/interfaces' 3 | import { ReactViewsOptions } from 'express-tsx-views' 4 | 5 | export interface TsxViewsModuleOptions extends ReactViewsOptions { 6 | /** 7 | * Excludes routes from the currently processed middleware. 8 | * 9 | * @param {(string | RouteInfo)[]} routes 10 | * @returns {MiddlewareConfigProxy} 11 | */ 12 | exclude?: (string | RouteInfo)[] 13 | 14 | /** 15 | * Attaches passed either routes or controllers to the currently configured middleware. 16 | * If you pass a class, Nest would attach middleware to every path defined within this controller. 17 | * 18 | * @param {(string | Type | RouteInfo)[]} routes 19 | * @returns {MiddlewareConsumer} 20 | */ 21 | forRoutes?: (string | Type | RouteInfo)[] 22 | } 23 | 24 | export interface TsxViewsModuleOptionsFactory { 25 | createTsxViewsOptions(): 26 | | Promise 27 | | TsxViewsModuleOptions 28 | } 29 | 30 | export interface TsxViewsModuleOptionsAsyncOptions 31 | extends Pick { 32 | extraProviders?: Provider[] 33 | inject?: any[] 34 | useExisting?: Type 35 | useClass?: Type 36 | useFactory?: ( 37 | ...args: any[] 38 | ) => Promise | TsxViewsModuleOptions 39 | } 40 | -------------------------------------------------------------------------------- /src/tsx-views.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, NestMiddleware } from '@nestjs/common' 2 | import { Request, Response } from 'express' 3 | import { setupReactViews } from 'express-tsx-views' 4 | import { TSX_VIEWS_OPTIONS } from './tsx-views.constants' 5 | import { TsxViewsModuleOptions } from './tsx-views.interface' 6 | 7 | @Injectable() 8 | export class TsxViewsMiddleware implements NestMiddleware { 9 | constructor( 10 | @Inject(TSX_VIEWS_OPTIONS) private readonly options: TsxViewsModuleOptions, 11 | ) {} 12 | 13 | use(req: Request, _res: Response, next: () => void): void { 14 | setupReactViews(req.app, this.options) 15 | 16 | next() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/tsx-views.middlware.test.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express' 2 | import { setupReactViews } from 'express-tsx-views' 3 | import { TsxViewsMiddleware } from './tsx-views.middleware' 4 | 5 | jest.mock('express-tsx-views', () => ({ setupReactViews: jest.fn() })) 6 | 7 | describe('TsxViewsMiddleware', () => { 8 | let tsxViewsMiddleware: TsxViewsMiddleware 9 | 10 | beforeEach(() => { 11 | tsxViewsMiddleware = new TsxViewsMiddleware({ viewsDirectory: '/tmp' }) 12 | }) 13 | 14 | test('use', () => { 15 | const next = jest.fn() 16 | const request = { app: {} } as Request 17 | const response = {} as Response 18 | 19 | tsxViewsMiddleware.use(request, response, next) 20 | expect(setupReactViews).toHaveBeenCalledWith({}, { viewsDirectory: '/tmp' }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/tsx-views.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DynamicModule, 3 | Inject, 4 | MiddlewareConsumer, 5 | Module, 6 | NestModule, 7 | Provider, 8 | } from '@nestjs/common' 9 | import { TSX_VIEWS_OPTIONS } from './tsx-views.constants' 10 | import { 11 | TsxViewsModuleOptions, 12 | TsxViewsModuleOptionsAsyncOptions, 13 | TsxViewsModuleOptionsFactory, 14 | } from './tsx-views.interface' 15 | import { TsxViewsMiddleware } from './tsx-views.middleware' 16 | import { TsxViewsService } from './tsx-views.service' 17 | 18 | @Module({}) 19 | export class TsxViewsModule implements NestModule { 20 | constructor( 21 | @Inject(TSX_VIEWS_OPTIONS) private readonly options: TsxViewsModuleOptions, 22 | ) {} 23 | 24 | configure(consumer: MiddlewareConsumer): void { 25 | consumer 26 | .apply(TsxViewsMiddleware) 27 | .exclude(...(this.options.exclude ?? [])) 28 | .forRoutes(...(this.options.forRoutes ?? '*')) 29 | } 30 | 31 | public static register(options: TsxViewsModuleOptions): DynamicModule { 32 | return { 33 | module: TsxViewsModule, 34 | providers: [ 35 | { 36 | provide: TSX_VIEWS_OPTIONS, 37 | useValue: options, 38 | }, 39 | ], 40 | } 41 | } 42 | 43 | public static registerAsync( 44 | options: TsxViewsModuleOptionsAsyncOptions, 45 | ): DynamicModule { 46 | const providers = this.createProviders(options) 47 | return { 48 | module: TsxViewsModule, 49 | imports: options.imports, 50 | providers: [ 51 | TsxViewsService, 52 | ...(options.extraProviders ?? []), 53 | ...providers, 54 | ], 55 | exports: [TsxViewsService, ...providers], 56 | } 57 | } 58 | 59 | static createProviders( 60 | options: TsxViewsModuleOptionsAsyncOptions, 61 | ): Provider[] { 62 | if (options.useExisting || options.useFactory) { 63 | return [this.createOptionsProvider(options)] 64 | } 65 | 66 | return [ 67 | this.createOptionsProvider(options), 68 | { 69 | provide: options.useClass!, 70 | useClass: options.useClass!, 71 | }, 72 | ] 73 | } 74 | 75 | private static createOptionsProvider( 76 | options: TsxViewsModuleOptionsAsyncOptions, 77 | ): Provider { 78 | if (options.useFactory) { 79 | return { 80 | provide: TSX_VIEWS_OPTIONS, 81 | useFactory: options.useFactory, 82 | inject: options.inject, 83 | } 84 | } 85 | 86 | return { 87 | provide: TSX_VIEWS_OPTIONS, 88 | useFactory: async (optionsFactory: TsxViewsModuleOptionsFactory) => 89 | await optionsFactory.createTsxViewsOptions(), 90 | inject: [options.useExisting || options.useClass!], 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/tsx-views.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Scope } from '@nestjs/common' 2 | import { REQUEST } from '@nestjs/core' 3 | import { Request, Response } from 'express' 4 | import { addReactContext } from 'express-tsx-views' 5 | import { Context } from 'react' 6 | 7 | @Injectable({ scope: Scope.REQUEST }) 8 | export class TsxViewsService { 9 | #response: Response 10 | 11 | constructor(@Inject(REQUEST) request: Request) { 12 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 13 | this.#response = request.res! 14 | } 15 | 16 | addContext(context: Context, value: T): void { 17 | addReactContext(this.#response, context, value) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "node_modules", 5 | "__tests__", 6 | "example", 7 | "test", 8 | "dist", 9 | "**/*spec.ts", 10 | "**/*test.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "declaration": true, 5 | "emitDecoratorMetadata": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "incremental": true, 10 | "jsx": "react", 11 | "module": "commonjs", 12 | "outDir": "./dist", 13 | "sourceMap": true, 14 | "strict": true, 15 | "strictNullChecks": true, 16 | "strictPropertyInitialization": true, 17 | "target": "es2019" 18 | }, 19 | "include": [ 20 | "src/**/*", 21 | "__tests__/**/*", 22 | "example/**/*", 23 | "__mocks__/**/*", 24 | "index.ts" 25 | ], 26 | "exclude": ["node_modules", "dist"] 27 | } 28 | --------------------------------------------------------------------------------