├── .github └── workflows │ ├── github-publish.yml │ └── npm-publish.yml ├── .gitignore ├── .husky └── pre-commit ├── .vscode └── launch.json ├── README.md ├── example ├── main │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── nest-cli.json │ ├── package.json │ ├── react-router.config.ts │ ├── src │ │ ├── common │ │ │ ├── global.exception.filter.ts │ │ │ ├── global.interceptor.ts │ │ │ ├── global.response.ts │ │ │ ├── react-router.exceptions.ts │ │ │ ├── test.decorator.ts │ │ │ └── user.auth.guard.ts │ │ ├── entry.client.tsx │ │ ├── entry.server.tsx │ │ ├── main.ts │ │ ├── modules │ │ │ └── app │ │ │ │ ├── app.controller.ts │ │ │ │ ├── app.module.ts │ │ │ │ ├── app.service.ts │ │ │ │ └── dto │ │ │ │ └── login.dto.ts │ │ ├── root.tsx │ │ ├── routes.ts │ │ └── routes │ │ │ ├── _index.tsx │ │ │ ├── foo.tsx │ │ │ └── server │ │ │ ├── foo.server.ts │ │ │ └── index.server.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── tsconfig.nest.json │ └── vite.config.mts └── microservices │ ├── .eslintrc.js │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── nest-cli.json │ ├── package.json │ ├── src │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ └── main.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── package.json ├── packages ├── nest-react-router │ └── package.json └── nestjs-remix │ └── package.json ├── rollup.config.mjs ├── src ├── client │ ├── helper.ts │ ├── index.ts │ └── usePromiseSubmit.ts ├── index.ts └── server │ ├── index.ts │ ├── remix.constant.ts │ ├── remix.core.ts │ ├── remix.decorator.ts │ ├── remix.exceptions.ts │ ├── remix.helper.ts │ ├── remix.middleware.ts │ ├── remix.resolve.services.ts │ ├── remix.route.params.factory.ts │ ├── remix.service.ts │ └── remix.type.d.ts ├── synchronous-version.js └── tsconfig.json /.github/workflows/github-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish github package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | contents: read 13 | id-token: write 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup node 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: "20.x" 23 | registry-url: "https://npm.pkg.github.com" 24 | 25 | - name: Install dependencies 26 | run: npm install 27 | 28 | - name: Build 29 | run: npm run build 30 | 31 | - name: Publish to Github packages 32 | run: cd nestjs-remix && npm publish --provenance --access public --registry=https://npm.pkg.github.com/ 33 | env: 34 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish npm package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | contents: read 13 | id-token: write 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup node 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: "20.x" 23 | 24 | - name: Install dependencies 25 | run: npm install 26 | 27 | - name: Authenticate with the npm Registry 28 | run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc 29 | env: 30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | 32 | - name: Publish to npm 33 | run: npm run publish 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /node_modules 3 | packages/nestjs-remix/* 4 | !packages/nestjs-remix/package.json 5 | packages/nest-react-router/* 6 | !packages/nest-react-router/package.json 7 | example/vite.config.mts.* 8 | 9 | # lock file 10 | *.lock 11 | *.lockb 12 | 13 | # yarn 14 | .yarn/ 15 | .yarnrc.yml -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run synchronous-version 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "Attach NestJS WS", 11 | "port": 9229, 12 | "restart": true, 13 | "stopOnEntry": false, 14 | "protocol": "inspector" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to nest-react-router! 2 | 3 |
4 | NPM Version 5 | Package License 6 | NPM Downloads 7 |
8 | 9 |
A library that connects nest and react-router v7, use react-router as the view layer of nest
10 | 11 | ## Migrated to react-route v7 12 | 13 | Now this repository has been migrated to react-router v7 by releasing the new library [nest-react-router](https://www.npmjs.com/package/nest-react-router), nestjs-remix will be maintained synchronously for a period of time and will be completely deprecated in the future, you can continue to use nestjs-remix, but please note that there are some minor changes. 14 | 15 | ### request flag 16 | 17 | | before | after | 18 | | ------------- | ------------------- | 19 | | handleByRemix | handleByReactRouter | 20 | | remixArgs | reactRouterArgs | 21 | | remixParams | reactRouterParams | 22 | 23 | ### decorator 24 | 25 | | before | after | 26 | | --------- | --------------- | 27 | | RemixArgs | ReactRouterArgs | 28 | 29 | ## How to use 30 | 31 | ### Nest side 32 | 33 | ```typescript 34 | import { Loader, Action, useServer } from "nest-react-router"; 35 | 36 | @Injectable() 37 | export class IndexBackend { 38 | constructor(private readonly appService: AppService) {} 39 | 40 | @Loader() 41 | loader(@Req() req: Request, @Query("name") name?: string) { 42 | return this.appService.getHello(); 43 | } 44 | 45 | @Action() 46 | action(@Body() body: LoginDto) { 47 | return {}; 48 | } 49 | 50 | @Action.Patch() 51 | patch() { 52 | return "[patch]: returned by server side"; 53 | } 54 | 55 | @Action.Delete() 56 | delete() { 57 | return "[delete]: returned by server side"; 58 | } 59 | } 60 | 61 | export const useIndexServer = useServer(IndexBackend); 62 | ``` 63 | 64 | ### react-router side 65 | 66 | ```typescript 67 | import { 68 | type IndexBackend, 69 | useIndexServer, 70 | } from './server/index.server'; 71 | import { 72 | useActionData, 73 | useLoaderData, 74 | } from 'nest-react-router/client'; 75 | 76 | export const loader: LoaderFunction = (args) => { 77 | return useIndexServer(args); 78 | }; 79 | 80 | export const action: ActionFunction = (args) => { 81 | return useIndexServer(args); 82 | }; 83 | 84 | export default function Index() { 85 | const data = useLoaderData(); 86 | const actionData = useActionData(); 87 | return
{data.message}
88 | } 89 | ``` 90 | 91 | For more detailed usage, please refer to Example 92 | 93 | ## Quick Start 94 | 95 | ``` 96 | git clone https://github.com/JinYuSha0/nest-remix.git 97 | ``` 98 | 99 | ## Running the example 100 | 101 | ```bash 102 | yarn install 103 | yarn start:dev 104 | # If you want to try to get data from microservice 105 | yarn start:dev2 106 | ``` 107 | 108 | ## Integrate 109 | 110 | ### 1.Install 111 | 112 | ``` 113 | yarn add nest-react-router 114 | ``` 115 | 116 | ### 2.Inject react-router services 117 | 118 | ```typescript 119 | import { Module } from "@nestjs/common"; 120 | import { resolve } from "path"; 121 | import { resolveReactRouterServices } from "nest-react-router"; 122 | 123 | @Module({ 124 | imports: [], 125 | controllers: [AppController], 126 | providers: [ 127 | AppService, 128 | ...resolveReactRouterServices(resolve("dist/routes/server")), 129 | ], 130 | }) 131 | export class AppModule {} 132 | ``` 133 | 134 | Please note that the path is the path after build, not the source code path 135 | 136 | ### 3. Start nest-react-router 137 | 138 | ```typescript 139 | import { startNestReactRouter } from "nest-react-router"; 140 | 141 | async function bootstrap() { 142 | const app = await NestFactory.create(AppModule); 143 | // ... 144 | await startNestReactRouter(app); 145 | await app.listen(3000); 146 | } 147 | bootstrap(); 148 | ``` 149 | 150 | ### 4.Modify package.json scripts (Optional) 151 | 152 | ```json 153 | "scripts": { 154 | "build": "concurrently \"npm run build:nest\" \"npm run build:react-router\" -n \"NEST,REACT-ROUTER\"", 155 | "start": "nest start", 156 | "start:dev": "cross-env NODE_ENV=development concurrently \"npm run start:dev:nest\" -n \"NEST\"", 157 | "start:prod": "cross-env NODE_ENV=production node dist/main", 158 | "build:nest": "rimraf dist && nest build -p tsconfig.nest.json", 159 | "build:react-router": "rimraf build && react-router build", 160 | "start:dev:nest": "rimraf dist && nest start --watch -p tsconfig.nest.json" 161 | } 162 | ``` 163 | 164 | ## Troubleshooting 165 | 166 |
167 | [vite] Error when evaluating SSR module /src/routes/server/xxx.server.ts: failed to import "nest-react-router" 168 | 169 | Modify vite.config.mts 170 | 171 | ```js 172 | ssr: { 173 | noExternal: process.env.NODE_ENV === 'development' 174 | ? ['nest-react-router', /* ... */] // add this 175 | : [/* ... */], 176 | }, 177 | ``` 178 | 179 |
180 | 181 |
182 | 183 |
184 | useloaderdata must be used within a data router 185 | 186 | Modify vite.config.mts 187 | 188 | ```js 189 | ssr: { 190 | noExternal: process.env.NODE_ENV === 'development' 191 | ? ['nest-react-router', /* ... */] // add this 192 | : [/* ... */], 193 | }, 194 | ``` 195 | 196 |
197 | -------------------------------------------------------------------------------- /example/main/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | /build 38 | /public/build 39 | *.tsbuildinfo 40 | .cache 41 | 42 | # lock file 43 | *.lock 44 | *.lockb 45 | 46 | # react-router 47 | .react-router/ -------------------------------------------------------------------------------- /example/main/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /example/main/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ yarn install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ yarn run start 40 | 41 | # watch mode 42 | $ yarn run start:dev 43 | 44 | # production mode 45 | $ yarn run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ yarn run test 53 | 54 | # e2e tests 55 | $ yarn run test:e2e 56 | 57 | # test coverage 58 | $ yarn run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /example/main/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example/main/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.0.2", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "concurrently \"npm run build:nest\" \"npm run build:react-router\" -n \"NEST,REACT-ROUTER\"", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "cross-env NODE_ENV=development concurrently \"npm run start:dev:nest\" -n \"NEST\"", 13 | "start:prod": "cross-env NODE_ENV=production node dist/main", 14 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 15 | "test": "jest", 16 | "test:watch": "jest --watch", 17 | "test:cov": "jest --coverage", 18 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 19 | "test:e2e": "jest --config ./test/jest-e2e.json", 20 | "build:nest": "rimraf dist && nest build -p tsconfig.nest.json", 21 | "build:react-router": "rimraf build && react-router build", 22 | "start:dev:nest": "rimraf dist && nest start --watch -p tsconfig.nest.json" 23 | }, 24 | "dependencies": { 25 | "@nestjs/common": "^10.4.8", 26 | "@nestjs/core": "^10.4.8", 27 | "@nestjs/platform-express": "^10.4.8", 28 | "@nestjs/serve-static": "^4.0.2", 29 | "@nestjs/swagger": "^8.0.7", 30 | "@react-router/express": "^7.0.0", 31 | "@react-router/fs-routes": "^7.0.1", 32 | "@react-router/node": "^7.0.0", 33 | "@react-router/serve": "^7.0.0", 34 | "class-transformer": "^0.5.1", 35 | "class-validator": "^0.14.1", 36 | "isbot": "^5.1.17", 37 | "react": "18.3.1", 38 | "react-dom": "18.3.1", 39 | "react-router": "^7.0.0", 40 | "reflect-metadata": "^0.2.2", 41 | "rxjs": "^7.8.2" 42 | }, 43 | "devDependencies": { 44 | "@nestjs/cli": "^10.4.8", 45 | "@nestjs/schematics": "^10.2.3", 46 | "@nestjs/testing": "^10.4.8", 47 | "@react-router/dev": "^7.0.0", 48 | "@types/express": "^5.0.0", 49 | "@types/jest": "^29.5.12", 50 | "@types/node": "^20.12.12", 51 | "@types/react": "^18.3.3", 52 | "@types/react-dom": "^18.3.0", 53 | "@types/supertest": "^2.0.12", 54 | "@typescript-eslint/eslint-plugin": "^6.0.0", 55 | "@typescript-eslint/parser": "^6.0.0", 56 | "concurrently": "^8.2.2", 57 | "cross-env": "^7.0.3", 58 | "eslint": "^8.42.0", 59 | "eslint-config-prettier": "^9.0.0", 60 | "eslint-plugin-prettier": "^5.0.0", 61 | "jest": "^29.5.0", 62 | "prettier": "^3.0.0", 63 | "rimraf": "^6.0.1", 64 | "source-map-support": "^0.5.21", 65 | "supertest": "^6.3.3", 66 | "ts-jest": "^29.1.3", 67 | "ts-loader": "^9.5.1", 68 | "ts-node": "^10.9.2", 69 | "tsconfig-paths": "^4.2.0", 70 | "typescript": "^5.2.2", 71 | "vite": "^5.4.11", 72 | "vite-plugin-commonjs": "^0.10.4", 73 | "vite-tsconfig-paths": "^5.1.3" 74 | }, 75 | "peerDependencies": { 76 | "@nestjs/common": "^10.3.8", 77 | "@nestjs/core": "^10.3.8", 78 | "@nestjs/serve-static": "^4.0.2", 79 | "express-serve-static-core": "^0.1.1", 80 | "reflect-metadata": "^0.2.2" 81 | }, 82 | "jest": { 83 | "moduleFileExtensions": [ 84 | "js", 85 | "json", 86 | "ts" 87 | ], 88 | "rootDir": "src", 89 | "testRegex": ".*\\.spec\\.ts$", 90 | "transform": { 91 | "^.+\\.(t|j)s$": "ts-jest" 92 | }, 93 | "collectCoverageFrom": [ 94 | "**/*.(t|j)s" 95 | ], 96 | "coverageDirectory": "../coverage", 97 | "testEnvironment": "node" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /example/main/react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@react-router/dev/config'; 2 | export default { 3 | ssr: true, 4 | appDirectory: 'src', 5 | serverModuleFormat: 'esm', 6 | serverBuildFile: 'index.mjs', 7 | } satisfies Config; 8 | -------------------------------------------------------------------------------- /example/main/src/common/global.exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentsHost, 3 | Catch, 4 | ExceptionFilter, 5 | HttpException, 6 | HttpExceptionBody, 7 | HttpExceptionBodyMessage, 8 | } from '@nestjs/common'; 9 | import { GlobalResponse } from './global.response'; 10 | import { ReactRouterException } from 'nest-react-router'; 11 | 12 | const IS_PRODUCTION_ENV = process.env.NODE_ENV === 'production'; 13 | const SYS_INTER_EXCEPTION_MSG = 'System internal exception'; 14 | 15 | @Catch() 16 | export class GlobalExceptionFilter implements ExceptionFilter { 17 | catch(exception: HttpException, host: ArgumentsHost) { 18 | const ctx = host.switchToHttp(); 19 | const response = ctx.getResponse(); 20 | const request: Request = ctx.getRequest(); 21 | const status = exception.getStatus ? exception.getStatus() : 500; 22 | const exceptionRes = exception.getResponse?.() as 23 | | HttpExceptionBody 24 | | undefined; 25 | let statusCode = 26 | (exceptionRes?.statusCode ?? exception.getStatus) 27 | ? exception.getStatus() 28 | : 500; 29 | let statusText = 30 | exceptionRes?.error ?? exception.message ?? SYS_INTER_EXCEPTION_MSG; 31 | let cause: HttpExceptionBodyMessage | undefined = 32 | exceptionRes?.message ?? 33 | (statusCode === 500 ? exception.stack : undefined); 34 | 35 | if (status === 500 && IS_PRODUCTION_ENV) { 36 | cause = void 0; 37 | statusText = SYS_INTER_EXCEPTION_MSG; 38 | } 39 | 40 | // Judge is handled by react-router 41 | if (request.handleByReactRouter) { 42 | if (exception instanceof Response) { 43 | throw exception; 44 | } else if (exception instanceof ReactRouterException) { 45 | throw exception.toResponse(); 46 | } else { 47 | throw new ReactRouterException( 48 | process.env.NODE_ENV === 'production' 49 | ? 'Internal Server Error' 50 | : exception.message, 51 | ).toResponse(); 52 | } 53 | } 54 | 55 | const errorResponse = new GlobalResponse( 56 | { msg: statusText, cause }, 57 | statusCode, 58 | false, 59 | ); 60 | 61 | response.status(status); 62 | response.header('Content-Type', 'application/json; charset=utf-8'); 63 | response.send(errorResponse.toPlainObject()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /example/main/src/common/global.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | import { GlobalResponse } from './global.response'; 5 | 6 | export class GlobalInterceptor implements NestInterceptor { 7 | intercept(context: ExecutionContext, next: CallHandler): Observable { 8 | return next.handle().pipe( 9 | map(async (data: any) => { 10 | if (data == null) return; 11 | const ctx = context.switchToHttp(); 12 | const request: Request = ctx.getRequest(); 13 | // react-router is not used GlobalResponse 14 | if (request.handleByReactRouter) { 15 | return data; 16 | } 17 | return new GlobalResponse(data, 200, true).toPlainObject(); 18 | }), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/main/src/common/global.response.ts: -------------------------------------------------------------------------------- 1 | export class GlobalResponse { 2 | public readonly data: T; 3 | public readonly code: number; 4 | public readonly success: boolean; 5 | 6 | constructor(data: T, code?: number, success?: boolean) { 7 | this.data = data; 8 | this.code = code ?? 200; 9 | this.success = success ?? true; 10 | } 11 | 12 | toPlainObject() { 13 | return { 14 | data: this.data, 15 | code: this.code, 16 | success: this.success, 17 | }; 18 | } 19 | 20 | toJson() { 21 | return JSON.stringify(this.toPlainObject()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/main/src/common/react-router.exceptions.ts: -------------------------------------------------------------------------------- 1 | import { ReactRouterException } from 'nest-react-router'; 2 | 3 | export const ReactRouterUnauthorizedException = (message?: string) => 4 | new ReactRouterException(message ?? 'Unauthorized', 401); 5 | export const ReactRouterForbiddenException = (message?: string) => 6 | new ReactRouterException(message ?? 'Forbidden', 403); 7 | -------------------------------------------------------------------------------- /example/main/src/common/test.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from '@nestjs/common'; 2 | 3 | export const Test = createParamDecorator((data, ctx) => { 4 | const request = ctx.switchToHttp().getRequest(); 5 | return 'test'; 6 | }); 7 | -------------------------------------------------------------------------------- /example/main/src/common/user.auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from 'react-router'; 2 | import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; 3 | import { Observable } from 'rxjs'; 4 | import { AppService } from '~/modules/app/app.service'; 5 | import { ReactRouterUnauthorizedException } from './react-router.exceptions'; 6 | 7 | @Injectable() 8 | export class UserAuthGuard implements CanActivate { 9 | private readonly redirectUrl?: string; 10 | 11 | constructor(options?: { redirectUrl?: string }) { 12 | this.redirectUrl = options?.redirectUrl; 13 | } 14 | 15 | canActivate( 16 | context: ExecutionContext, 17 | ): boolean | Promise | Observable { 18 | const req: Request = context.switchToHttp().getRequest(); 19 | if (AppService.logged) { 20 | return true; 21 | } 22 | if ( 23 | this.redirectUrl != null && 24 | req.method === 'GET' && 25 | req.handleByReactRouter 26 | ) { 27 | throw redirect(this.redirectUrl); 28 | } 29 | throw ReactRouterUnauthorizedException(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/main/src/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { HydratedRouter } from 'react-router/dom'; 2 | import { startTransition, useEffect, StrictMode } from 'react'; 3 | import { hydrateRoot } from 'react-dom/client'; 4 | 5 | const Mount: React.FC void }>> = ( 6 | props, 7 | ) => { 8 | useEffect(props.onMount, []); 9 | return props.children; 10 | }; 11 | 12 | function hydrate() { 13 | let recoverMethods: Function[] = []; 14 | function remove(elem: HTMLElement) { 15 | elem.parentElement?.removeChild(elem); 16 | return elem; 17 | } 18 | function keepClean( 19 | container: HTMLElement, 20 | detectKeep: (node: HTMLElement) => boolean, 21 | ) { 22 | let temp: HTMLElement[] = []; 23 | container.childNodes.forEach((elem) => { 24 | const _elem = elem as HTMLElement; 25 | if (!detectKeep(_elem)) { 26 | temp.push(remove(_elem)); 27 | } 28 | }); 29 | return () => { 30 | if (!temp.length) return; 31 | temp.forEach((elem) => container.appendChild(elem)); 32 | temp = []; 33 | }; 34 | } 35 | function recover() { 36 | recoverMethods.forEach((method) => method()); 37 | recoverMethods = []; 38 | } 39 | recoverMethods.push( 40 | keepClean(document.body.parentElement!, (node) => { 41 | return ['HEAD', 'BODY'].includes(node.tagName); 42 | }), 43 | keepClean(document.body, (node) => { 44 | return node.getAttribute?.('id') === 'root'; 45 | }), 46 | ); 47 | startTransition(() => { 48 | hydrateRoot( 49 | document, 50 | 51 | 52 | 53 | 54 | , 55 | ); 56 | }); 57 | } 58 | 59 | if (window.requestIdleCallback) { 60 | window.requestIdleCallback(hydrate); 61 | } else { 62 | window.setTimeout(hydrate, 1); 63 | } 64 | -------------------------------------------------------------------------------- /example/main/src/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import type { AppLoadContext, EntryContext } from 'react-router'; 2 | import { ServerRouter } from 'react-router'; 3 | import { isbot } from 'isbot'; 4 | import { PassThrough } from 'stream'; 5 | import { renderToPipeableStream } from 'react-dom/server'; 6 | import { createReadableStreamFromReadable } from '@react-router/node'; 7 | 8 | const ABORT_DELAY = 5000; 9 | 10 | export default async function handleRequest( 11 | request: Request, 12 | responseStatusCode: number, 13 | responseHeaders: Headers, 14 | routerContext: EntryContext, 15 | _loadContext: AppLoadContext, 16 | ) { 17 | const callbackName = isbot(request.headers.get('user-agent')) 18 | ? 'onAllReady' 19 | : 'onShellReady'; 20 | 21 | return new Promise((resolve, reject) => { 22 | let didError = false; 23 | const { pipe, abort } = renderToPipeableStream( 24 | , 25 | { 26 | [callbackName]: () => { 27 | const body = new PassThrough(); 28 | const stream = createReadableStreamFromReadable(body); 29 | responseHeaders.set('Content-Type', 'text/html'); 30 | 31 | resolve( 32 | new Response(stream, { 33 | headers: responseHeaders, 34 | status: didError ? 500 : responseStatusCode, 35 | }), 36 | ); 37 | 38 | pipe(body); 39 | }, 40 | onShellError(error: unknown) { 41 | reject(error); 42 | }, 43 | onError(error: unknown) { 44 | didError = true; 45 | 46 | console.error(error); 47 | }, 48 | }, 49 | ); 50 | 51 | setTimeout(abort, ABORT_DELAY); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /example/main/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestApplication } from '@nestjs/core'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './modules/app/app.module'; 4 | import { startNestReactRouter } from 'nest-react-router'; 5 | import { ValidationPipe } from '@nestjs/common'; 6 | import { GlobalExceptionFilter } from './common/global.exception.filter'; 7 | import { GlobalInterceptor } from './common/global.interceptor'; 8 | 9 | async function bootstrap() { 10 | const app = await NestFactory.create(AppModule); 11 | app.useGlobalPipes(new ValidationPipe({ transform: true })); 12 | app.useGlobalFilters(new GlobalExceptionFilter()); 13 | app.useGlobalInterceptors(new GlobalInterceptor()); 14 | await startNestReactRouter(app); 15 | await app.listen(3000); 16 | console.log('listen on port 3000'); 17 | } 18 | bootstrap(); 19 | -------------------------------------------------------------------------------- /example/main/src/modules/app/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller('/') 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get('/hello') 9 | getHello() { 10 | return this.appService.getHello(); 11 | } 12 | 13 | @Get('/json') 14 | getJson() { 15 | return this.appService.getJson(); 16 | } 17 | 18 | @Get('/bad') 19 | getBad() { 20 | throw new BadRequestException(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/main/src/modules/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { resolve } from 'path'; 3 | import { resolveReactRouterServices } from 'nest-react-router'; 4 | import { AppController } from './app.controller'; 5 | import { AppService } from './app.service'; 6 | 7 | @Module({ 8 | controllers: [AppController], 9 | providers: [ 10 | AppService, 11 | ...resolveReactRouterServices(resolve('dist/routes/server')), 12 | ], 13 | }) 14 | export class AppModule {} 15 | -------------------------------------------------------------------------------- /example/main/src/modules/app/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { LoginDto } from './dto/login.dto'; 3 | 4 | @Injectable() 5 | export class AppService { 6 | static logged = false; 7 | 8 | getHello(name?: string): string { 9 | return name ? `Hello, ${name}` : 'Hello World!'; 10 | } 11 | 12 | getJson() { 13 | return { foo: 1, bar: '2' }; 14 | } 15 | 16 | login(dto: LoginDto) { 17 | if (dto.username === 'admin' && dto.password === '123456') { 18 | AppService.logged = true; 19 | return true; 20 | } 21 | return false; 22 | } 23 | 24 | logout() { 25 | AppService.logged = false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/main/src/modules/app/dto/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsString, IsNotEmpty, Length } from 'class-validator'; 3 | import { Expose } from 'class-transformer'; 4 | 5 | export class LoginDto { 6 | @Expose() 7 | @ApiProperty({ 8 | description: 'username', 9 | required: true, 10 | }) 11 | @IsString() 12 | @IsNotEmpty() 13 | @Length(4, 36) 14 | readonly username: string; 15 | 16 | @Expose() 17 | @ApiProperty({ 18 | description: 'password', 19 | required: true, 20 | }) 21 | @IsString() 22 | @IsNotEmpty() 23 | @Length(6, 32) 24 | readonly password: string; 25 | } 26 | -------------------------------------------------------------------------------- /example/main/src/root.tsx: -------------------------------------------------------------------------------- 1 | import type { MetaFunction } from 'react-router'; 2 | import type { ReactRouterError } from 'nest-react-router/client'; 3 | import { 4 | Link, 5 | Links, 6 | Meta, 7 | Outlet, 8 | Scripts, 9 | useRouteError, 10 | } from 'react-router'; 11 | 12 | export const meta: MetaFunction = () => [ 13 | { 14 | charset: 'utf-8', 15 | viewport: 'width=device-width,initial-scale=1', 16 | }, 17 | ]; 18 | 19 | export default function App() { 20 | return ( 21 | 22 | 23 | nest-react-router demo 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
32 | 33 | 34 | ); 35 | } 36 | 37 | export function ErrorBoundary() { 38 | const error = useRouteError() as ReactRouterError; 39 | return ( 40 | 41 | 42 | {error.statusText} 43 | 44 | 45 | 46 | 47 |
48 |

{error.data.code}

49 |

{error.data.message}

50 | 51 | 52 | 53 |
54 | 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /example/main/src/routes.ts: -------------------------------------------------------------------------------- 1 | import { type RouteConfig } from '@react-router/dev/routes'; 2 | import { flatRoutes } from '@react-router/fs-routes'; 3 | 4 | export default flatRoutes() satisfies RouteConfig; 5 | -------------------------------------------------------------------------------- /example/main/src/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | ActionFunction, 3 | LoaderFunction, 4 | ShouldRevalidateFunction, 5 | } from 'react-router'; 6 | import { Form, useRevalidator, Await } from 'react-router'; 7 | import { type IndexBackend, useIndexServer } from './server/index.server'; 8 | import { 9 | useActionData, 10 | useLoaderData, 11 | usePromiseSubmit, 12 | } from 'nest-react-router/client'; 13 | import { Suspense } from 'react'; 14 | 15 | export const loader: LoaderFunction = (args) => { 16 | return useIndexServer(args); 17 | }; 18 | 19 | export const action: ActionFunction = (args) => { 20 | return useIndexServer(args); 21 | }; 22 | 23 | export const shouldRevalidate: ShouldRevalidateFunction = ({ formMethod }) => { 24 | return !formMethod || formMethod === 'GET'; 25 | }; 26 | 27 | export default function Index() { 28 | const { message, loadData, sumFromMicroService } = 29 | useLoaderData() ?? {}; 30 | const actionData = useActionData(); 31 | const revalidator = useRevalidator(); 32 | const [patch] = usePromiseSubmit(); 33 | const [deleted] = usePromiseSubmit(); 34 | const syncAlert = (wrapped: () => Promise) => { 35 | return async () => { 36 | const data = await wrapped(); 37 | if (typeof data === 'string') alert(data); 38 | }; 39 | }; 40 | return ( 41 |
42 |
43 |

Loader data

44 |

{message}

45 | 46 |
47 |
48 |
49 |

RESTFUL

50 | 55 | 60 |
61 |
62 |
63 |

Defer data

64 | loading...}> 65 | {(data) => {data}} 66 | 67 |
68 |
69 |
70 |

MicroService data

71 | loading...}> 72 | 73 | {(data) => {data}} 74 | 75 | 76 |
77 |
78 |
79 |

Form submit

80 |
81 | 87 |
88 | 94 |
95 | 96 |
97 | {typeof actionData?.msg === 'string' && ( 98 | {actionData.msg} 99 | )} 100 |
101 |
102 |

Nestjs api

103 | 104 | 105 | 106 | 107 | 108 | 109 |
110 |
111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /example/main/src/routes/foo.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionFunction, LoaderFunction } from 'react-router'; 2 | import { useLoaderData } from 'nest-react-router/client'; 3 | import { type FooBackend, useFooServer } from './server/foo.server'; 4 | import { usePromiseSubmit } from 'nest-react-router/client'; 5 | 6 | export const loader: LoaderFunction = (args) => { 7 | return useFooServer(args); 8 | }; 9 | 10 | export const action: ActionFunction = (args) => { 11 | return useFooServer(args); 12 | }; 13 | 14 | export default function Index() { 15 | const data = useLoaderData(); 16 | const [submit] = usePromiseSubmit(); 17 | return ( 18 |
19 |

{data}

20 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /example/main/src/routes/server/foo.server.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderFunctionArgs } from 'react-router'; 2 | import { Injectable, Req, UseGuards } from '@nestjs/common'; 3 | import { Loader, Action, useServer } from 'nest-react-router'; 4 | import { AppService } from '~/modules/app/app.service'; 5 | import { UserAuthGuard } from '~/common/user.auth.guard'; 6 | 7 | @Injectable() 8 | @UseGuards( 9 | new UserAuthGuard({ 10 | // redirectUrl: '/', 11 | }), 12 | ) 13 | export class FooBackend { 14 | constructor(private readonly appService: AppService) {} 15 | 16 | @Loader() 17 | loader(@Req() req: Request) { 18 | return 'Congratulations you have permission to access this page'; 19 | } 20 | 21 | @Action() 22 | action() { 23 | this.appService.logout(); 24 | return true; 25 | } 26 | } 27 | 28 | export const useFooServer = (args: LoaderFunctionArgs) => 29 | useServer(FooBackend)(args); 30 | -------------------------------------------------------------------------------- /example/main/src/routes/server/index.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect, type LoaderFunctionArgs } from 'react-router'; 2 | import { Body, Injectable, OnModuleInit, Query, Req } from '@nestjs/common'; 3 | import { Loader, Action, ReactRouterArgs, useServer } from 'nest-react-router'; 4 | import { AppService } from '~/modules/app/app.service'; 5 | import { LoginDto } from '~/modules/app/dto/login.dto'; 6 | import { Test } from '~/common/test.decorator'; 7 | import { Client, ClientProxy, Transport } from '@nestjs/microservices'; 8 | import { lastValueFrom } from 'rxjs'; 9 | 10 | @Injectable() 11 | export class IndexBackend implements OnModuleInit { 12 | @Client({ 13 | transport: Transport.TCP, 14 | options: { port: 3001 }, 15 | }) 16 | private microService: ClientProxy; 17 | private enableMicroService: boolean; 18 | 19 | constructor(private readonly appService: AppService) { 20 | this.enableMicroService = process.env.ENABLE_MICRO_SERVICE === 'true'; 21 | } 22 | 23 | async onModuleInit() { 24 | if (this.enableMicroService) { 25 | try { 26 | await this.microService.connect(); 27 | console.log('Connected to microservice'); 28 | } catch (error) { 29 | console.error('Failed to connect to microservice:', error); 30 | } 31 | } else { 32 | console.log('Microservice is disabled.'); 33 | } 34 | } 35 | 36 | @Loader() 37 | loader( 38 | @ReactRouterArgs() args: LoaderFunctionArgs, 39 | @Req() req: Request, 40 | @Test() test: string, 41 | @Query('name') name?: string, 42 | ) { 43 | return { 44 | message: this.appService.getHello(name) + ', now: ' + Date.now(), 45 | loadData: new Promise((res) => { 46 | setTimeout(() => res(`loaded success, now: ${Date.now()}`), 2000); 47 | }), 48 | sumFromMicroService: this.enableMicroService 49 | ? lastValueFrom( 50 | this.microService.send('sum', [1, 2, 3, 4, 5, 6, 8]), 51 | ) 52 | : Promise.resolve('Try using script: yarn start:dev:microservice'), 53 | }; 54 | } 55 | 56 | @Action() 57 | action(@Body() body: LoginDto) { 58 | if (this.appService.login(body)) { 59 | return redirect('/foo'); 60 | } 61 | return { msg: 'The username or password is incorrect' }; 62 | } 63 | 64 | @Action.Patch() 65 | patch() { 66 | return '[patch]: returned by server side'; 67 | } 68 | 69 | @Action.Delete() 70 | delete() { 71 | return '[delete]: returned by server side'; 72 | } 73 | } 74 | 75 | export const useIndexServer = (args: LoaderFunctionArgs) => 76 | useServer(IndexBackend)(args); 77 | -------------------------------------------------------------------------------- /example/main/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /example/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ESNext", 5 | "moduleResolution": "bundler", 6 | "noEmitOnError": true, 7 | "noImplicitAny": true, 8 | "esModuleInterop": true, 9 | "isolatedModules": true, 10 | "jsx": "react-jsx", 11 | "sourceMap": true, 12 | "resolveJsonModule": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "allowSyntheticDefaultImports": true, 15 | "incremental": true, 16 | "strict": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "~/*": ["./src/*"] 20 | }, 21 | "lib": ["ESNext", "esnext.asynciterable", "DOM", "DOM.Iterable"], 22 | "emitDecoratorMetadata": true, 23 | "experimentalDecorators": true, 24 | "allowJs": true, 25 | "noEmit": true, 26 | "strictPropertyInitialization": false, 27 | "skipLibCheck": true 28 | }, 29 | "include": ["src/**/*", ".react-router/types/**/*", "vite.config.mts"], 30 | "exclude": ["build", "scripts", "dist", "test", "node_modules"] 31 | } 32 | -------------------------------------------------------------------------------- /example/main/tsconfig.nest.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "jsx": "react-jsx", 10 | "target": "es2017", 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "baseUrl": "./", 14 | "incremental": true, 15 | "skipLibCheck": true, 16 | "strictNullChecks": false, 17 | "noImplicitAny": false, 18 | "resolveJsonModule": true, 19 | "strictBindCallApply": false, 20 | "forceConsistentCasingInFileNames": false, 21 | "noFallthroughCasesInSwitch": false, 22 | "paths": { 23 | "~/*": ["./src/*"] 24 | } 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "test", 29 | "dist", 30 | "**/*spec.ts", 31 | "src/**/*.tsx", 32 | "src/routes.ts", 33 | "*.ts", 34 | "*.mts" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /example/main/vite.config.mts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from '@react-router/dev/vite'; 2 | import { UserConfig, defineConfig } from 'vite'; 3 | import tsconfigPaths from 'vite-tsconfig-paths'; 4 | import commonjs from 'vite-plugin-commonjs'; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | reactRouter(), 9 | tsconfigPaths(), 10 | commonjs(), 11 | ] as UserConfig['plugins'], 12 | ssr: { 13 | external: ['rxjs'], 14 | }, 15 | build: { 16 | rollupOptions: { 17 | external: ['nest-react-router'], 18 | }, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /example/microservices/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /example/microservices/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # dotenv environment variable files 39 | .env 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | .env.local 44 | 45 | # temp directory 46 | .temp 47 | .tmp 48 | 49 | # Runtime data 50 | pids 51 | *.pid 52 | *.seed 53 | *.pid.lock 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 57 | -------------------------------------------------------------------------------- /example/microservices/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /example/microservices/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | Donate us 19 | Support us 20 | Follow us on Twitter 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Project setup 30 | 31 | ```bash 32 | $ yarn install 33 | ``` 34 | 35 | ## Compile and run the project 36 | 37 | ```bash 38 | # development 39 | $ yarn run start 40 | 41 | # watch mode 42 | $ yarn run start:dev 43 | 44 | # production mode 45 | $ yarn run start:prod 46 | ``` 47 | 48 | ## Run tests 49 | 50 | ```bash 51 | # unit tests 52 | $ yarn run test 53 | 54 | # e2e tests 55 | $ yarn run test:e2e 56 | 57 | # test coverage 58 | $ yarn run test:cov 59 | ``` 60 | 61 | ## Deployment 62 | 63 | When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information. 64 | 65 | If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps: 66 | 67 | ```bash 68 | $ yarn install -g mau 69 | $ mau deploy 70 | ``` 71 | 72 | With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure. 73 | 74 | ## Resources 75 | 76 | Check out a few resources that may come in handy when working with NestJS: 77 | 78 | - Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. 79 | - For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). 80 | - To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). 81 | - Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks. 82 | - Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). 83 | - Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). 84 | - To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). 85 | - Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). 86 | 87 | ## Support 88 | 89 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 90 | 91 | ## Stay in touch 92 | 93 | - Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec) 94 | - Website - [https://nestjs.com](https://nestjs.com/) 95 | - Twitter - [@nestframework](https://twitter.com/nestframework) 96 | 97 | ## License 98 | 99 | Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). 100 | -------------------------------------------------------------------------------- /example/microservices/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example/microservices/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-microservices", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^10.0.0", 24 | "@nestjs/core": "^10.0.0", 25 | "@nestjs/microservices": "^10.4.9", 26 | "@nestjs/platform-express": "^10.0.0", 27 | "reflect-metadata": "^0.2.0", 28 | "rxjs": "^7.8.1" 29 | }, 30 | "devDependencies": { 31 | "@nestjs/cli": "^10.0.0", 32 | "@nestjs/schematics": "^10.0.0", 33 | "@nestjs/testing": "^10.0.0", 34 | "@types/express": "^5.0.0", 35 | "@types/jest": "^29.5.2", 36 | "@types/node": "^20.3.1", 37 | "@types/supertest": "^6.0.0", 38 | "@typescript-eslint/eslint-plugin": "^8.0.0", 39 | "@typescript-eslint/parser": "^8.0.0", 40 | "eslint": "^8.0.0", 41 | "eslint-config-prettier": "^9.0.0", 42 | "eslint-plugin-prettier": "^5.0.0", 43 | "jest": "^29.5.0", 44 | "prettier": "^3.0.0", 45 | "source-map-support": "^0.5.21", 46 | "supertest": "^7.0.0", 47 | "ts-jest": "^29.1.0", 48 | "ts-loader": "^9.4.3", 49 | "ts-node": "^10.9.1", 50 | "tsconfig-paths": "^4.2.0", 51 | "typescript": "^5.1.3" 52 | }, 53 | "jest": { 54 | "moduleFileExtensions": [ 55 | "js", 56 | "json", 57 | "ts" 58 | ], 59 | "rootDir": "src", 60 | "testRegex": ".*\\.spec\\.ts$", 61 | "transform": { 62 | "^.+\\.(t|j)s$": "ts-jest" 63 | }, 64 | "collectCoverageFrom": [ 65 | "**/*.(t|j)s" 66 | ], 67 | "coverageDirectory": "../coverage", 68 | "testEnvironment": "node" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /example/microservices/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | import { MessagePattern } from '@nestjs/microservices'; 4 | 5 | @Controller() 6 | export class AppController { 7 | constructor(private readonly appService: AppService) {} 8 | 9 | @Get() 10 | getHello(): string { 11 | return this.appService.getHello(); 12 | } 13 | 14 | @MessagePattern('sum') 15 | sum(numArr: number[]): number { 16 | return this.appService.sum(numArr); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/microservices/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | @Module({ 6 | imports: [], 7 | controllers: [AppController], 8 | providers: [AppService], 9 | }) 10 | export class AppModule {} 11 | -------------------------------------------------------------------------------- /example/microservices/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | 9 | sum(numArr: number[]): number { 10 | console.log('micro-service recive:', numArr); 11 | return numArr.reduce((total, item) => total + item, 0); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/microservices/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { MicroserviceOptions, Transport } from '@nestjs/microservices'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.createMicroservice( 7 | AppModule, 8 | { 9 | transport: Transport.TCP, 10 | options: { 11 | port: 3001, 12 | }, 13 | }, 14 | ); 15 | await app.listen(); 16 | } 17 | bootstrap(); 18 | -------------------------------------------------------------------------------- /example/microservices/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /example/microservices/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-react-router-monorepo", 3 | "version": "1.0.7", 4 | "description": "Using react-router in nestjs", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "private": true, 8 | "sideEffects": false, 9 | "scripts": { 10 | "start:dev": "cd example/main && npm run start:dev", 11 | "start:dev2": "cross-env ENABLE_MICRO_SERVICE=true npm-run-all --parallel start:microservice start:dev", 12 | "start:microservice": "cd example/microservices && npm run start", 13 | "build": "rollup -c --bundleConfigAsCjs", 14 | "publish": "npm-run-all --parallel publish:router publish:remix", 15 | "publish:router": "cd packages/nest-react-router && npm publish --provenance --access public", 16 | "publish:remix": "cd packages/nestjs-remix && npm publish --provenance --access public", 17 | "postinstall": "npm run build", 18 | "prepare": "husky", 19 | "synchronous-version": "node ./synchronous-version.js" 20 | }, 21 | "workspaces": [ 22 | "example/main", 23 | "example/microservices", 24 | "packages/nest-react-router" 25 | ], 26 | "lint-staged": { 27 | "*": "npm run synchronous-version" 28 | }, 29 | "keywords": [ 30 | "nest", 31 | "nestjs", 32 | "remix", 33 | "react-router" 34 | ], 35 | "homepage": "https://github.com/JinYuSha0/nest-remix", 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/JinYuSha0/nest-remix.git" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/JinYuSha0/nest-remix/issues" 42 | }, 43 | "author": "JinYuSha0", 44 | "license": "ISC", 45 | "publishConfig": { 46 | "registry": "https://registry.npmjs.org/" 47 | }, 48 | "githubPackages": { 49 | "registry": "https://npm.pkg.github.com/" 50 | }, 51 | "dependencies": { 52 | "@grpc/proto-loader": "^0.7.13", 53 | "@nestjs/common": "^10.4.8", 54 | "@nestjs/core": "^10.4.8", 55 | "@nestjs/microservices": "^10.4.9", 56 | "@nestjs/serve-static": "^4.0.2", 57 | "@nestjs/testing": "^10.4.8", 58 | "@react-router/express": "^7.0.0", 59 | "@react-router/node": "^7.0.0", 60 | "@react-router/serve": "^7.0.0", 61 | "await-import-dont-compile": "^0.0.1", 62 | "body-parser": "^1.20.3", 63 | "express-serve-static-core": "^0.1.1", 64 | "object-to-formdata": "^4.5.1", 65 | "react-router": "^7.0.0", 66 | "reflect-metadata": "^0.2.2" 67 | }, 68 | "devDependencies": { 69 | "@nestjs/cli": "^10.4.8", 70 | "@rollup/plugin-commonjs": "^28.0.1", 71 | "@rollup/plugin-node-resolve": "^15.3.0", 72 | "@testing-library/react": "^15.0.7", 73 | "@types/node": "^20.12.12", 74 | "concurrently": "^8.2.2", 75 | "cross-env": "^7.0.3", 76 | "husky": "^9.1.7", 77 | "lint-staged": "^15.2.10", 78 | "npm-run-all": "^4.1.5", 79 | "rollup": "^4.27.4", 80 | "rollup-plugin-typescript2": "^0.36.0", 81 | "typescript": "^5.2.2", 82 | "vite": "^6.0.1" 83 | }, 84 | "peerDependencies": { 85 | "@nestjs/common": "^10.3.8", 86 | "@nestjs/core": "^10.3.8", 87 | "@nestjs/serve-static": "^4.0.2", 88 | "@react-router/express": "^7.0.0", 89 | "@react-router/node": "^7.0.0", 90 | "@react-router/serve": "^7.0.0", 91 | "express-serve-static-core": "^0.1.1", 92 | "react-router": "^7.0.0", 93 | "reflect-metadata": "^0.2.2" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/nest-react-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-react-router", 3 | "version": "1.0.7", 4 | "description": "Using react-router in nest", 5 | "main": "server.js", 6 | "types": "index.d.ts", 7 | "exports": { 8 | ".": { 9 | "node": { 10 | "types": "./index.d.ts", 11 | "development": { 12 | "module-sync": "./server.js", 13 | "default": "./server.js" 14 | }, 15 | "module-sync": "./server.js", 16 | "default": "./server.js" 17 | }, 18 | "import": { 19 | "types": "./index.d.ts", 20 | "development": "./server.js", 21 | "default": "./server.js" 22 | }, 23 | "default": { 24 | "types": "./index.d.ts", 25 | "development": "./server.js", 26 | "default": "./server.js" 27 | } 28 | }, 29 | "./server": { 30 | "node": { 31 | "types": "./index.d.ts", 32 | "development": { 33 | "module-sync": "./server.js", 34 | "default": "./server.js" 35 | }, 36 | "module-sync": "./server.js", 37 | "default": "./server.js" 38 | }, 39 | "import": { 40 | "types": "./index.d.ts", 41 | "development": "./server.js", 42 | "default": "./server.js" 43 | }, 44 | "default": { 45 | "types": "./index.d.ts", 46 | "development": "./server.js", 47 | "default": "./server.js" 48 | } 49 | }, 50 | "./client": { 51 | "node": { 52 | "types": "./client/index.d.ts", 53 | "development": { 54 | "module-sync": "./client.mjs", 55 | "default": "./client.mjs" 56 | }, 57 | "module-sync": "./client.mjs", 58 | "default": "./client.js" 59 | }, 60 | "import": { 61 | "types": "./client/index.d.ts", 62 | "development": "./client.mjs", 63 | "default": "./client.mjs" 64 | }, 65 | "default": { 66 | "types": "./client/index.d.ts", 67 | "development": "./client.mjs", 68 | "default": "./client.js" 69 | } 70 | } 71 | }, 72 | "imports": { 73 | "#react-router": { 74 | "node": "react-router", 75 | "development": "./node_modules/react-router/dist/development/index.js", 76 | "default": "./node_modules/react-router/dist/production/index.js" 77 | } 78 | }, 79 | "keywords": [ 80 | "nest", 81 | "nestjs", 82 | "react", 83 | "react-router" 84 | ], 85 | "homepage": "https://github.com/JinYuSha0/nest-react-router", 86 | "repository": { 87 | "type": "git", 88 | "url": "https://github.com/JinYuSha0/nest-react-router.git" 89 | }, 90 | "bugs": { 91 | "url": "https://github.com/JinYuSha0/nest-react-router/issues" 92 | }, 93 | "author": "JinYuSha0", 94 | "license": "ISC", 95 | "sideEffects": false, 96 | "dependencies": { 97 | "@nestjs/common": "^10.3.8", 98 | "@nestjs/core": "^10.3.8", 99 | "@nestjs/serve-static": "^4.0.2", 100 | "@react-router/express": "^7.0.0", 101 | "@react-router/node": "^7.0.0", 102 | "@react-router/serve": "^7.0.0", 103 | "await-import-dont-compile": "^0.0.1", 104 | "express-serve-static-core": "^0.1.1", 105 | "object-to-formdata": "^4.5.1", 106 | "react-router": "^7.0.0", 107 | "reflect-metadata": "^0.2.2" 108 | }, 109 | "devDependencies": { 110 | "@nestjs/cli": "^10.3.2", 111 | "@types/node": "^20.12.12", 112 | "typescript": "^5.2.2", 113 | "vite": "^5.4.10" 114 | }, 115 | "peerDependencies": { 116 | "@nestjs/common": "^10.3.8", 117 | "@nestjs/core": "^10.3.8", 118 | "@nestjs/serve-static": "^4.0.2", 119 | "@react-router/express": "^7.0.0", 120 | "@react-router/node": "^7.0.0", 121 | "@react-router/serve": "^7.0.0", 122 | "express-serve-static-core": "^0.1.1", 123 | "react-router": "^7.0.0", 124 | "reflect-metadata": "^0.2.2", 125 | "vite": "^5.4.10" 126 | } 127 | } -------------------------------------------------------------------------------- /packages/nestjs-remix/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-remix", 3 | "version": "1.0.7", 4 | "description": "Using remix in nestjs", 5 | "main": "server.cjs.js", 6 | "types": "index.d.ts", 7 | "exports": { 8 | ".": { 9 | "node": { 10 | "types": "./index.d.ts", 11 | "development": { 12 | "module-sync": "./server.js", 13 | "default": "./server.js" 14 | }, 15 | "module-sync": "./server.js", 16 | "default": "./server.js" 17 | }, 18 | "import": { 19 | "types": "./index.d.ts", 20 | "development": "./server.js", 21 | "default": "./server.js" 22 | }, 23 | "default": { 24 | "types": "./index.d.ts", 25 | "development": "./server.js", 26 | "default": "./server.js" 27 | } 28 | }, 29 | "./server": { 30 | "node": { 31 | "types": "./index.d.ts", 32 | "development": { 33 | "module-sync": "./server.js", 34 | "default": "./server.js" 35 | }, 36 | "module-sync": "./server.js", 37 | "default": "./server.js" 38 | }, 39 | "import": { 40 | "types": "./index.d.ts", 41 | "development": "./server.js", 42 | "default": "./server.js" 43 | }, 44 | "default": { 45 | "types": "./index.d.ts", 46 | "development": "./server.js", 47 | "default": "./server.js" 48 | } 49 | }, 50 | "./client": { 51 | "node": { 52 | "types": "./client/index.d.ts", 53 | "development": { 54 | "module-sync": "./client.mjs", 55 | "default": "./client.mjs" 56 | }, 57 | "module-sync": "./client.mjs", 58 | "default": "./client.js" 59 | }, 60 | "import": { 61 | "types": "./client/index.d.ts", 62 | "development": "./client.mjs", 63 | "default": "./client.mjs" 64 | }, 65 | "default": { 66 | "types": "./client/index.d.ts", 67 | "development": "./client.mjs", 68 | "default": "./client.js" 69 | } 70 | } 71 | }, 72 | "keywords": [ 73 | "nestjs", 74 | "remix" 75 | ], 76 | "homepage": "https://github.com/JinYuSha0/nest-react-router", 77 | "repository": { 78 | "type": "git", 79 | "url": "https://github.com/JinYuSha0/nest-react-router.git" 80 | }, 81 | "bugs": { 82 | "url": "https://github.com/JinYuSha0/nest-react-router/issues" 83 | }, 84 | "author": "JinYuSha0", 85 | "license": "ISC", 86 | "sideEffects": false, 87 | "dependencies": { 88 | "@nestjs/common": "^10.3.8", 89 | "@nestjs/core": "^10.3.8", 90 | "@nestjs/serve-static": "^4.0.2", 91 | "@react-router/express": "^7.0.0", 92 | "@react-router/node": "^7.0.0", 93 | "@react-router/serve": "^7.0.0", 94 | "await-import-dont-compile": "^0.0.1", 95 | "express-serve-static-core": "^0.1.1", 96 | "object-to-formdata": "^4.5.1", 97 | "react-router": "^7.0.0", 98 | "reflect-metadata": "^0.2.2" 99 | }, 100 | "devDependencies": { 101 | "@nestjs/cli": "^10.3.2", 102 | "@types/node": "^20.12.12", 103 | "typescript": "^5.2.2", 104 | "vite": "^5.4.10" 105 | }, 106 | "peerDependencies": { 107 | "@nestjs/common": "^10.3.8", 108 | "@nestjs/core": "^10.3.8", 109 | "@nestjs/serve-static": "^4.0.2", 110 | "@react-router/express": "^7.0.0", 111 | "@react-router/node": "^7.0.0", 112 | "@react-router/serve": "^7.0.0", 113 | "express-serve-static-core": "^0.1.1", 114 | "react-router": "^7.0.0", 115 | "reflect-metadata": "^0.2.2", 116 | "vite": "^5.4.10" 117 | } 118 | } -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import typescript from "rollup-plugin-typescript2"; 3 | import resolve from "@rollup/plugin-node-resolve"; 4 | import commonjs from "@rollup/plugin-commonjs"; 5 | 6 | export default [ 7 | // nest-react-router 8 | { 9 | input: path.resolve(__dirname, "src/index.ts"), 10 | output: { 11 | file: path.resolve(__dirname, "packages/nest-react-router/server.js"), 12 | format: "cjs", 13 | sourcemap: true, 14 | }, 15 | plugins: [resolve(), commonjs(), typescript()], 16 | external: (id) => /node_modules/.test(id), 17 | }, 18 | { 19 | input: path.resolve(__dirname, "src/client/index.ts"), 20 | output: { 21 | file: path.resolve(__dirname, "packages/nest-react-router/client.mjs"), 22 | format: "esm", 23 | sourcemap: true, 24 | }, 25 | plugins: [resolve(), commonjs(), typescript()], 26 | external: (id) => /node_modules/.test(id), 27 | }, 28 | // nestjs-remix 29 | { 30 | input: path.resolve(__dirname, "src/index.ts"), 31 | output: { 32 | file: path.resolve(__dirname, "packages/nestjs-remix/server.js"), 33 | format: "cjs", 34 | sourcemap: true, 35 | }, 36 | plugins: [resolve(), commonjs(), typescript()], 37 | external: (id) => /node_modules/.test(id), 38 | }, 39 | { 40 | input: path.resolve(__dirname, "src/client/index.ts"), 41 | output: { 42 | file: path.resolve(__dirname, "packages/nestjs-remix/client.mjs"), 43 | format: "esm", 44 | sourcemap: true, 45 | }, 46 | plugins: [resolve(), commonjs(), typescript()], 47 | external: (id) => /node_modules/.test(id), 48 | }, 49 | ]; 50 | -------------------------------------------------------------------------------- /src/client/helper.ts: -------------------------------------------------------------------------------- 1 | export function noop(): any {} 2 | 3 | export function deferred() { 4 | let resolve: (value: T | PromiseLike) => void = noop; 5 | let reject: (reason?: any) => void = noop; 6 | const promise = new Promise((res, rej) => { 7 | resolve = res; 8 | reject = rej; 9 | }); 10 | return { 11 | promise, 12 | resolve, 13 | reject, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useLoaderData as useReactRouterLoaderData, 3 | useActionData as useReactRouterActionData, 4 | } from "react-router"; 5 | export { usePromiseSubmit } from "./usePromiseSubmit"; 6 | 7 | type ExcludeResponse = T extends Response ? never : T; 8 | 9 | export type AnyFunction = (...args: any) => any; 10 | 11 | export interface ReactRouterError { 12 | data: { 13 | message?: string; 14 | code?: number; 15 | success?: false; 16 | }; 17 | internal: boolean; 18 | status: number; 19 | statusText: string; 20 | } 21 | 22 | export type ReactRouterReturnType< 23 | T, 24 | P extends keyof T, 25 | > = T[P] extends AnyFunction 26 | ? ExcludeResponse>> 27 | : never; 28 | 29 | export type LoaderReturnType< 30 | T extends { loader?: AnyFunction } & object, 31 | P extends keyof T = "loader", 32 | > = ReactRouterReturnType; 33 | 34 | export type ActionReturnType< 35 | T extends { action?: AnyFunction } & object, 36 | P extends keyof T = "action", 37 | > = ReactRouterReturnType; 38 | 39 | export const useLoaderData = < 40 | T extends { loader?: AnyFunction } & object, 41 | P extends keyof T = "loader", 42 | >( 43 | ...args: any 44 | ): LoaderReturnType => useReactRouterLoaderData.apply(null, args); 45 | 46 | export const useActionData = < 47 | T extends { action?: AnyFunction } & object, 48 | P extends keyof T = "action", 49 | >( 50 | ...args: any 51 | ): ActionReturnType => useReactRouterActionData.apply(null, args); 52 | -------------------------------------------------------------------------------- /src/client/usePromiseSubmit.ts: -------------------------------------------------------------------------------- 1 | import type { ActionReturnType, AnyFunction } from "./index"; 2 | import { useEffect, useRef, useState, useCallback } from "react"; 3 | import { useActionData, useSubmit } from "react-router"; 4 | import { deferred } from "./helper"; 5 | import { serialize } from "object-to-formdata"; 6 | 7 | type Options = { 8 | delay?: number; 9 | }; 10 | 11 | export function usePromiseSubmit< 12 | T extends { action?: AnyFunction } & object, 13 | K extends keyof T = "action", 14 | P = ActionReturnType, 15 | >( 16 | options?: Options 17 | ): [ 18 | (...args: Parameters>) => Promise

, 19 | boolean, 20 | ] { 21 | const { delay = 0 } = options ?? {}; 22 | const submit = useSubmit(); 23 | const actionData = useActionData

(); 24 | const $deferred = useRef(deferred

()); 25 | const nextCanActiveTs = useRef(); 26 | const [loading, setLoading] = useState(false); 27 | const _submit = useCallback( 28 | (...args: Parameters): Promise

=> { 29 | if (nextCanActiveTs.current && Date.now() < nextCanActiveTs.current) { 30 | return Promise.reject(); 31 | } 32 | setLoading(true); 33 | nextCanActiveTs.current = Date.now() + delay; 34 | if (!(args[0] instanceof FormData)) { 35 | args[0] = serialize(args[0], { 36 | indices: true, 37 | noFilesWithArrayNotation: true, 38 | }); 39 | } 40 | submit.apply(null, args); 41 | return $deferred.current.promise; 42 | }, 43 | [submit] 44 | ); 45 | useEffect(() => { 46 | if (actionData) { 47 | function resolve() { 48 | $deferred.current.resolve(actionData as P); 49 | $deferred.current = deferred(); 50 | setLoading(false); 51 | } 52 | if (nextCanActiveTs.current && Date.now() < nextCanActiveTs.current) { 53 | setTimeout(resolve, nextCanActiveTs.current - Date.now()); 54 | } else { 55 | resolve(); 56 | } 57 | } 58 | }, [actionData]); 59 | return [_submit, loading]; 60 | } 61 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { AppLoadContext, Params } from "react-router"; 2 | import type { LoaderFunctionArgs, ActionFunctionArgs } from "react-router"; 3 | import type { RemixService } from "server/remix.service"; 4 | import type { ServeStaticOptions } from "@nestjs/platform-express/interfaces/serve-static-options.interface"; 5 | import type * as core from "express-serve-static-core"; 6 | import path from "path"; 7 | 8 | export { 9 | Loader, 10 | Action, 11 | useAction, 12 | useLoader, 13 | useServer, 14 | ReactRouterArgs, 15 | ReactRouterArgs as RemixArgs, 16 | resolveReactRouterServices, 17 | resolveReactRouterServices as resolveRemixServices, 18 | startNestReactRouter, 19 | startNestReactRouter as startNestRemix, 20 | type ReactRouterError, 21 | ReactRouterException, 22 | type ReactRouterError as RemixError, 23 | ReactRouterException as RemixException, 24 | } from "./server"; 25 | 26 | export interface ReactRouterLoadContext extends AppLoadContext { 27 | moduleKey: string; 28 | moduleRef: RemixService; 29 | req: Request; 30 | res: Response; 31 | next: NextFunction; 32 | } 33 | 34 | export type ReactRouterConfig = { 35 | clientDir: string; 36 | serverFile: string; 37 | clientFileOptions: ServeStaticOptions; 38 | }; 39 | 40 | export const defaultRemixConfig: ReactRouterConfig = { 41 | clientDir: path.join(process.cwd(), "/build/client"), 42 | serverFile: path.join(process.cwd(), "/build/server/index.mjs"), 43 | clientFileOptions: { immutable: true, maxAge: "1d" }, 44 | }; 45 | 46 | declare global { 47 | namespace Express { 48 | interface Request { 49 | handleByReactRouter?: boolean; 50 | reactRouterArgs?: LoaderFunctionArgs | ActionFunctionArgs; 51 | reactRouterParams?: Params; 52 | } 53 | } 54 | 55 | interface Request extends core.Request {} 56 | 57 | interface Response extends core.Response {} 58 | 59 | type NextFunction = core.NextFunction; 60 | } 61 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | export { resolveReactRouterServices } from "./remix.resolve.services"; 2 | export { Loader, Action, ReactRouterArgs } from "./remix.decorator"; 3 | export { 4 | startNestReactRouter, 5 | useAction, 6 | useLoader, 7 | useServer, 8 | } from "./remix.core"; 9 | export { 10 | type ReactRouterError, 11 | ReactRouterException, 12 | } from "./remix.exceptions"; 13 | -------------------------------------------------------------------------------- /src/server/remix.constant.ts: -------------------------------------------------------------------------------- 1 | export const ReactRouterParamtypes = { 2 | REACT_ROUTER_ARGS: Number.MAX_SAFE_INTEGER - 888, 3 | }; 4 | -------------------------------------------------------------------------------- /src/server/remix.core.ts: -------------------------------------------------------------------------------- 1 | import type { NestApplication } from "@nestjs/core"; 2 | import type { Type } from "@nestjs/common"; 3 | import type { 4 | LoaderFunction, 5 | ActionFunction, 6 | LoaderFunctionArgs, 7 | ActionFunctionArgs, 8 | } from "react-router"; 9 | import type { NestContainer } from "@nestjs/core/injector/container"; 10 | import type { ReactRouterLoadContext, ReactRouterConfig } from "../index"; 11 | import type { ViteDevServer } from "vite"; 12 | import { ExternalContextCreator } from "@nestjs/core"; 13 | import { RequestMethod } from "@nestjs/common/enums"; 14 | import { IS_DEV, isConstructor } from "./remix.helper"; 15 | import { remixMiddleware } from "./remix.middleware"; 16 | import { defaultRemixConfig, ReactRouterException } from "../index"; 17 | import { ROUTE_ARGS_METADATA } from "@nestjs/common/constants"; 18 | import { RemixRouteParamsFactory } from "./remix.route.params.factory"; 19 | import bodyParser from "body-parser"; 20 | 21 | export enum RemixProperty { 22 | Loader = "Loader", 23 | ActionPost = "Action.Post", 24 | ActionPut = "Action.Put", 25 | ActionPatch = "Action.Patch", 26 | ActionDelete = "Action.Delete", 27 | ActionOptions = "Action.Options", 28 | ActionHead = "Action.Head", 29 | ActionSearch = "Action.Search", 30 | } 31 | 32 | export let viteDevServer: ViteDevServer; 33 | let remixExecutionContextCreator: ExternalContextCreator; 34 | const remixRouteParamsFactory = new RemixRouteParamsFactory(); 35 | 36 | const getProviderName = (type: Type | string) => 37 | typeof type === "string" ? type : type.name; 38 | 39 | const getPropertyNameByRequest = ( 40 | request: Request 41 | ): [RemixProperty, RequestMethod] => { 42 | switch (request.method) { 43 | case "GET": 44 | return [RemixProperty.Loader, RequestMethod.GET]; 45 | case "POST": 46 | return [RemixProperty.ActionPost, RequestMethod.POST]; 47 | case "PUT": 48 | return [RemixProperty.ActionPut, RequestMethod.PUT]; 49 | case "PATCH": 50 | return [RemixProperty.ActionPatch, RequestMethod.PATCH]; 51 | case "DELETE": 52 | return [RemixProperty.ActionDelete, RequestMethod.DELETE]; 53 | case "OPTIONS": 54 | return [RemixProperty.ActionOptions, RequestMethod.OPTIONS]; 55 | case "HEAD": 56 | return [RemixProperty.ActionHead, RequestMethod.HEAD]; 57 | case "SEARCH": 58 | return [RemixProperty.ActionSearch, RequestMethod.SEARCH]; 59 | } 60 | }; 61 | 62 | type RemixDescriptor = Partial<{ 63 | [key in RemixProperty]: string; 64 | }>; 65 | 66 | export const setRemixTypeDescriptor = ( 67 | type: Type, 68 | methodName: string, 69 | property: RemixProperty 70 | ) => 71 | Reflect.defineMetadata( 72 | "__RemixTypeDescriptor__", 73 | { 74 | ...getRemixTypeDescriptor(type), 75 | [property]: methodName, 76 | }, 77 | type 78 | ); 79 | 80 | const getRemixTypeDescriptor = (type: Type): RemixDescriptor | undefined => 81 | Reflect.getMetadata("__RemixTypeDescriptor__", type); 82 | 83 | type ReturnFunction = LoaderFunction | ActionFunction; 84 | 85 | const useDecorator = ( 86 | typeOrDescriptor: Type | RemixDescriptor, 87 | typeName?: string 88 | ): ReturnFunction => { 89 | if (isConstructor(typeOrDescriptor)) { 90 | const descriptor = getRemixTypeDescriptor(typeOrDescriptor); 91 | return useDecorator(descriptor, typeName ?? typeOrDescriptor.name); 92 | } 93 | return async (args: LoaderFunctionArgs | ActionFunctionArgs) => { 94 | const { moduleRef, req, res, next } = 95 | args.context as ReactRouterLoadContext; 96 | req.reactRouterArgs = args; 97 | req.reactRouterParams = args.params; 98 | const providerName = getProviderName(typeName); 99 | const instance = moduleRef.get(providerName); 100 | const [requestProperty, requestMethod] = getPropertyNameByRequest( 101 | args.context.req as Request 102 | ); 103 | const methodName = typeOrDescriptor[requestProperty]; 104 | 105 | if (!methodName) { 106 | return new ReactRouterException( 107 | `No method found using @${requestProperty} decorator on class ${providerName}` 108 | ).toResponse(); 109 | } 110 | 111 | const _remixExecutionContextCreator: ExternalContextCreator = 112 | remixExecutionContextCreator ?? global.remixExecutionContext; 113 | 114 | const executionContext = await _remixExecutionContextCreator.create( 115 | instance, 116 | instance[methodName], 117 | methodName, 118 | ROUTE_ARGS_METADATA, 119 | remixRouteParamsFactory 120 | ); 121 | 122 | return executionContext(req, res, next); 123 | }; 124 | }; 125 | 126 | export const startNestReactRouter = async ( 127 | app: NestApplication, 128 | remixConfig: ReactRouterConfig = defaultRemixConfig 129 | ) => { 130 | // client static file middleware 131 | app.useStaticAssets(remixConfig.clientDir, remixConfig.clientFileOptions); 132 | 133 | // vite middleware (DEV only) 134 | if (IS_DEV) { 135 | if (!viteDevServer) { 136 | viteDevServer = await import("vite").then((vite) => 137 | vite.createServer({ 138 | server: { middlewareMode: true }, 139 | }) 140 | ); 141 | app.use(viteDevServer.middlewares); 142 | } 143 | } 144 | 145 | // remix middleware 146 | app.use( 147 | bodyParser.urlencoded({ extended: true }), 148 | await remixMiddleware(app, remixConfig) 149 | ); 150 | 151 | const container = (app as any).container as NestContainer; 152 | 153 | remixExecutionContextCreator = 154 | ExternalContextCreator.fromContainer(container); 155 | 156 | if (IS_DEV) { 157 | global.remixExecutionContext = remixExecutionContextCreator; 158 | } 159 | }; 160 | 161 | /** 162 | * @deprecated Use `useServer()` instead. 163 | */ 164 | export const useLoader = (type: Type) => useDecorator(type) as LoaderFunction; 165 | /** 166 | * @deprecated Use `useServer()` instead. 167 | */ 168 | export const useAction = (type: Type) => useDecorator(type) as ActionFunction; 169 | 170 | export const useServer = (type: Type) => 171 | useDecorator(type) as LoaderFunction | ActionFunction; 172 | -------------------------------------------------------------------------------- /src/server/remix.decorator.ts: -------------------------------------------------------------------------------- 1 | import type { ParamData } from "@nestjs/common"; 2 | import { assignMetadata } from "@nestjs/common"; 3 | import { ROUTE_ARGS_METADATA } from "@nestjs/common/constants"; 4 | import { RemixProperty, setRemixTypeDescriptor } from "./remix.core"; 5 | import { isConstructor } from "./remix.helper"; 6 | import { ReactRouterParamtypes } from "./remix.constant"; 7 | 8 | function createRouteParamDecorator(paramtype: number) { 9 | return (data?: ParamData): ParameterDecorator => 10 | (target, key, index) => { 11 | if (!key) return; 12 | const args = 13 | Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key) || {}; 14 | Reflect.defineMetadata( 15 | ROUTE_ARGS_METADATA, 16 | assignMetadata(args, paramtype, index, data), 17 | target.constructor, 18 | key 19 | ); 20 | }; 21 | } 22 | 23 | export const ReactRouterArgs = createRouteParamDecorator( 24 | ReactRouterParamtypes.REACT_ROUTER_ARGS 25 | ); 26 | 27 | function Decorator(...properties: RemixProperty[]) { 28 | return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { 29 | const type = target.constructor; 30 | if (!isConstructor(type)) { 31 | return; 32 | } 33 | for (const property of properties) { 34 | setRemixTypeDescriptor(type, propertyKey, property); 35 | } 36 | }; 37 | } 38 | 39 | export const Loader = () => Decorator(RemixProperty.Loader); 40 | export function Action() { 41 | return Decorator(RemixProperty.ActionPost); 42 | } 43 | Action.Post = () => Action; 44 | Action.Put = () => Decorator(RemixProperty.ActionPut); 45 | Action.Patch = () => Decorator(RemixProperty.ActionPatch); 46 | Action.Delete = () => Decorator(RemixProperty.ActionDelete); 47 | Action.Options = () => Decorator(RemixProperty.ActionOptions); 48 | Action.Head = () => Decorator(RemixProperty.ActionHead); 49 | Action.Search = () => Decorator(RemixProperty.ActionSearch); 50 | -------------------------------------------------------------------------------- /src/server/remix.exceptions.ts: -------------------------------------------------------------------------------- 1 | export interface ReactRouterError { 2 | data: { 3 | message?: string; 4 | code?: number; 5 | success?: false; 6 | }; 7 | internal: boolean; 8 | status: number; 9 | statusText: string; 10 | } 11 | 12 | export class ReactRouterException extends Error { 13 | message: string; 14 | 15 | code: number; 16 | 17 | constructor(message?: string, code?: number) { 18 | super(message); 19 | this.message = message ?? "Internal Server Error"; 20 | this.code = code ?? 500; 21 | } 22 | 23 | toResponse() { 24 | return new Response( 25 | JSON.stringify({ 26 | message: this.message, 27 | code: this.code, 28 | success: false, 29 | }), 30 | { 31 | status: this.code, 32 | statusText: this.message, 33 | headers: { 34 | "Content-Type": "application/json", 35 | }, 36 | } 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/server/remix.helper.ts: -------------------------------------------------------------------------------- 1 | import type { Type } from "@nestjs/common"; 2 | import { matchRoutes } from "react-router"; 3 | import { pathToFileURL } from "url"; 4 | import awaitImport from "await-import-dont-compile"; 5 | import { 6 | AgnosticRouteObject, 7 | ServerRoute, 8 | ServerRouteManifest, 9 | RouteMatch, 10 | } from "./remix.type"; 11 | 12 | export const IS_DEV = process.env.NODE_ENV !== "production"; 13 | 14 | export const isConstructor = (type: any): type is Type => { 15 | try { 16 | new type(); 17 | } catch (err) { 18 | if ((err as Error).message.indexOf("is not a constructor") > -1) { 19 | return false; 20 | } 21 | } 22 | return true; 23 | }; 24 | 25 | export const dynamicImport = async (filepath: string) => { 26 | let href = pathToFileURL(filepath).href; 27 | // dynamic import have a cache, will cause the remix page not to be updated 28 | if (IS_DEV) { 29 | href += `?d=${Date.now()}`; 30 | } 31 | return await awaitImport(href); 32 | }; 33 | 34 | export const delay = (ms: number) => { 35 | return new Promise((resolve) => { 36 | setTimeout(() => { 37 | resolve(); 38 | }, ms); 39 | }); 40 | }; 41 | 42 | function groupRoutesByParentId( 43 | manifest: ServerRouteManifest 44 | ): Record { 45 | let routes: Record = {}; 46 | 47 | Object.values(manifest).forEach((route) => { 48 | if (route) { 49 | let parentId = route.parentId || ""; 50 | if (!routes[parentId]) { 51 | routes[parentId] = []; 52 | } 53 | routes[parentId].push(route); 54 | } 55 | }); 56 | 57 | return routes; 58 | } 59 | 60 | export function createRoutes( 61 | manifest: ServerRouteManifest, 62 | parentId: string = "", 63 | routesByParentId: Record = groupRoutesByParentId( 64 | manifest 65 | ) 66 | ): ServerRoute[] { 67 | return (routesByParentId[parentId] || []).map((route) => ({ 68 | ...route, 69 | children: createRoutes(manifest, route.id, routesByParentId), 70 | })); 71 | } 72 | 73 | export function matchServerRoutes( 74 | routes: ServerRoute[], 75 | pathname: string, 76 | basename?: string 77 | ): RouteMatch[] | null { 78 | let matches = matchRoutes( 79 | routes as unknown as AgnosticRouteObject[], 80 | pathname, 81 | basename 82 | ); 83 | if (!matches) return null; 84 | 85 | return matches.map((match) => ({ 86 | params: match.params, 87 | pathname: match.pathname, 88 | route: match.route as unknown as ServerRoute, 89 | })); 90 | } 91 | -------------------------------------------------------------------------------- /src/server/remix.middleware.ts: -------------------------------------------------------------------------------- 1 | import type { ServerBuild } from "react-router"; 2 | import type { GetLoadContextFunction } from "@react-router/express"; 3 | import type { ReactRouterConfig } from "index"; 4 | import type { ServerRoute } from "./remix.type"; 5 | import { NestApplication } from "@nestjs/core"; 6 | import { viteDevServer } from "./remix.core"; 7 | import { createRequestHandler } from "@react-router/express"; 8 | import { RemixService } from "./remix.service"; 9 | import { 10 | createRoutes, 11 | dynamicImport, 12 | IS_DEV, 13 | matchServerRoutes, 14 | } from "./remix.helper"; 15 | 16 | const serverBuildId = "virtual:react-router/server-build"; 17 | 18 | export const remixMiddleware = async ( 19 | app: NestApplication, 20 | remixConfig: ReactRouterConfig 21 | ) => { 22 | let build: ServerBuild; 23 | let routes: ServerRoute[]; 24 | 25 | const moduleRef = app.get(RemixService); 26 | 27 | if (!IS_DEV) { 28 | build = (await dynamicImport(remixConfig.serverFile)) as ServerBuild; 29 | routes = createRoutes(build.routes); 30 | } 31 | 32 | return async (req: Request, res: Response, next: NextFunction) => { 33 | try { 34 | if (IS_DEV) { 35 | build = (await viteDevServer.ssrLoadModule( 36 | serverBuildId 37 | )) as ServerBuild; 38 | routes = createRoutes(build.routes); 39 | } 40 | 41 | const manifestUrl = `${build.basename ?? "/"}/__manifest`.replace( 42 | /\/+/g, 43 | "/" 44 | ); 45 | 46 | if ( 47 | matchServerRoutes(routes, req.url, build.basename) || 48 | req.path === manifestUrl || 49 | (req.path.endsWith(".data") && 50 | matchServerRoutes( 51 | routes, 52 | req.path.replace(/\.data$/, "").replace(/^\/_root$/, "/"), 53 | build.basename 54 | )) 55 | ) { 56 | // Mark this request to be handled by react-router 57 | req.handleByReactRouter = true; 58 | const getLoadContext: GetLoadContextFunction = (req) => { 59 | return { 60 | moduleRef, 61 | req, 62 | res, 63 | next, 64 | }; 65 | }; 66 | return createRequestHandler({ 67 | build, 68 | getLoadContext, 69 | })(req, res, next); 70 | } else { 71 | next(); 72 | } 73 | } catch (err) { 74 | next(err); 75 | } 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /src/server/remix.resolve.services.ts: -------------------------------------------------------------------------------- 1 | import { INJECTABLE_WATERMARK } from "@nestjs/common/constants"; 2 | import { RemixService } from "./remix.service"; 3 | import * as path from "path"; 4 | import fs from "fs"; 5 | 6 | // Related link: https://vite.dev/guide/features#dynamic-import 7 | function hackDynamicImport(file: string) { 8 | return new Function("require", "file", "return require(file)")(require, file); 9 | } 10 | 11 | function filterServices(module: { [x: string]: any }) { 12 | return Object.keys(module) 13 | .map((key) => module[key]) 14 | .filter((property) => Reflect.hasMetadata(INJECTABLE_WATERMARK, property)); 15 | } 16 | 17 | function scanRemixServerDir(remixServerDir: string) { 18 | const files = fs.readdirSync(remixServerDir); 19 | const allServices = []; 20 | for (const file of files) { 21 | if (path.parse(file).ext === ".js") { 22 | try { 23 | const module = hackDynamicImport(path.join(remixServerDir, file)); 24 | const services = filterServices(module); 25 | allServices.push(...services); 26 | } catch (err) { 27 | console.error(`dynamic import file ${file} failed`, err); 28 | } 29 | } 30 | } 31 | return allServices.map((useClass) => ({ 32 | provide: useClass.name, 33 | useClass, 34 | })); 35 | } 36 | 37 | export function resolveReactRouterServices(remixServerDir: string) { 38 | return [RemixService, ...scanRemixServerDir(remixServerDir)]; 39 | } 40 | -------------------------------------------------------------------------------- /src/server/remix.route.params.factory.ts: -------------------------------------------------------------------------------- 1 | import { RouteParamtypes } from "@nestjs/common/enums/route-paramtypes.enum"; 2 | import { ReactRouterParamtypes } from "./remix.constant"; 3 | 4 | export class RemixRouteParamsFactory { 5 | public exchangeKeyForValue< 6 | TRequest extends Record = any, 7 | TResponse = any, 8 | TResult = any, 9 | >( 10 | key: RouteParamtypes | string, 11 | data: string | object | any, 12 | [req, res, next]: [req: TRequest, res: TResponse, next: Function] 13 | ): TResult { 14 | switch (key) { 15 | case RouteParamtypes.NEXT: 16 | return next as any; 17 | case RouteParamtypes.REQUEST: 18 | return req as any; 19 | case RouteParamtypes.RESPONSE: 20 | return res as any; 21 | case RouteParamtypes.BODY: 22 | return data && req.body ? req.body[data] : req.body; 23 | case RouteParamtypes.RAW_BODY: 24 | return req.rawBody; 25 | case RouteParamtypes.PARAM: 26 | return data ? req.params[data] : req.params; 27 | case RouteParamtypes.HOST: 28 | const hosts = req.hosts || {}; 29 | return data ? hosts[data] : hosts; 30 | case RouteParamtypes.QUERY: 31 | return data ? req.query[data] : req.query; 32 | case RouteParamtypes.HEADERS: 33 | return data ? req.headers[data.toLowerCase()] : req.headers; 34 | case RouteParamtypes.SESSION: 35 | return req.session; 36 | case RouteParamtypes.FILE: 37 | return req[data || "file"]; 38 | case RouteParamtypes.FILES: 39 | return req.files; 40 | case RouteParamtypes.IP: 41 | return req.ip; 42 | case ReactRouterParamtypes.REACT_ROUTER_ARGS: 43 | return req.reactRouterArgs; 44 | default: 45 | return null; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/server/remix.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { ModuleRef } from "@nestjs/core"; 3 | 4 | @Injectable() 5 | export class RemixService { 6 | constructor(private readonly moduleRef: ModuleRef) {} 7 | 8 | public get(serviceName: string) { 9 | const instance = this.moduleRef.get(serviceName, { strict: false }); 10 | return instance; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/server/remix.type.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ServerBuild, 3 | ActionFunction, 4 | LazyRouteFunction, 5 | LoaderFunction, 6 | ShouldRevalidateFunction, 7 | Params, 8 | } from "react-router"; 9 | 10 | export type ServerRouteManifest = ServerBuild["routes"]; 11 | export type ServerRoute = ServerRouteManifest[string]; 12 | 13 | export type AgnosticBaseRouteObject = { 14 | caseSensitive?: boolean; 15 | path?: string; 16 | id?: string; 17 | loader?: LoaderFunction | boolean; 18 | action?: ActionFunction | boolean; 19 | hasErrorBoundary?: boolean; 20 | shouldRevalidate?: ShouldRevalidateFunction; 21 | handle?: any; 22 | lazy?: LazyRouteFunction; 23 | }; 24 | 25 | export type AgnosticIndexRouteObject = AgnosticBaseRouteObject & { 26 | children?: undefined; 27 | index: true; 28 | }; 29 | 30 | export type AgnosticNonIndexRouteObject = AgnosticBaseRouteObject & { 31 | children?: AgnosticRouteObject[]; 32 | index?: false; 33 | }; 34 | 35 | export type AgnosticRouteObject = 36 | | AgnosticIndexRouteObject 37 | | AgnosticNonIndexRouteObject; 38 | 39 | export type AgnosticDataIndexRouteObject = AgnosticIndexRouteObject & { 40 | id: string; 41 | }; 42 | 43 | export type AgnosticDataNonIndexRouteObject = AgnosticNonIndexRouteObject & { 44 | children?: AgnosticDataRouteObject[]; 45 | id: string; 46 | }; 47 | 48 | export type AgnosticDataRouteObject = 49 | | AgnosticDataIndexRouteObject 50 | | AgnosticDataNonIndexRouteObject; 51 | 52 | export interface RouteMatch { 53 | params: Params; 54 | pathname: string; 55 | route: Route; 56 | } 57 | -------------------------------------------------------------------------------- /synchronous-version.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const { execSync } = require("child_process"); 4 | 5 | const rootPackage = require("./package.json"); 6 | 7 | const packages = fs 8 | .readdirSync("./packages") 9 | .filter((dir) => fs.statSync(path.join("./packages", dir)).isDirectory()); 10 | 11 | packages.forEach((packageDir) => { 12 | const packagePath = path.join( 13 | __dirname, 14 | "./packages", 15 | packageDir, 16 | "package.json" 17 | ); 18 | 19 | console.log(packagePath); 20 | 21 | const packageJson = require(packagePath); 22 | 23 | packageJson.version = rootPackage.version; 24 | 25 | fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2)); 26 | 27 | execSync(`git add ${packagePath}`, { stdio: "inherit" }); 28 | }); 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDirs": ["./src"], 4 | "baseUrl": "./src", 5 | "module": "ESNext", 6 | "target": "ESNext", 7 | "lib": ["DOM", "DOM.Iterable", "ES2017"], 8 | "sourceMap": true, 9 | "allowJs": true, 10 | "moduleResolution": "bundler", 11 | "forceConsistentCasingInFileNames": true, 12 | "noImplicitReturns": false, 13 | "noImplicitThis": false, 14 | "noImplicitAny": false, 15 | "strictNullChecks": false, 16 | "noUnusedLocals": false, 17 | "declaration": true, 18 | "allowSyntheticDefaultImports": true, 19 | "experimentalDecorators": true, 20 | "emitDecoratorMetadata": true, 21 | "esModuleInterop": true, 22 | "strict": false, 23 | "skipLibCheck": true 24 | }, 25 | "include": ["src/**/*"], 26 | "exclude": [ 27 | "node_modules", 28 | "nestjs-remix", 29 | "nest-react-router", 30 | "test", 31 | "__test__", 32 | "**/__test__/**/*", 33 | "**/*spec.ts", 34 | "**/*it.ts", 35 | "**/*test.ts", 36 | "**/*e2e.ts", 37 | "**/*e2e-spec.ts" 38 | ] 39 | } 40 | --------------------------------------------------------------------------------