├── .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 |
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 |
--------------------------------------------------------------------------------