├── .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 | [![npm version](https://badge.fury.io/js/express-tsx-views.svg)](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 | --------------------------------------------------------------------------------