├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
└── workflows
│ ├── nodejs.yml
│ └── release.yml
├── .gitignore
├── .husky
├── commit-msg
├── pre-commit
└── pre-push
├── .lintstagedrc
├── .npmignore
├── .npmrc
├── .releaserc
├── .vscode
├── extensions.json
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __tests__
├── __snapshots__
│ └── react-view-engine.test.ts.snap
└── react-view-engine.test.ts
├── commitlint.config.js
├── example
├── app.ts
├── bin
│ └── www.ts
├── my-context.ts
└── views
│ ├── dumper.component.tsx
│ ├── my-component.tsx
│ ├── my-gql-view.tsx
│ ├── my-layout.tsx
│ └── my-view.tsx
├── jest.config.js
├── nodemon.json
├── package.json
├── renovate.json
├── src
├── apollo.ts
├── handler
│ ├── index.ts
│ ├── middleware
│ │ ├── apollo-render.middleware.ts
│ │ ├── create-react-context.middleware.ts
│ │ ├── default-render.middleware.ts
│ │ ├── prettify-render.middleware.ts
│ │ └── tsx-render.middleware.ts
│ └── tsx-render-context.ts
├── index.ts
├── react-view-engine.interface.ts
├── react-view-engine.test.ts
└── react-view-engine.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 | // Fails for unknown reasons in CI (not locally)
10 | '@typescript-eslint/no-unsafe-call': 'off',
11 | },
12 | env: {
13 | node: true,
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.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@v2
16 | with:
17 | node-version: "14.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: Coveralls
29 | uses: coverallsapp/github-action@master
30 | with:
31 | github-token: ${{ secrets.GITHUB_TOKEN }}
32 |
--------------------------------------------------------------------------------
/.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@v2
20 | with:
21 | node-version: 14
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/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 test
5 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "*.{js,ts}": [
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/express-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": ["dbaeumer.vscode-eslint", "editorconfig.editorconfig"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/.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.4.14](https://github.com/pmb0/express-tsx-views/compare/v1.4.13...v1.4.14) (2023-03-12)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * upgrade deps ([e2de905](https://github.com/pmb0/express-tsx-views/commit/e2de905e7d1d976279dcd939c5535237dd655051))
7 |
8 | ## [1.4.13](https://github.com/pmb0/express-tsx-views/compare/v1.4.12...v1.4.13) (2023-02-08)
9 |
10 |
11 | ### Bug Fixes
12 |
13 | * **deps:** update dependency prettier to v2.8.4 ([894c6f9](https://github.com/pmb0/express-tsx-views/commit/894c6f9e353caa377b4fe809be3544dfdb317384))
14 |
15 | ## [1.4.12](https://github.com/pmb0/express-tsx-views/compare/v1.4.11...v1.4.12) (2023-01-14)
16 |
17 |
18 | ### Bug Fixes
19 |
20 | * **deps:** update dependency prettier to v2.8.3 ([dfb3644](https://github.com/pmb0/express-tsx-views/commit/dfb3644fef4039be7f50a747b617a71498ee72df))
21 |
22 | ## [1.4.11](https://github.com/pmb0/express-tsx-views/compare/v1.4.10...v1.4.11) (2023-01-07)
23 |
24 |
25 | ### Bug Fixes
26 |
27 | * **deps:** update dependency prettier to v2.8.2 ([d7a279d](https://github.com/pmb0/express-tsx-views/commit/d7a279d2916b78ea03cd1d0698219c75ac533571))
28 |
29 | ## [1.4.10](https://github.com/pmb0/express-tsx-views/compare/v1.4.9...v1.4.10) (2022-12-19)
30 |
31 |
32 | ### Bug Fixes
33 |
34 | * **deps:** update dependency prettier to v2.8.0 ([0cdd136](https://github.com/pmb0/express-tsx-views/commit/0cdd136f7433422c71e0be1133c9a3b17f74a7a1))
35 | * **deps:** update dependency prettier to v2.8.1 ([055482a](https://github.com/pmb0/express-tsx-views/commit/055482aaaff1dc6b4f96fdd746546766201b93d2))
36 |
37 | ## [1.4.9](https://github.com/pmb0/express-tsx-views/compare/v1.4.8...v1.4.9) (2022-06-16)
38 |
39 |
40 | ### Bug Fixes
41 |
42 | * **deps:** update dependency prettier to v2.7.1 ([83a5b3d](https://github.com/pmb0/express-tsx-views/commit/83a5b3df707bca8e9cecc91c3f315344559b2cbd))
43 |
44 | ## [1.4.8](https://github.com/pmb0/express-tsx-views/compare/v1.4.7...v1.4.8) (2022-06-14)
45 |
46 |
47 | ### Bug Fixes
48 |
49 | * **deps:** update dependency prettier to v2.7.0 ([d32c8a1](https://github.com/pmb0/express-tsx-views/commit/d32c8a14d856e2d211a20662c8340700029db141))
50 |
51 | ## [1.4.7](https://github.com/pmb0/express-tsx-views/compare/v1.4.6...v1.4.7) (2022-04-15)
52 |
53 |
54 | ### Bug Fixes
55 |
56 | * **deps:** update dependency prettier to v2.6.2 ([7b9e962](https://github.com/pmb0/express-tsx-views/commit/7b9e962ed86b9954ae7c554b9506b89840945ccf))
57 |
58 | ## [1.4.6](https://github.com/pmb0/express-tsx-views/compare/v1.4.5...v1.4.6) (2021-10-19)
59 |
60 |
61 | ### Bug Fixes
62 |
63 | * ugprade deps ([03a2cf9](https://github.com/pmb0/express-tsx-views/commit/03a2cf9b956ed65db1a8b3b4178c73dc2441ab7b))
64 |
65 | ## [1.4.5](https://github.com/pmb0/express-tsx-views/compare/v1.4.4...v1.4.5) (2021-09-15)
66 |
67 |
68 | ### Bug Fixes
69 |
70 | * upgrade deps ([26d7528](https://github.com/pmb0/express-tsx-views/commit/26d7528c3c306334b723efaa193810c5a7b1509e))
71 |
72 | ## [1.4.4](https://github.com/pmb0/express-tsx-views/compare/v1.4.3...v1.4.4) (2021-08-03)
73 |
74 |
75 | ### Bug Fixes
76 |
77 | * run middleware pipeline in correct order ([c64d2b1](https://github.com/pmb0/express-tsx-views/commit/c64d2b1373558f864538df02466aff641da2c362))
78 |
79 | ## [1.4.3](https://github.com/pmb0/express-tsx-views/compare/v1.4.2...v1.4.3) (2021-07-31)
80 |
81 |
82 | ### Bug Fixes
83 |
84 | * remove debug ([14bc3be](https://github.com/pmb0/express-tsx-views/commit/14bc3be6d079b66bc4567104f4985ff9d09f1a68))
85 |
86 | ## [1.4.2](https://github.com/pmb0/express-tsx-views/compare/v1.4.1...v1.4.2) (2021-07-31)
87 |
88 |
89 | ### Bug Fixes
90 |
91 | * do not require() optional dependencies ([fc25974](https://github.com/pmb0/express-tsx-views/commit/fc259743c84e5be34bf551a700becb6846f9570d))
92 |
93 | ## [1.4.1](https://github.com/pmb0/express-tsx-views/compare/v1.4.0...v1.4.1) (2021-07-31)
94 |
95 |
96 | ### Bug Fixes
97 |
98 | * fix runtime error ([0f4bc6c](https://github.com/pmb0/express-tsx-views/commit/0f4bc6ccf6e14be57c0c594130b165ac40cb92f2))
99 |
100 | # [1.4.0](https://github.com/pmb0/express-tsx-views/compare/v1.3.2...v1.4.0) (2021-07-31)
101 |
102 |
103 | ### Features
104 |
105 | * enable creation of React contexts ([a0b2393](https://github.com/pmb0/express-tsx-views/commit/a0b23932c12227f74e200d3abdf06bb86b792a6b))
106 |
107 | ## [1.3.2](https://github.com/pmb0/express-tsx-views/compare/v1.3.1...v1.3.2) (2021-07-25)
108 |
109 |
110 | ### Bug Fixes
111 |
112 | * upgrade deps ([7f32376](https://github.com/pmb0/express-tsx-views/commit/7f32376811dcce9d27084e5a810baa414a8524c5))
113 |
114 | ## [1.3.1](https://github.com/pmb0/express-tsx-views/compare/v1.3.0...v1.3.1) (2021-06-20)
115 |
116 |
117 | ### Bug Fixes
118 |
119 | * @apollo/client is an optional dep ([ccf72d0](https://github.com/pmb0/express-tsx-views/commit/ccf72d0df16e761c1a716891464414070ffddb52))
120 |
121 | # [1.3.0](https://github.com/pmb0/express-tsx-views/compare/v1.2.6...v1.3.0) (2021-06-20)
122 |
123 |
124 | ### Bug Fixes
125 |
126 | * upgrade deps ([d1f7ce2](https://github.com/pmb0/express-tsx-views/commit/d1f7ce25130233c57646324127e4993d6270bfb7))
127 |
128 |
129 | ### Features
130 |
131 | * support Apollo GraphQL queries ([e729508](https://github.com/pmb0/express-tsx-views/commit/e72950819be8a821fce32852dcf427a94a7eeb29))
132 |
133 | ## [1.2.6](https://github.com/pmb0/express-tsx-views/compare/v1.2.5...v1.2.6) (2020-12-13)
134 |
135 |
136 | ### Bug Fixes
137 |
138 | * add missing `ReactViewsOptions` docs ([20ea5c9](https://github.com/pmb0/express-tsx-views/commit/20ea5c9dd8bd2747890c38a8297d847866aa4590))
139 |
140 | ## [1.2.5](https://github.com/pmb0/express-tsx-views/compare/v1.2.4...v1.2.5) (2020-12-13)
141 |
142 |
143 | ### Bug Fixes
144 |
145 | * support `nodemon --exec ts-node` ([d277c0f](https://github.com/pmb0/express-tsx-views/commit/d277c0fb6cfa2d254eb639c18812afbb04614ad7))
146 |
147 | ## [1.2.4](https://github.com/pmb0/express-tsx-views/compare/v1.2.3...v1.2.4) (2020-11-24)
148 |
149 |
150 | ### Bug Fixes
151 |
152 | * upgrade deps ([419e02d](https://github.com/pmb0/express-tsx-views/commit/419e02d206a1a985bdc753cbfbca2bf4e5428f25))
153 |
154 | ## [1.2.3](https://github.com/pmb0/express-tsx-views/compare/v1.2.2...v1.2.3) (2020-10-27)
155 |
156 |
157 | ### Bug Fixes
158 |
159 | * support react@^17 ([79bc57d](https://github.com/pmb0/express-tsx-views/commit/79bc57d88861797cdc54be7da36dd045d41fec5c))
160 |
161 | ## [1.2.2](https://github.com/pmb0/express-tsx-views/compare/v1.2.1...v1.2.2) (2020-09-16)
162 |
163 |
164 | ### Bug Fixes
165 |
166 | * **deps:** update dependency prettier to v2.1.2 ([4436466](https://github.com/pmb0/express-tsx-views/commit/4436466534b9967ce36440a07b16072cfd2c4414))
167 |
168 | ## [1.2.1](https://github.com/pmb0/express-tsx-views/compare/v1.2.0...v1.2.1) (2020-09-13)
169 |
170 |
171 | ### Bug Fixes
172 |
173 | * **transform:** transform function may be async ([8fb4a9f](https://github.com/pmb0/express-tsx-views/commit/8fb4a9f1e3c2378d8510087d1ecd8297bad395a8))
174 |
175 | # [1.2.0](https://github.com/pmb0/express-tsx-views/compare/v1.1.3...v1.2.0) (2020-09-13)
176 |
177 |
178 | ### Bug Fixes
179 |
180 | * **deps:** update dependency prettier to v2.1.0 ([a28cd52](https://github.com/pmb0/express-tsx-views/commit/a28cd52689d453628798b7ca39e433b502231e87))
181 | * **deps:** update dependency prettier to v2.1.1 ([4b66aee](https://github.com/pmb0/express-tsx-views/commit/4b66aee3497528a9f3109ade14ea434dcdd3a9cd))
182 |
183 |
184 | ### Features
185 |
186 | * **transform:** add post-render html transform ([6cddbb2](https://github.com/pmb0/express-tsx-views/commit/6cddbb21ca9a9f2b843751e516d5b998baa85a64))
187 |
188 | ## [1.1.3](https://github.com/pmb0/express-tsx-views/compare/v1.1.2...v1.1.3) (2020-08-10)
189 |
190 |
191 | ### Bug Fixes
192 |
193 | * **build:** do not bundle test files ([29e12be](https://github.com/pmb0/express-tsx-views/commit/29e12bef2be46df441539da480087347bc7b07d3))
194 |
195 | ## [1.1.2](https://github.com/pmb0/express-tsx-views/compare/v1.1.1...v1.1.2) (2020-08-01)
196 |
197 |
198 | ### Bug Fixes
199 |
200 | * add missing package.json fields ([ec78e7c](https://github.com/pmb0/express-tsx-views/commit/ec78e7c3ffd03d8a7b90a2bbc3d2af05794552a1))
201 |
202 | ## [1.1.1](https://github.com/pmb0/express-tsx-views/compare/v1.1.0...v1.1.1) (2020-07-31)
203 |
204 |
205 | ### Bug Fixes
206 |
207 | * use strong types ([8c609df](https://github.com/pmb0/express-tsx-views/commit/8c609df29b112cb71bb19c8dcc6fe78ca141ab9c))
208 |
209 | # [1.1.0](https://github.com/pmb0/express-tsx-views/compare/v1.0.1...v1.1.0) (2020-07-30)
210 |
211 |
212 | ### Features
213 |
214 | * support NestJS express applications ([c9d5f70](https://github.com/pmb0/express-tsx-views/commit/c9d5f70e0fea8bb7e1538b1cd894fae31ecf0314))
215 |
216 | ## [1.0.1](https://github.com/pmb0/express-tsx-views/compare/v1.0.0...v1.0.1) (2020-07-30)
217 |
218 |
219 | ### Bug Fixes
220 |
221 | * add missing main/types properties ([d3ca322](https://github.com/pmb0/express-tsx-views/commit/d3ca322831211956bf78895024429a205a2663d5))
222 |
223 | # 1.0.0 (2020-07-30)
224 |
225 |
226 | ### Features
227 |
228 | * initial version ([130d220](https://github.com/pmb0/express-tsx-views/commit/130d220511427d7bbda908cc7a68b9ff154d0186))
229 |
--------------------------------------------------------------------------------
/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 |
Server-side JSX/TSX rendering for your express or NestJS application
3 |
4 |
5 | [](https://www.npmjs.com/package/express-tsx-views)
6 | [![Test Coverage][coveralls-image]][coveralls-url]
7 | [![Build Status][build-image]][build-url]
8 |
9 | # Description
10 |
11 | With this template engine, TSX files can be rendered server-side by your Express application. Unlike other JSX express renderers, this one does not rely on JSX files being transpiled by `babel` at runtime. Instead, TSX files are processed once by the `tsc` compiler.
12 |
13 | For this to work, the templates are imported dynamically during rendering. And for this **you have to provide a default export in your main TSX files**. (Embeddable TSX components don't have to use a default export).
14 |
15 | # Highlights
16 |
17 | - Fast, since the JSX/TSX files do not have to be transpiled on-the-fly with every request
18 | - Works with compiled files (`.js` / `node`) and uncompiled files (`.tsx` / `ts-node`, `ts-jest`, ...)
19 | - Provides the definition of React contexts on middleware level
20 | - Supports execution of GraphQL queries from JSX components
21 |
22 | # Table of contents
23 |
24 | - [Usage](#usage)
25 | - [Express](#express)
26 | - [NestJS](#nestjs)
27 | - [Render Middlewares](#render-middlewares)
28 | - [Prettify](#prettify)
29 | - [Provide React Context](#provide-react-context)
30 | - [GraphQL](#graphql)
31 | - [License](#license)
32 |
33 | # Usage
34 |
35 | ```sh
36 | $ npm install --save express-tsx-views
37 | ```
38 |
39 | You have to set the `jsx` setting in your TypeScript configuration `tsconfig.json` to the value `react` and to enable `esModuleInterop`:
40 |
41 | ```json
42 | {
43 | "compilerOptions": {
44 | "jsx": "react",
45 | "esModuleInterop": true
46 | }
47 | }
48 | ```
49 |
50 | This template engine can be used in express and NestJS applications. The function `setupReactViews()` is provided, with which the engine is made available to the application.
51 |
52 | ```ts
53 | import { setupReactViews } from "express-tsx-views";
54 |
55 | const options = {
56 | viewsDirectory: path.resolve(__dirname, "../views"),
57 | };
58 |
59 | setupReactViews(app, options);
60 | ```
61 |
62 | The following options may be passed:
63 |
64 | | Option | Type | Description | Default |
65 | | ---------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
66 | | `viewsDirectory` | `string` | The directory where your views (`.tsx` files) are stored. Must be specified. | - |
67 | | `doctype` | `string` | [Doctype](https://developer.mozilla.org/en-US/docs/Glossary/Doctype) to be used. | `\n` |
68 | | `transform` | `(html: string) => string` | With this optional function the rendered HTML document can be modified. For this purpose a function must be defined which gets the HTML `string` as argument. The function returns a modified version of the HTML string as `string`. | - |
69 | | `middlewares` | `TsxRenderMiddleware[]` | A list of `TsxRenderMiddleware` objects that can be used to modify the render context. See [Render middlewares](#render-middlewares) | - |
70 |
71 | ## Express
72 |
73 | Example express app (See also `example/app.ts` in this project):
74 |
75 | ```js
76 | import express from "express";
77 | import { resolve } from "path";
78 | import { setupReactViews } from "express-tsx-views";
79 | import { Props } from "./views/my-view";
80 |
81 | export const app = express();
82 |
83 | setupReactViews(app, {
84 | viewsDirectory: resolve(__dirname, "views"),
85 | prettify: true, // Prettify HTML output
86 | });
87 |
88 | app.get("/my-route", (req, res, next) => {
89 | const data: Props = { title: "Test", lang: "de" };
90 | res.render("my-view", data);
91 | });
92 |
93 | app.listen(8080);
94 | ```
95 |
96 | `views/my-view.tsx`:
97 |
98 | ```tsx
99 | import React, { Component } from "react";
100 | import MyComponent from "./my-component";
101 | import { MyLayout } from "./my-layout";
102 |
103 | export interface Props {
104 | title: string;
105 | lang: string;
106 | }
107 |
108 | // Important -- use the `default` export
109 | export default class MyView extends Component {
110 | render() {
111 | return Hello from React! Title: {this.props.title}
;
112 | }
113 | }
114 | ```
115 |
116 | ## NestJS
117 |
118 | See [nestjs-tsx-views](https://github.com/pmb0/nestjs-tsx-views).
119 |
120 | express-tsx-views can also be used in [NestJS](https://nestjs.com/). For this purpose the template engine must be made available in your `main.ts`:
121 |
122 | # Render Middlewares
123 |
124 | ## Prettify
125 |
126 | Prettifies generated HTML markup using [prettier](https://github.com/prettier/prettier).
127 |
128 | ```ts
129 | setupReactViews(app, {
130 | middlewares: [new PrettifyRenderMiddleware()],
131 | });
132 | ```
133 |
134 | ## Provide React Context
135 |
136 | Provides a react context when rendering your react view.
137 |
138 | ```ts
139 | // my-context.ts
140 | import {createContext} from 'react'
141 |
142 | export interface MyContextProps = {name: string}
143 |
144 | export const MyContext = createContext(undefined)
145 | ```
146 |
147 | Use `addReactContext()` to set the context in your route or in any other middleware:
148 |
149 | ```ts
150 | // app.ts
151 |
152 | // Route:
153 | app.get("/", (request: Request, res: Response) => {
154 | addReactContext(res, MyContext, { name: "philipp" });
155 |
156 | res.render("my-view");
157 | });
158 |
159 | // Middleware:
160 | app.use((req: Request, res: Response, next: NextFunction) => {
161 | addReactContext(res, MyContext, {
162 | name: "philipp",
163 | });
164 | next();
165 | });
166 | ```
167 |
168 | Now you can consume the context data in any component:
169 |
170 | ```tsx
171 | // my-component.tsx
172 | import { useContext } from "react";
173 | import { MyContext } from "./my-context";
174 |
175 | export function MyComponent() {
176 | const { name } = useContext(MyContext);
177 | return Hallo, {name}! ;
178 | }
179 | ```
180 |
181 | ## GraphQL
182 |
183 | 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:
184 |
185 | ```sh
186 | $ npm install --save @apollo/client cross-fetch
187 | ```
188 |
189 | Now you can create an `ApolloRenderMiddleware` object and configure it as a middleware within `express-tsx-views`:
190 |
191 | ```ts
192 | import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
193 | import { ApolloRenderMiddleware } from "express-tsx-views/dist/apollo";
194 | // needed to create a apollo client HTTP link:
195 | import { fetch } from "cross-fetch";
196 |
197 | // Apollo client linking to an example GraphQL server
198 | const apollo = new ApolloClient({
199 | ssrMode: true,
200 | link: createHttpLink({
201 | uri: "https://swapi-graphql.netlify.app/.netlify/functions/index",
202 | fetch,
203 | }),
204 | cache: new InMemoryCache(),
205 | });
206 |
207 | setupReactViews(app, {
208 | viewsDirectory: resolve(__dirname, "views"),
209 | middlewares: [new ApolloRenderMiddleware(apollo)],
210 | });
211 | ```
212 |
213 | Example view (see the example folder in this project):
214 |
215 | ```ts
216 | export interface Film {
217 | id: string;
218 | title: string;
219 | releaseDate: string;
220 | }
221 |
222 | export interface AllFilms {
223 | allFilms: {
224 | films: Film[];
225 | };
226 | }
227 |
228 | const MY_QUERY = gql`
229 | query AllFilms {
230 | allFilms {
231 | films {
232 | id
233 | title
234 | releaseDate
235 | }
236 | }
237 | }
238 | `;
239 |
240 | export interface Props {
241 | title: string;
242 | lang: string;
243 | }
244 |
245 | export default function MyView(props: Props): ReactElement {
246 | const { data, error } = useQuery(MY_QUERY);
247 |
248 | if (error) {
249 | throw error;
250 | }
251 |
252 | return (
253 |
254 | Films:
255 | {data?.allFilms.films.map((film) => (
256 |
257 | {film.title} ({new Date(film.releaseDate).getFullYear()})
258 |
259 | ))}
260 |
261 | );
262 | }
263 | ```
264 |
265 | # License
266 |
267 | express-tsx-views is distributed under the MIT license. [See LICENSE](./LICENSE) for details.
268 |
269 | [coveralls-image]: https://img.shields.io/coveralls/pmb0/express-tsx-views/master.svg
270 | [coveralls-url]: https://coveralls.io/r/pmb0/express-tsx-views?branch=master
271 | [build-image]: https://github.com/pmb0/express-tsx-views/workflows/Tests/badge.svg
272 | [build-url]: https://github.com/pmb0/express-tsx-views/actions?query=workflow%3ATests
273 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/react-view-engine.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`react-view-engine Rendering 1`] = `
4 | "
5 |
6 |
7 |
10 |
11 |
12 | Test
13 |
14 |
15 | Test
16 | Some component:
17 | Hello from MyComponent! Provided prop:
18 | "foo"
19 |
20 |
21 | props:
22 |
23 | {
24 | "title": "Test",
25 | "lang": "de"
26 | }
28 |
29 |
30 |
31 | context1:
32 |
33 | {
34 | "id": 1,
35 | "name": "test"
36 | }
38 |
39 |
40 |
41 | context2:
42 |
43 | {
44 | "someProperty": "mausi"
45 | }
47 |
48 |
49 |
50 | "
51 | `;
52 |
53 | exports[`react-view-engine Rendering with locals 1`] = `
54 | "
55 |
56 |
57 |
60 |
61 |
62 | from locals
63 |
64 |
65 | from locals
66 | Some component:
67 | Hello from MyComponent! Provided prop:
68 | "foo"
69 |
70 |
71 | props:
72 |
73 | {
74 | "title": "from locals"
75 | }
77 |
78 |
79 |
80 | context1:
81 |
82 | {
83 | "id": 1,
84 | "name": "test"
85 | }
87 |
88 |
89 |
90 | context2:
91 |
92 | {
93 | "someProperty": "mausi"
94 | }
96 |
97 |
98 |
99 | "
100 | `;
101 |
--------------------------------------------------------------------------------
/__tests__/react-view-engine.test.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import request from 'supertest'
3 | import { app } from '../example/app'
4 | import { setupReactViews } from '../src'
5 |
6 | describe('react-view-engine', () => {
7 | test('Rendering', async () => {
8 | const response = await request(app).get('/').expect(200)
9 |
10 | expect(response.text).toMatchSnapshot()
11 | })
12 |
13 | test('Rendering with locals', async () => {
14 | const response = await request(app).get('/with-locals').expect(200)
15 |
16 | expect(response.text).toMatchSnapshot()
17 | })
18 |
19 | test('without DOCTYPE', async () => {
20 | setupReactViews(app, {
21 | viewsDirectory: resolve(__dirname, '../example/views'),
22 | prettify: true,
23 | doctype: '',
24 | })
25 |
26 | const response = await request(app).get('/with-locals').expect(200)
27 | expect(response.text).toMatchInlineSnapshot(`
28 | "
29 |
30 |
33 |
34 |
35 | from locals
36 |
37 |
38 | from locals
39 | Some component:
40 | Hello from MyComponent! Provided prop:
41 | "foo"
42 |
43 |
44 | props:
45 |
46 | {
47 | "title": "from locals"
48 | }
50 |
51 |
52 |
53 | context1:
54 |
55 | {
56 | "id": 1,
57 | "name": "test"
58 | }
60 |
61 |
62 |
63 | context2:
64 |
65 |
66 |
67 |
68 | "
69 | `)
70 | })
71 | })
72 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | }
4 |
--------------------------------------------------------------------------------
/example/app.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable toplevel/no-toplevel-side-effect */
2 | import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client'
3 | import { fetch } from 'cross-fetch'
4 | import express, { NextFunction, Request, Response } from 'express'
5 | import { resolve } from 'path'
6 | import {
7 | addReactContext,
8 | PrettifyRenderMiddleware,
9 | setupReactViews,
10 | } from '../src'
11 | import { ApolloRenderMiddleware } from '../src/apollo'
12 | import { MyContext, MyContext2 } from './my-context'
13 | import { Props } from './views/my-view'
14 |
15 | export const app = express()
16 |
17 | const apollo = new ApolloClient({
18 | ssrMode: true,
19 | link: createHttpLink({
20 | uri: 'https://swapi-graphql.netlify.app/.netlify/functions/index',
21 | fetch,
22 | }),
23 | cache: new InMemoryCache(),
24 | })
25 |
26 | setupReactViews(app, {
27 | viewsDirectory: resolve(__dirname, 'views'),
28 | middlewares: [
29 | new ApolloRenderMiddleware(apollo),
30 | new PrettifyRenderMiddleware(),
31 | ],
32 | transform: async (html) => {
33 | // eslint-disable-next-line no-magic-numbers
34 | await new Promise((resolve) => setTimeout(resolve, 100))
35 | return html.replace('', 'h1{color:red}')
36 | },
37 | })
38 |
39 | // Set react context globally for all routes
40 | app.use((req: Request, res: Response, next: NextFunction) => {
41 | addReactContext(res, MyContext, {
42 | id: 1,
43 | name: 'test',
44 | })
45 | next()
46 | })
47 |
48 | app.get('/', (request: Request, res: Response) => {
49 | const data: Props = { title: 'Test', lang: 'de' }
50 |
51 | addReactContext(res, MyContext2, { someProperty: 'mausi' })
52 |
53 | res.render('my-view', data)
54 | })
55 |
56 | app.get(
57 | '/with-locals',
58 | (request: Request, res: Response>) => {
59 | res.locals.title = 'from locals'
60 | res.render('my-view')
61 | },
62 | )
63 |
64 | app.get('/gql', (request: Request, res: Response) => {
65 | const data: Props = { title: 'Test', lang: 'de' }
66 | res.render('my-gql-view', data)
67 | })
68 |
--------------------------------------------------------------------------------
/example/bin/www.ts:
--------------------------------------------------------------------------------
1 | import { AddressInfo } from 'net'
2 | import { app } from '../app'
3 |
4 | // eslint-disable-next-line no-magic-numbers
5 | const server = app.listen(8080, function () {
6 | const { address, port } = server.address() as AddressInfo
7 | // eslint-disable-next-line no-console
8 | console.log('✔ Example app listening at http://%s:%s', address, port)
9 | })
10 |
--------------------------------------------------------------------------------
/example/my-context.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 |
3 | export interface MyContextProps {
4 | id: number
5 | name: string
6 | }
7 |
8 | export const MyContext = createContext(undefined)
9 |
10 | export interface MyContext2Props {
11 | someProperty: string
12 | }
13 |
14 | export const MyContext2 = createContext(undefined)
15 |
--------------------------------------------------------------------------------
/example/views/dumper.component.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-magic-numbers */
2 | import React, { ReactElement } from 'react'
3 |
4 | export interface DumperProps {
5 | data: unknown
6 | caption?: string
7 | }
8 |
9 | export function Dumper({ caption, data }: DumperProps): ReactElement {
10 | return (
11 |
12 | {caption && {caption}: }
13 | {JSON.stringify(data, null, 2)}
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/example/views/my-component.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, ReactElement } from 'react'
2 | import { Dumper } from './dumper.component'
3 |
4 | export interface Properties {
5 | myProp: string
6 | }
7 |
8 | export default class MyComponent extends Component {
9 | render(): ReactElement {
10 | return (
11 | <>
12 | Hello from {MyComponent.name}! Provided prop:{' '}
13 |
14 | >
15 | )
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/example/views/my-gql-view.tsx:
--------------------------------------------------------------------------------
1 | import { gql, useQuery } from '@apollo/client'
2 | import React, { ReactElement } from 'react'
3 | import MyComponent from './my-component'
4 | import { MyLayout } from './my-layout'
5 |
6 | export interface Film {
7 | id: string
8 | title: string
9 | releaseDate: string
10 | }
11 |
12 | export interface AllFilms {
13 | allFilms: {
14 | films: Film[]
15 | }
16 | }
17 |
18 | const MY_QUERY = gql`
19 | query AllFilms {
20 | allFilms {
21 | films {
22 | id
23 | title
24 | releaseDate
25 | }
26 | }
27 | }
28 | `
29 |
30 | export interface Props {
31 | title: string
32 | lang: string
33 | }
34 |
35 | export default function MyView(props: Props): ReactElement {
36 | const { data, error } = useQuery(MY_QUERY)
37 |
38 | if (error) {
39 | throw error
40 | }
41 |
42 | return (
43 |
44 | {props.title}
45 | Some component:
46 |
47 |
48 | Films:
49 | {data?.allFilms.films.map((film) => (
50 |
51 | {film.title} ({new Date(film.releaseDate).getFullYear()})
52 |
53 | ))}
54 |
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/example/views/my-layout.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement, ReactNode } from 'react'
2 |
3 | export type Language = string
4 |
5 | export interface LayoutProps {
6 | lang: Language
7 | title: string
8 | children: ReactNode
9 | }
10 |
11 | export const MyLayout = ({
12 | children,
13 | lang,
14 | title,
15 | }: LayoutProps): ReactElement => {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 | {title}
23 |
24 | {children}
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/example/views/my-view.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement, useContext } from 'react'
2 | import { MyContext, MyContext2 } from '../my-context'
3 | import { Dumper } from './dumper.component'
4 | import MyComponent from './my-component'
5 | import { MyLayout } from './my-layout'
6 |
7 | export interface Props {
8 | title: string
9 | lang: string
10 | }
11 |
12 | export default function MyView(props: Props): ReactElement {
13 | const context1 = useContext(MyContext)
14 | const context2 = useContext(MyContext2)
15 |
16 | return (
17 |
18 | {props.title}
19 | Some component:
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "restartable": "rs",
3 | "ignore": [".git", "node_modules/", "dist/", "coverage/"],
4 | "watch": ["src/"],
5 | "execMap": {
6 | "ts": "node -r ts-node/register --enable-source-maps"
7 | },
8 | "env": {
9 | "NODE_ENV": "development"
10 | },
11 | "ext": "js,json,ts,tsx"
12 | }
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "express-tsx-views",
3 | "version": "1.4.14",
4 | "main": "dist/index.js",
5 | "types": "dist/index.d.ts",
6 | "description": "Server-side JSX/TSX rendering for your express or NestJS application 🚀",
7 | "author": "Philipp Busse",
8 | "bugs": {
9 | "url": "https://github.com/pmb0/express-tsx-views/issues"
10 | },
11 | "homepage": "https://github.com/pmb0/express-tsx-views#readme",
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/pmb0/express-tsx-views.git"
15 | },
16 | "keywords": [
17 | "react",
18 | "typescript",
19 | "express",
20 | "nestjs",
21 | "ssr",
22 | "template-engine"
23 | ],
24 | "devDependencies": {
25 | "@apollo/client": "^3.3.11",
26 | "@commitlint/cli": "^17.0.0",
27 | "@commitlint/config-conventional": "^17.0.0",
28 | "@heise/eslint-config": "^20.0.7",
29 | "@semantic-release/changelog": "^6.0.0",
30 | "@semantic-release/commit-analyzer": "^9.0.0",
31 | "@semantic-release/git": "^10.0.0",
32 | "@semantic-release/npm": "^9.0.0",
33 | "@semantic-release/release-notes-generator": "^10.0.0",
34 | "@types/express": "^4.17.8",
35 | "@types/jest": "^28.0.0",
36 | "@types/react-dom": "^18.0.0",
37 | "@types/supertest": "^2.0.10",
38 | "cross-fetch": "^3.0.6",
39 | "express": "^4.17.1",
40 | "graphql": "^16.0.0",
41 | "husky": "^8.0.0",
42 | "jest": "^28.0.0",
43 | "lint-staged": "^13.0.0",
44 | "nodemon": "^2.0.13",
45 | "react": "^18.0.0",
46 | "react-dom": "^18.0.0",
47 | "semantic-release": "^19.0.0",
48 | "supertest": "^6.1.3",
49 | "ts-jest": "^28.0.0",
50 | "ts-node": "^10.0.0",
51 | "typescript": "^4.0.0"
52 | },
53 | "peerDependencies": {
54 | "react": ">= 16.13.1",
55 | "react-dom": ">= 16.13.1"
56 | },
57 | "husky": {
58 | "hooks": {
59 | "pre-commit": "lint-staged",
60 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
61 | }
62 | },
63 | "dependencies": {
64 | "prettier": "^2.0.5"
65 | },
66 | "scripts": {
67 | "build:test": "tsc --noEmit",
68 | "build": "tsc --build tsconfig.build.json",
69 | "clean": "rimraf dist",
70 | "lint": "eslint --cache .",
71 | "prebuild": "yarn clean",
72 | "start:example": "nodemon -w example --inspect example/bin/www.ts",
73 | "test": "jest",
74 | "prepare": "husky install"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/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/apollo.ts:
--------------------------------------------------------------------------------
1 | export * from './handler/middleware/apollo-render.middleware'
2 |
--------------------------------------------------------------------------------
/src/handler/index.ts:
--------------------------------------------------------------------------------
1 | // Do not export apollo since it is an optional dependency
2 | // export * from './middleware/apollo-render.middleware'
3 | export * from './middleware/create-react-context.middleware'
4 | export * from './middleware/default-render.middleware'
5 | export * from './middleware/prettify-render.middleware'
6 | export * from './middleware/tsx-render.middleware'
7 | export * from './tsx-render-context'
8 |
--------------------------------------------------------------------------------
/src/handler/middleware/apollo-render.middleware.ts:
--------------------------------------------------------------------------------
1 | import { ApolloClient, ApolloProvider } from '@apollo/client'
2 | import { getMarkupFromTree } from '@apollo/client/react/ssr'
3 | import React from 'react'
4 | import { TsxRenderContext } from '../tsx-render-context'
5 | import { TsxRenderMiddleware } from './tsx-render.middleware'
6 |
7 | export class ApolloRenderMiddleware extends TsxRenderMiddleware {
8 | #apollo: ApolloClient
9 |
10 | constructor(apollo: ApolloClient) {
11 | super()
12 | this.#apollo = apollo
13 | }
14 |
15 | public createElement(context: TsxRenderContext): TsxRenderContext {
16 | context.element = React.createElement(
17 | ApolloProvider,
18 | {
19 | client: this.#apollo,
20 | children: null,
21 | },
22 | context.element,
23 | )
24 |
25 | return super.createElement(context)
26 | }
27 |
28 | public async render(context: TsxRenderContext): Promise {
29 | context.html = await getMarkupFromTree({ tree: context.element })
30 | return super.render(context)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/handler/middleware/create-react-context.middleware.ts:
--------------------------------------------------------------------------------
1 | import React, { Context } from 'react'
2 | import { TsxRenderContext } from '../tsx-render-context'
3 | import { TsxRenderMiddleware } from './tsx-render.middleware'
4 |
5 | export class CreateReactContextRenderMiddleware<
6 | T = unknown,
7 | > extends TsxRenderMiddleware {
8 | readonly #context: Context
9 | readonly #value: T
10 |
11 | constructor(context: Context, value: T) {
12 | super()
13 | this.#context = context
14 | this.#value = value
15 | }
16 |
17 | public createElement(context: TsxRenderContext): TsxRenderContext {
18 | context.element = React.createElement(
19 | this.#context.Provider,
20 | { value: this.#value },
21 | context.element,
22 | )
23 |
24 | return super.createElement(context)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/handler/middleware/default-render.middleware.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/server'
3 | import { TsxRenderContext } from '../tsx-render-context'
4 | import { TsxRenderMiddleware } from './tsx-render.middleware'
5 |
6 | /**
7 | * All Concrete Handlers either handle a request or pass it to the next handler
8 | * in the chain.
9 | */
10 | export class DefaultTsxRenderMiddleware extends TsxRenderMiddleware {
11 | public createElement(context: TsxRenderContext): TsxRenderContext {
12 | context.element = React.createElement(context.component, context.vars)
13 | return super.createElement(context)
14 | }
15 |
16 | public async render(context: TsxRenderContext): Promise {
17 | if (!context.isRendered && context.element) {
18 | context.html = ReactDOM.renderToStaticMarkup(context.element)
19 | }
20 |
21 | return super.render(context)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/handler/middleware/prettify-render.middleware.ts:
--------------------------------------------------------------------------------
1 | import prettier from 'prettier'
2 | import { TsxRenderContext } from '../tsx-render-context'
3 | import { TsxRenderMiddleware } from './tsx-render.middleware'
4 |
5 | export class PrettifyRenderMiddleware extends TsxRenderMiddleware {
6 | // eslint-disable-next-line @typescript-eslint/require-await
7 | public async render(context: TsxRenderContext): Promise {
8 | if (context.html) {
9 | context.html = prettier.format(context.html, {
10 | parser: 'html',
11 | })
12 | }
13 |
14 | return context
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/handler/middleware/tsx-render.middleware.ts:
--------------------------------------------------------------------------------
1 | import { TsxRenderContext } from '../tsx-render-context'
2 |
3 | /**
4 | * The default chaining behavior can be implemented inside a base handler class.
5 | */
6 | export abstract class TsxRenderMiddleware {
7 | private next?: TsxRenderMiddleware
8 |
9 | public setNext(next: TsxRenderMiddleware): TsxRenderMiddleware {
10 | this.next = next
11 | return next
12 | }
13 |
14 | public createElement(context: TsxRenderContext): TsxRenderContext {
15 | if (this.next) {
16 | return this.next.createElement(context)
17 | }
18 |
19 | return context
20 | }
21 |
22 | public async render(context: TsxRenderContext): Promise {
23 | if (this.next) {
24 | return this.next.render(context)
25 | }
26 |
27 | return context
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/handler/tsx-render-context.ts:
--------------------------------------------------------------------------------
1 | import { ReactElement, ReactHTML } from 'react'
2 |
3 | export class TsxRenderContext {
4 | private _element?: ReactElement
5 | private _html?: string
6 |
7 | constructor(
8 | private readonly _component: keyof ReactHTML,
9 | private readonly _vars: Record,
10 | ) {}
11 |
12 | get component(): keyof ReactHTML {
13 | return this._component
14 | }
15 |
16 | get vars(): Record {
17 | return this._vars
18 | }
19 |
20 | get element(): ReactElement | undefined {
21 | return this._element
22 | }
23 |
24 | set element(element: ReactElement | undefined) {
25 | this._element = element
26 | }
27 |
28 | get html(): string | undefined {
29 | return this._html
30 | }
31 |
32 | set html(html: string | undefined) {
33 | this._html = html
34 | }
35 |
36 | get isRendered(): boolean {
37 | return !!this._html
38 | }
39 |
40 | hasElement(): boolean {
41 | return this._element !== undefined
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './handler'
2 | export * from './react-view-engine'
3 | export * from './react-view-engine.interface'
4 |
--------------------------------------------------------------------------------
/src/react-view-engine.interface.ts:
--------------------------------------------------------------------------------
1 | import { Application } from 'express'
2 | import { Context } from 'react'
3 | import { TsxRenderMiddleware } from './handler'
4 |
5 | export interface ReactViewsOptions {
6 | /**
7 | * The directory where your views (`.tsx` files) are stored. Must be
8 | * specified.
9 | */
10 | viewsDirectory: string
11 |
12 | /**
13 | * [Doctype](https://developer.mozilla.org/en-US/docs/Glossary/Doctype) to
14 | * be used. */
15 | doctype?: string
16 |
17 | /**
18 | * If activated, the generated HTML string is formatted using
19 | * [prettier](https://github.com/prettier/prettier)
20 | *
21 | * @deprecated use `PrettifyRenderMiddleware` instead, @see `middlewares`
22 | */
23 | prettify?: boolean
24 |
25 | /**
26 | * With this optional function the rendered HTML document can be modified. For
27 | * this purpose a function must be defined which gets the HTML `string` as
28 | * argument. The function returns a modified version of the HTML string as
29 | * `string`.
30 | */
31 | transform?: (html: string) => string | Promise
32 |
33 | middlewares?: TsxRenderMiddleware[]
34 | }
35 |
36 | export type EngineCallbackParameters = Parameters<
37 | Parameters[1]
38 | >
39 |
40 | export type ExpressLikeApp = Application
41 |
42 | export type ContextDefinition = [Context, T]
43 |
44 | export interface ReactViewsContext {
45 | contexts: ContextDefinition[]
46 | }
47 |
48 | export interface ExpressRenderOptions {
49 | [name: string]: unknown
50 | settings: Record
51 | _locals: Record
52 | cache: unknown
53 | contexts?: ContextDefinition[]
54 | }
55 |
--------------------------------------------------------------------------------
/src/react-view-engine.test.ts:
--------------------------------------------------------------------------------
1 | import express, { Express } from 'express'
2 | import { resolve } from 'path'
3 | import { reactViews, setupReactViews } from './react-view-engine'
4 |
5 | describe('react-view-engine', () => {
6 | let app: Express
7 | let engineSpy: jest.SpyInstance
8 | let setSpy: jest.SpyInstance
9 |
10 | beforeEach(() => {
11 | app = express()
12 | engineSpy = jest.spyOn(app, 'engine').mockImplementation()
13 | setSpy = jest.spyOn(app, 'set').mockImplementation()
14 | })
15 |
16 | describe('setupReactViews()', () => {
17 | it('throws an error if "viewDirectory" was not provided', () => {
18 | // @ts-ignore
19 | expect(() => setupReactViews(app, {})).toThrow(
20 | new Error('viewsDirectory missing'),
21 | )
22 | })
23 |
24 | it('sets the view engine', () => {
25 | setupReactViews(app, { viewsDirectory: '/tmp' })
26 |
27 | expect(engineSpy).toHaveBeenCalledWith('tsx', expect.any(Function))
28 | expect(setSpy).toHaveBeenCalledWith('view engine', 'tsx')
29 | expect(setSpy).toHaveBeenCalledWith('views', '/tmp')
30 | })
31 | })
32 |
33 | describe('reactViews()', () => {
34 | it('catches missing JSX file errors', async () => {
35 | const renderFile = reactViews({ viewsDirectory: __dirname })
36 |
37 | const callback = jest.fn()
38 | await renderFile('does-not-exist', {}, callback)
39 |
40 | expect(callback).toHaveBeenCalledWith(
41 | expect.objectContaining({
42 | message: `Cannot find module 'does-not-exist' from 'src/react-view-engine.ts'`,
43 | }),
44 | )
45 | })
46 |
47 | it('catches missing default exports', async () => {
48 | const renderFile = reactViews({ viewsDirectory: __dirname })
49 |
50 | const callback = jest.fn()
51 |
52 | // any .ts(x) file without a default export
53 | await renderFile(__filename, {}, callback)
54 |
55 | expect(callback).toHaveBeenCalledWith(
56 | new Error(`Module ${__filename} does not have a default export`),
57 | )
58 | })
59 |
60 | it('renders .tsx files', async () => {
61 | const renderFile = reactViews({ viewsDirectory: __dirname })
62 |
63 | const callback = jest.fn()
64 |
65 | await renderFile(
66 | resolve(__dirname, '../example/views/my-component'),
67 | {},
68 | callback,
69 | )
70 |
71 | expect(callback).toHaveBeenCalledWith(
72 | null,
73 | '\nHello from MyComponent! Provided prop: ',
74 | )
75 | })
76 | })
77 | })
78 |
--------------------------------------------------------------------------------
/src/react-view-engine.ts:
--------------------------------------------------------------------------------
1 | import { Application, Response } from 'express'
2 | import { Context } from 'react'
3 | import {
4 | CreateReactContextRenderMiddleware,
5 | DefaultTsxRenderMiddleware,
6 | TsxRenderContext,
7 | } from './handler'
8 | import { PrettifyRenderMiddleware } from './handler/middleware/prettify-render.middleware'
9 | import {
10 | EngineCallbackParameters,
11 | ExpressRenderOptions,
12 | ReactViewsContext,
13 | ReactViewsOptions,
14 | } from './react-view-engine.interface'
15 |
16 | export function isTranspiled(): boolean {
17 | return require.main?.filename?.endsWith('.js') ?? true
18 | }
19 |
20 | export function setupReactViews(
21 | app: Application,
22 | options: ReactViewsOptions,
23 | ): void {
24 | if (!options.viewsDirectory) {
25 | throw new Error('viewsDirectory missing')
26 | }
27 |
28 | const extension = isTranspiled() ? 'js' : 'tsx'
29 |
30 | // eslint-disable-next-line @typescript-eslint/no-misused-promises
31 | app.engine(extension, reactViews(options))
32 | app.set('view engine', extension)
33 | app.set('views', options.viewsDirectory)
34 | }
35 |
36 | export function addReactContext(
37 | res: Response,
38 | context: Context,
39 | value: T,
40 | ): void {
41 | const locals = (res as Response>).locals
42 | locals.contexts ??= []
43 | locals.contexts.unshift([context, value])
44 | }
45 |
46 | export function reactViews(reactViewOptions: ReactViewsOptions) {
47 | // eslint-disable-next-line complexity, sonarjs/cognitive-complexity
48 | return async function renderFile(
49 | ...[filename, options, next]: EngineCallbackParameters
50 | ): Promise {
51 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
52 | const { settings, _locals, cache, contexts, ...vars } =
53 | options as ExpressRenderOptions
54 |
55 | try {
56 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
57 | const Component = (await import(filename)).default
58 |
59 | if (!Component) {
60 | throw new Error(`Module ${filename} does not have a default export`)
61 | }
62 |
63 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
64 | let context = new TsxRenderContext(Component, vars)
65 |
66 | const defaultRenderer = new DefaultTsxRenderMiddleware()
67 |
68 | const middlewares = reactViewOptions.middlewares ?? []
69 |
70 | contexts?.forEach(([Context, props]) => {
71 | middlewares.push(new CreateReactContextRenderMiddleware(Context, props))
72 | })
73 |
74 | if (reactViewOptions.prettify ?? false) {
75 | middlewares.push(new PrettifyRenderMiddleware())
76 | }
77 |
78 | // eslint-disable-next-line sonarjs/no-ignored-return, unicorn/no-array-reduce
79 | middlewares.reduce((prev, next) => {
80 | prev.setNext(next)
81 | return next
82 | }, defaultRenderer)
83 |
84 | context = defaultRenderer.createElement(context)
85 |
86 | if (!context.hasElement()) {
87 | throw new Error('element was not created')
88 | }
89 |
90 | context = await defaultRenderer.render(context)
91 |
92 | if (!context.isRendered) {
93 | throw new Error('element was not rendered')
94 | }
95 |
96 | const doctype = reactViewOptions.doctype ?? '\n'
97 | const transform = reactViewOptions.transform || ((html) => html)
98 |
99 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
100 | next(null, await transform(doctype + context.html!))
101 | } catch (error) {
102 | next(error)
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------