Skip to content

Commit b00922e

Browse files
authored
feat: router v7 plugin (#7106)
1 parent 5b26802 commit b00922e

File tree

30 files changed

+872
-73
lines changed

30 files changed

+872
-73
lines changed

.changeset/thirty-spies-dream.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@modern-js/plugin-router-v7': patch
3+
'@modern-js/plugin-data-loader': patch
4+
'@modern-js/runtime': patch
5+
'@modern-js/runtime-utils': patch
6+
---
7+
8+
feat: support plugin-router-v7
9+
feat: support plugin-router-v7

packages/cli/plugin-data-loader/src/cli/createRequest.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { UNSAFE_DeferredData as DeferredData } from '@modern-js/runtime-utils/remix-router';
1+
import type { DeferredData } from '@modern-js/runtime-utils/browser';
22
import { redirect } from '@modern-js/runtime-utils/router';
33
// Todo move this file to `runtime/` dir
44
import { compile } from 'path-to-regexp';
@@ -80,6 +80,7 @@ const handleNetworkErrorResponse = async (res: Response) => {
8080
};
8181

8282
export const createRequest = (routeId: string, method = 'get') => {
83+
const isRouterV7 = process.env._MODERN_ROUTER_VERSION === 'v7';
8384
return async ({
8485
params,
8586
request,
@@ -109,7 +110,8 @@ export const createRequest = (routeId: string, method = 'get') => {
109110
}
110111

111112
if (isDeferredResponse(res)) {
112-
return await parseDeferredReadableStream(res.body!);
113+
const deferredData = await parseDeferredReadableStream(res.body!);
114+
return isRouterV7 ? deferredData.data : deferredData;
113115
}
114116

115117
// some response error not from modern.js

packages/cli/plugin-data-loader/src/cli/data.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
*/
1010
import {
1111
AbortedDeferredError,
12-
UNSAFE_DeferredData as DeferredData,
13-
} from '@modern-js/runtime-utils/remix-router';
12+
DeferredData,
13+
} from '@modern-js/runtime-utils/browser';
1414

1515
const DEFERRED_VALUE_PLACEHOLDER_PREFIX = '__deferred_promise:';
1616
export async function parseDeferredReadableStream(

packages/cli/plugin-data-loader/src/runtime/index.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
type UNSAFE_DeferredData as DeferredData,
1010
createStaticHandler,
1111
isRouteErrorResponse,
12-
json,
1312
} from '@modern-js/runtime-utils/remix-router';
1413
import { matchEntry } from '@modern-js/runtime-utils/server';
1514
import { time } from '@modern-js/runtime-utils/time';
@@ -72,12 +71,14 @@ export const handleRequest: ServerLoaderBundle['handleRequest'] = async ({
7271
const end = time();
7372
const { reporter, loaderContext, monitors } = context;
7473
const headersData = parseHeaders(request);
74+
const activeDeferreds = new Map<string, DeferredData>();
7575

7676
return storage.run(
7777
{
7878
headers: headersData,
7979
monitors,
8080
request,
81+
activeDeferreds,
8182
},
8283
async () => {
8384
const routes = transformNestedRoutes(routesConfig);
@@ -102,9 +103,18 @@ export const handleRequest: ServerLoaderBundle['handleRequest'] = async ({
102103
response.headers as unknown as Headers,
103104
basename,
104105
);
105-
} else if (isPlainObject(response) && DEFERRED_SYMBOL in response) {
106-
const deferredData = response[DEFERRED_SYMBOL] as DeferredData;
106+
} else if (
107+
isPlainObject(response) &&
108+
(DEFERRED_SYMBOL in response || activeDeferreds.get(routeId))
109+
) {
110+
let deferredData;
111+
if (DEFERRED_SYMBOL in response) {
112+
deferredData = response[DEFERRED_SYMBOL] as DeferredData;
113+
} else {
114+
deferredData = activeDeferreds.get(routeId)!;
115+
}
107116
const body = createDeferredReadableStream(
117+
// @ts-ignore
108118
deferredData,
109119
request.signal,
110120
);
@@ -152,12 +162,16 @@ export const handleRequest: ServerLoaderBundle['handleRequest'] = async ({
152162
: new Error('Unexpected Server Error');
153163

154164
// Handle errors uniformly using the application/json
155-
response = json(serializeError(errorInstance), {
156-
status: 500,
157-
headers: {
158-
'X-Modernjs-Error': 'yes',
165+
response = new Response(
166+
JSON.stringify(serializeError(errorInstance)),
167+
{
168+
status: 500,
169+
headers: {
170+
'X-Modernjs-Error': 'yes',
171+
'Content-Type': 'application/json',
172+
},
159173
},
160-
});
174+
);
161175
}
162176
}
163177

packages/document/main-doc/docs/en/apis/app/hooks/src/routes.mdx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,73 @@ export default () => {
8888
`<Outlet>` is a new API in React Router 6. For details, see [Outlet](https://reactrouter.com/en/main/components/outlet#outlet).
8989

9090
:::
91+
92+
## Upgrading to React Router v7
93+
94+
React Router v7 reduces bundle size (approximately 15% smaller) compared to React Router v6, provides a more efficient route matching algorithm, and offers better support for React 19 and TypeScript. There are very few breaking changes compared to React Router v6, and Modern.js has made both versions compatible, allowing for a seamless upgrade by simply installing and registering the appropriate plugin.
95+
96+
:::info
97+
98+
For more changes from React Router v6 to React Router v7, check the [documentation](https://reactrouter.com/upgrading/v6#upgrade-to-v7)
99+
100+
:::
101+
102+
### Requirements
103+
104+
React Router v7 has certain environment requirements:
105+
106+
- Node.js 20+
107+
- React 18+
108+
- React DOM 18+
109+
110+
### Install the Plugin
111+
112+
First, install the Modern.js React Router v7 plugin:
113+
114+
```bash
115+
pnpm add @modern-js/plugin-router-v7
116+
```
117+
118+
### Configure the Plugin
119+
120+
Register the plugin in `modern.config.ts`:
121+
122+
```ts title="modern.config.ts"
123+
import { routerPlugin } from '@modern-js/plugin-router-v7';
124+
125+
export default {
126+
runtime: {
127+
router: true,
128+
},
129+
plugins: [routerPlugin()],
130+
};
131+
```
132+
133+
### Code Changes
134+
135+
In React Router v7, you no longer need to use the `defer` API; you can directly return data in the data loader:
136+
137+
```ts title="routes/page.data.ts"
138+
import { defer } from '@modern-js/runtime/router';
139+
140+
export const loader = async ({ params }) => {
141+
// Recommended v7 style
142+
const user = fetchUser(params.id)
143+
return { user };
144+
145+
// v6 style, still compatible with Modern.js
146+
return defer({ data: 'hello' });
147+
};
148+
```
149+
150+
React Router v7 has also deprecated the `json` API:
151+
152+
```ts title="routes/page.data.ts"
153+
export const loader = async ({ params }) => {
154+
// Recommended v7 style
155+
return { data: 'hello' };
156+
157+
// v6 style, still compatible with Modern.js
158+
return json({ data: 'hello' });
159+
};
160+
```

packages/document/main-doc/docs/zh/guides/basic-features/routes.mdx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,78 @@ import Motivation from '@site-docs/components/convention-routing-motivation';
495495

496496
<Motivation />
497497

498+
## 升级到 react-router v7
499+
500+
React Router v7 相比 React Router v6 减少了包体积(小约 15%),提供了更高效的路由匹配算法,对 React 19TypeScript 也提供了更好的支持,
501+
相比 React Router v6 breaking change 非常少,同时 Modern.js 也对两个版本做了兼容,只需在项目中安装并注册相应的插件即可无缝升级。
502+
503+
:::info
504+
505+
更多 react router v6react router v7 的变更,请查看[文档](https://reactrouter.com/upgrading/v6#upgrade-to-v7)
506+
507+
:::
508+
509+
### 环境要求
510+
511+
React Router v7 对环境有一定要求:
512+
513+
- Node.js 20+
514+
- React 18+
515+
- React DOM 18+
516+
517+
### 安装插件
518+
519+
首先,安装 Modern.jsReact Router v7 插件:
520+
521+
```bash
522+
pnpm add @modern-js/plugin-router-v7
523+
```
524+
525+
### 配置插件
526+
527+
`modern.config.ts` 中注册插件:
528+
529+
```ts title="modern.config.ts"
530+
import { routerPlugin } from '@modern-js/plugin-router-v7';
531+
532+
export default {
533+
runtime: {
534+
router: true,
535+
},
536+
plugins: [routerPlugin()],
537+
};
538+
```
539+
540+
### 修改代码
541+
542+
react router v7 中,不需要再使用 `defer` API 了,直接在 data loader 中返回数据即可:
543+
544+
```ts title="routes/page.data.ts"
545+
import { defer } from '@modern-js/runtime/router';
546+
547+
export const loader = async ({ params }) => {
548+
// 推荐的 v7 风格
549+
const user = fetchUser(params.id)
550+
return { user };
551+
552+
// v6 风格,Modern.js 做了兼容,仍然可以继续使用
553+
return defer({ data: 'hello' });
554+
};
555+
```
556+
557+
react router v7 同样废弃了 `json` API
558+
559+
```ts title="routes/page.data.ts"
560+
export const loader = async ({ params }) => {
561+
// 推荐的 v7 风格
562+
return { data: 'hello' };
563+
564+
// v6 风格,Modern.js 做了兼容,仍然可以继续使用
565+
return json({ data: 'hello' });
566+
};
567+
```
568+
569+
498570
## 常见问题
499571

500572
1. 为什么要提供 `@modern-js/runtime/router` 来导出 React Router API ?
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
.DS_Store
2+
3+
.pnp
4+
.pnp.js
5+
.env.local
6+
.env.*.local
7+
*.log*
8+
9+
node_modules/
10+
*.tsbuildinfo
11+
.eslintcache
12+
13+
coverage/
14+
output/
15+
output_resource/
16+
tests/
17+
18+
.vscode/**/*
19+
!.vscode/settings.json
20+
!.vscode/extensions.json
21+
.idea/
22+
23+
src/
24+
25+
modern.config.*
26+
jest.config.js
27+
.eslintrc.js
28+
.eslintrc
29+
tsconfig.json
30+
CHANGELOG.md
31+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021-present Modern.js
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<p align="center">
2+
<a href="https://modernjs.dev" target="blank"><img src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ylaelkeh7nuhfnuhf/modernjs-cover.png" width="300" alt="Modern.js Logo" /></a>
3+
</p>
4+
5+
<h1 align="center">Modern.js</h1>
6+
7+
<p align="center">
8+
A Progressive React Framework for modern web development.
9+
</p>
10+
11+
## Getting Started
12+
13+
Please follow [Quick Start](https://modernjs.dev/en/guides/get-started/quick-start) to get started with Modern.js.
14+
15+
## Documentation
16+
17+
- [English Documentation](https://modernjs.dev/en/)
18+
- [中文文档](https://modernjs.dev)
19+
20+
## Contributing
21+
22+
Please read the [Contributing Guide](https://github.com/web-infra-dev/modern.js/blob/main/CONTRIBUTING.md).
23+
24+
## License
25+
26+
Modern.js is [MIT licensed](https://github.com/web-infra-dev/modern.js/blob/main/LICENSE).
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const sharedConfig = require('@scripts/jest-config');
2+
3+
/** @type {import('@jest/types').Config.InitialOptions} */
4+
module.exports = {
5+
...sharedConfig,
6+
rootDir: __dirname,
7+
moduleNameMapper: {
8+
'^@meta/runtime$': '<rootDir>/node_modules/@modern-js/runtime/src',
9+
'^@meta/runtime/context$':
10+
'<rootDir>/node_modules/@modern-js/runtime/src/core/context',
11+
},
12+
};

0 commit comments

Comments
 (0)