@bleedingdev/modern-js-main-doc 3.9.0-ultramodern.1 → 3.9.0-ultramodern.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,10 +46,9 @@ import EnableBFFCaution from "@site-docs/components/enable-bff-caution";
46
46
  仅当 `bff.runtimeFramework` 设置为 `'effect'` 时,`bff.effect` 才会生效。
47
47
 
48
48
  :::caution 需要自行安装 Effect peer 依赖
49
- `effect` 与 `@effect/opentelemetry` 是 `@modern-js/plugin-bff` 的**可选精确 peer
50
- 依赖**,而不是直接依赖。插件不再自带副本——Effect 4 的 `Context` / `Service` 键按模块
51
- 实例生成,自带副本会让应用出现第二份 Effect 身份。在设置 `runtimeFramework: 'effect'`
52
- 或引入 `@modern-js/plugin-bff/effect`、`/effect-server`、`/effect-edge`、
49
+ `effect` 与 `@effect/opentelemetry` 是 `@modern-js/bff-effect` 的**可选精确 peer
50
+ 依赖**。应用必须安装这些依赖,让 API 模块与框架使用同一个 Effect 实例。在设置
51
+ `runtimeFramework: 'effect'` 或引入 `@modern-js/bff-effect/effect`、`/effect-edge`、
53
52
  `/effect-client` 之前,请安装精确版本的依赖组:
54
53
 
55
54
  ```bash
@@ -57,9 +56,19 @@ pnpm add effect@4.0.0-rc.112 @effect/opentelemetry@4.0.0-rc.112
57
56
  ```
58
57
 
59
58
  采用精确版本是因为 UltraModern 以锁步依赖组的方式发布 Effect。只使用
60
- `runtimeFramework: 'hono'` 或 `./data-platform` 通道的应用无需安装这两个包。
59
+ 原生 `runtimeFramework: 'hono'` 或 `@modern-js/bff-effect/data-platform` 的应用无需安装这两个包。
61
60
  :::
62
61
 
62
+ 使用 `@modern-js/plugin-bff-build-extensions` 导出的 `bffPlugin` 注册 Effect
63
+ 运行时,它会组合原生 BFF 插件。将同一框架版本组中的 `@modern-js/bff-effect` 和
64
+ `@modern-js/plugin-bff-extensions` 安装为应用的生产依赖,保证移除开发依赖后仍能加载
65
+ 运行时适配器。构建插件可以作为开发依赖。参见[运行时配置示例](/guides/advanced-features/bff/frameworks)。
66
+
67
+ 原生 `@modern-js/plugin-bff/server` 导出 Hono API。Node Effect API 从
68
+ `@modern-js/bff-effect/effect` 导入 `defineEffectBff` 等框架 helper,命名空间从对应的
69
+ `effect/*` 模块导入。Worker handler 和 Worker 请求上下文使用
70
+ `@modern-js/bff-effect/effect-edge`。
71
+
63
72
  生成的 UltraModern workspace 只把这个运行时作为生成 HTTP API 路径。API 契约固定在
64
73
  `shared/api.ts`,服务端运行时固定在 `api/index.ts`,客户端固定在
65
74
  `src/api/*-client.ts`。生成检查会拒绝 `api/effect`、`api/lambda`、
@@ -169,7 +178,6 @@ export default defineConfig({
169
178
  endpoint: '/_data/batch',
170
179
  maxBatchSize: 16,
171
180
  maxBatchBytes: 64 * 1024,
172
- flushIntervalMs: 8,
173
181
  maxConcurrency: 4,
174
182
  requestTimeoutMs: 10000,
175
183
  allowedMethods: ['GET'],
@@ -203,9 +211,9 @@ export default defineConfig({
203
211
  });
204
212
  ```
205
213
 
206
- `batch.flushIntervalMs` 用于控制生成的 Effect 客户端微批处理窗口;`maxConcurrency` 与 `requestTimeoutMs` 由服务端批处理网关用于内部请求分发。
214
+ `maxConcurrency` 与 `requestTimeoutMs` 控制服务端批处理网关。原生 `HttpApiClient` 发送独立请求,不会自动批处理。
207
215
 
208
- 生成的 `api.client.*` API 只存在于 loader 物化后的 `@api/index` 导入中。直接导入服务端入口(`api/index`)时拿到的是 Effect BFF 定义;其中的 `client` 属性只是占位,并会在访问具体操作时报错。
216
+ 导入共享 `HttpApi` 契约并传给 `HttpApiClient.make` 或 `makeEffectHttpApiClient`,即可获得完全类型推导的客户端。`defineEffectBff` 只提供服务端 handler,不包含客户端。
209
217
 
210
218
  ## Effect 版本组
211
219
 
@@ -237,7 +245,7 @@ attestation 的迁移,并不等同于 release-age 审批。
237
245
  Edge 兼容测试可以使用框架 helper:
238
246
 
239
247
  ```ts
240
- import { createEffectBffTestHandler } from '@modern-js/plugin-bff/effect-edge';
248
+ import { createEffectBffTestHandler } from '@modern-js/bff-effect/effect-edge';
241
249
  import apiModule from '../api/index';
242
250
 
243
251
  const testApi = await createEffectBffTestHandler({
@@ -252,12 +260,9 @@ const response = await testApi.handler(new Request('http://localhost/api/ping'))
252
260
  `HttpServer.layerServices`:
253
261
 
254
262
  ```ts
255
- import {
256
- HttpApiBuilder,
257
- HttpRouter,
258
- HttpServer,
259
- Layer,
260
- } from '@modern-js/plugin-bff/effect-server';
263
+ import * as Layer from 'effect/Layer';
264
+ import { HttpRouter, HttpServer } from 'effect/unstable/http';
265
+ import { HttpApiBuilder } from 'effect/unstable/httpapi';
261
266
 
262
267
  const handler = HttpRouter.toWebHandler(
263
268
  HttpApiBuilder.layer(api).pipe(
@@ -275,13 +280,10 @@ const handler = HttpRouter.toWebHandler(
275
280
  Effect v4 beta 版本组中,`HttpRouter.middleware(...)` 直接返回 `Layer`:
276
281
 
277
282
  ```ts
278
- import {
279
- Effect,
280
- HttpApiBuilder,
281
- HttpMiddleware,
282
- HttpRouter,
283
- Layer,
284
- } from '@modern-js/plugin-bff/effect-server';
283
+ import * as Effect from 'effect/Effect';
284
+ import * as Layer from 'effect/Layer';
285
+ import { HttpMiddleware, HttpRouter } from 'effect/unstable/http';
286
+ import { HttpApiBuilder } from 'effect/unstable/httpapi';
285
287
 
286
288
  const corsLayer = HttpRouter.middleware(
287
289
  Effect.succeed(
@@ -17,7 +17,7 @@ Modern.js 的 Effect BFF 已支持基于请求信封(request envelope)的数
17
17
 
18
18
  ## 运行时契约工具
19
19
 
20
- 可通过 `@modern-js/plugin-bff/data-platform` 使用以下能力:
20
+ 可通过 `@modern-js/bff-effect/data-platform` 使用以下能力:
21
21
 
22
22
  ```ts
23
23
  import {
@@ -30,7 +30,7 @@ import {
30
30
  validateHydrationEnvelope,
31
31
  createInvalidationEvent,
32
32
  shouldApplyInvalidation,
33
- } from '@modern-js/plugin-bff/data-platform';
33
+ } from '@modern-js/bff-effect/data-platform';
34
34
  ```
35
35
 
36
36
  ## Effect 运行时校验
@@ -5,10 +5,10 @@ title: 运行时框架
5
5
 
6
6
  # 运行时框架
7
7
 
8
- Modern.js 目前支持两种 BFF 运行时框架:
8
+ Modern.js 与 UltraModern BFF 扩展提供两种运行时框架:
9
9
 
10
- - `effect`(默认):使用 `api/index` 的 [Effect HttpApi](https://effect.website/) 运行时。
11
- - `hono`:使用 `api/lambda/**` 的文件约定 BFF 处理函数。
10
+ - `hono` 是原生插件的默认运行时,使用 `api/lambda/**` 的文件约定处理函数。
11
+ - `effect` 是 UltraModern 扩展的默认运行时,使用 `api/index` 的 [Effect HttpApi](https://effect.website/) 运行时。
12
12
 
13
13
  `effect` 与 `hono` 为严格模式,两者之间不会自动回退。
14
14
 
@@ -22,12 +22,16 @@ workspace 中加入 Hono/file-convention handler、原始 request parsing 或手
22
22
 
23
23
  ## 切换到 Effect 运行时
24
24
 
25
+ 使用下面的 fork 构建插件,它会包含原生 BFF 插件并注册 Effect 适配器。应用需要将
26
+ 同一框架版本组中的 `@modern-js/bff-effect` 和 `@modern-js/plugin-bff-extensions`
27
+ 安装为生产依赖。Effect 的精确 peer 版本见 [`bff.effect`](/configure/app/bff/effect)。
28
+
25
29
  ```ts title="modern.config.ts"
26
- import { bffPlugin } from '@modern-js/plugin-bff';
27
- import { defineConfig } from '@modern-js/app-tools';
30
+ import { bffPlugin } from '@modern-js/plugin-bff-build-extensions';
31
+ import { appTools, defineConfig } from '@modern-js/app-tools';
28
32
 
29
33
  export default defineConfig({
30
- plugins: [bffPlugin()],
34
+ plugins: [appTools(), bffPlugin()],
31
35
  bff: {
32
36
  runtimeFramework: 'effect',
33
37
  effect: {
@@ -49,7 +53,7 @@ import {
49
53
  HttpApiEndpoint,
50
54
  HttpApiGroup,
51
55
  Schema,
52
- } from '@modern-js/plugin-bff/effect-client';
56
+ } from '@modern-js/bff-effect/effect-client';
53
57
 
54
58
  export const bffApi = HttpApi.make('MyApi').add(
55
59
  HttpApiGroup.make('hello').add(
@@ -63,14 +67,12 @@ export const bffApi = HttpApi.make('MyApi').add(
63
67
  然后在 `api/index.ts` 中实现 Effect BFF 入口:
64
68
 
65
69
  ```ts title="api/index.ts"
66
- import {
67
- Schema,
68
- Effect,
69
- HttpApiBuilder,
70
- defineEffectBff,
71
- Layer,
72
- ServiceMap,
73
- } from '@modern-js/plugin-bff/effect-server';
70
+ import { defineEffectBff } from '@modern-js/bff-effect/effect';
71
+ import * as Context from 'effect/Context';
72
+ import * as Effect from 'effect/Effect';
73
+ import * as Layer from 'effect/Layer';
74
+ import * as Schema from 'effect/Schema';
75
+ import { HttpApiBuilder } from 'effect/unstable/httpapi';
74
76
  import { bffApi } from '../shared/api';
75
77
 
76
78
  class GreetingUnavailableError extends Schema.TaggedError<GreetingUnavailableError>()(
@@ -80,7 +82,7 @@ class GreetingUnavailableError extends Schema.TaggedError<GreetingUnavailableErr
80
82
  },
81
83
  ) {}
82
84
 
83
- class GreetingService extends ServiceMap.Service<GreetingService>()('GreetingService', {
85
+ class GreetingService extends Context.Service<GreetingService>()('GreetingService', {
84
86
  make: Effect.succeed({
85
87
  hello: Effect.fn('GreetingService.hello')(function* () {
86
88
  if (Date.now() < 0) {
@@ -115,15 +117,24 @@ const layer = HttpApiBuilder.layer(bffApi).pipe(
115
117
  export default defineEffectBff({ api: bffApi, layer });
116
118
  ```
117
119
 
118
- 在浏览器代码中通过 `@api/index` 调用接口:
120
+ 从共享契约创建原生、完全类型推导的客户端:
119
121
 
120
122
  ```ts title="src/routes/page.tsx"
121
- import api from '@api/index';
123
+ import { Effect, makeEffectHttpApiClient } from '@modern-js/bff-effect/effect-client';
124
+ import { bffApi } from '../../shared/api';
122
125
 
123
- const response = await api.client.hello.ping({});
126
+ const response = await Effect.runPromise(
127
+ makeEffectHttpApiClient(bffApi, { baseUrl: '/api' }).pipe(
128
+ Effect.flatMap(client => client.hello.ping({})),
129
+ ),
130
+ );
124
131
  ```
125
132
 
126
- `api.client.*` 由 BFF loader 针对 `@api/index` 导入物化生成。不要直接导入 `api/index` 并期望在服务端代码、脚本或测试中运行 `client`;直接导入入口时拿到的是服务端运行时定义,其中的 `client` 只是类型占位。
133
+ 请求、响应和声明的错误类型均从 `bffApi` 推导,无需生成客户端代码或导入服务端入口。
134
+
135
+ 原生 Hono 应用从 `@modern-js/plugin-bff/server` 导入操作符。生成的 UltraModern
136
+ 应用继续使用严格 Effect API。Worker handler 及其请求上下文使用
137
+ `@modern-js/bff-effect/effect-edge`;Node handler 使用上面示例中的 Effect 入口。
127
138
 
128
139
  import Hono from '@site-docs/components/hono';
129
140
 
@@ -188,7 +188,7 @@ Dynamic Path 之后的参数是包含 querystring、request body 的对象 `Requ
188
188
  在不存在动态路由的普通函数中,可以从第一个入参中获取传入的 `data` 和 `query`,例如:
189
189
 
190
190
  ```ts title="api/lambda/hello.ts"
191
- import type { RequestOption } from '@modern-js/plugin-bff/hono-server';
191
+ import type { RequestOption } from '@modern-js/plugin-bff/server';
192
192
 
193
193
  export async function post({
194
194
  query,
@@ -201,7 +201,7 @@ export async function post({
201
201
  这里你也可以使用自定义类型:
202
202
 
203
203
  ```ts title="api/lambda/hello.ts"
204
- import type { RequestOption } from '@modern-js/plugin-bff/hono-server';
204
+ import type { RequestOption } from '@modern-js/plugin-bff/server';
205
205
 
206
206
  type IQuery = {
207
207
  // some types
@@ -36,7 +36,7 @@ import BFFOperatorCode from '@site-docs/components/bff-operator-code';
36
36
  <BFFOperatorCode>
37
37
 
38
38
  ```typescript title="api/lambda/user.ts"
39
- import { Api, Post, Query, Data } from '@modern-js/plugin-bff/hono-server';
39
+ import { Api, Post, Query, Data } from '@modern-js/plugin-bff/server';
40
40
  import { z } from 'zod';
41
41
 
42
42
  const UserSchema = z.object({
@@ -89,7 +89,7 @@ addUser({
89
89
  <BFFOperatorCode>
90
90
 
91
91
  ```typescript title="api/lambda/user.ts"
92
- import { Api, Get, Query, Data } from '@modern-js/plugin-bff/hono-server';
92
+ import { Api, Get, Query, Data } from '@modern-js/plugin-bff/server';
93
93
 
94
94
  // 指定接口路由,Modern.js 默认设置 `bff.prefix` 为 `/api`,
95
95
  // 因此该接口路由为 `/api/user`,Http Method 为 GET。
@@ -107,7 +107,7 @@ export const getHello = Api(
107
107
  <BFFOperatorCode>
108
108
 
109
109
  ```typescript title="api/lambda/user.ts"
110
- import { Api, Get, Query, Data } from '@modern-js/plugin-bff/hono-server';
110
+ import { Api, Get, Query, Data } from '@modern-js/plugin-bff/server';
111
111
 
112
112
  // 未指定接口路由,根据文件约定和函数名,该接口为 api/user,Http Method 为 get。
113
113
  export const get = Api(Query(UserSchema), async ({ query }) => query);
@@ -144,7 +144,7 @@ Modern.js 推荐基于文件约定去定义接口,保持项目中路由清晰
144
144
 
145
145
  ```typescript title="api/lambda/user.ts"
146
146
  // 服务端代码
147
- import { Api, Query } from '@modern-js/plugin-bff/hono-server';
147
+ import { Api, Query } from '@modern-js/plugin-bff/server';
148
148
  import { z } from 'zod';
149
149
 
150
150
  const UserSchema = z.object({
@@ -176,7 +176,7 @@ URL query 参数默认是字符串类型,如果需要数字类型,需要使
176
176
  <BFFOperatorCode>
177
177
 
178
178
  ```typescript title="api/lambda/user.ts"
179
- import { Api, Get, Query } from '@modern-js/plugin-bff/hono-server';
179
+ import { Api, Get, Query } from '@modern-js/plugin-bff/server';
180
180
  import { z } from 'zod';
181
181
 
182
182
  const QuerySchema = z.object({
@@ -216,7 +216,7 @@ URL query 参数都是字符串类型,如果需要数字类型,需要使用
216
216
  <BFFOperatorCode>
217
217
 
218
218
  ```typescript title="api/lambda/user.ts"
219
- import { Api, Data } from '@modern-js/plugin-bff/hono-server';
219
+ import { Api, Data } from '@modern-js/plugin-bff/server';
220
220
  import { z } from 'zod';
221
221
 
222
222
  const DataSchema = z.object({
@@ -249,7 +249,7 @@ post({
249
249
  <BFFOperatorCode>
250
250
 
251
251
  ```typescript
252
- import { Api, Get, Params } from '@modern-js/plugin-bff/hono-server';
252
+ import { Api, Get, Params } from '@modern-js/plugin-bff/server';
253
253
  import { z } from 'zod';
254
254
 
255
255
  const UserSchema = z.object({
@@ -274,7 +274,7 @@ export const queryUser = Api(
274
274
  <BFFOperatorCode>
275
275
 
276
276
  ```typescript
277
- import { Api, Headers } from '@modern-js/plugin-bff/hono-server';
277
+ import { Api, Headers } from '@modern-js/plugin-bff/server';
278
278
  import { z } from 'zod';
279
279
 
280
280
  const headerSchema = z.object({
@@ -336,7 +336,7 @@ try {
336
336
  <BFFOperatorCode>
337
337
 
338
338
  ```typescript
339
- import { Api, Query, Middleware } from '@modern-js/plugin-bff/hono-server';
339
+ import { Api, Query, Middleware } from '@modern-js/plugin-bff/server';
340
340
  import { z } from 'zod';
341
341
 
342
342
  const UserSchema = z.object({
@@ -376,7 +376,7 @@ export const get = Api(
376
376
  <BFFOperatorCode>
377
377
 
378
378
  ```typescript
379
- import { Api, Query, Pipe } from '@modern-js/plugin-bff/hono-server';
379
+ import { Api, Query, Pipe } from '@modern-js/plugin-bff/server';
380
380
  import { z } from 'zod';
381
381
 
382
382
  const UserSchema = z.object({
@@ -408,7 +408,7 @@ export const get = Api(
408
408
  <BFFOperatorCode>
409
409
 
410
410
  ```typescript
411
- import { Api, Query, Pipe } from '@modern-js/plugin-bff/hono-server';
411
+ import { Api, Query, Pipe } from '@modern-js/plugin-bff/server';
412
412
  import { z } from 'zod';
413
413
 
414
414
  const UserSchema = z.object({
@@ -443,7 +443,7 @@ export const get = Api(
443
443
  <BFFOperatorCode>
444
444
 
445
445
  ```typescript
446
- import { Api, Query, Pipe } from '@modern-js/plugin-bff/hono-server';
446
+ import { Api, Query, Pipe } from '@modern-js/plugin-bff/server';
447
447
  import { z } from 'zod';
448
448
 
449
449
  const UserSchema = z.object({
@@ -487,7 +487,7 @@ export const get = Api(
487
487
  <BFFOperatorCode>
488
488
 
489
489
  ```typescript
490
- import { Api, Query, Data, HttpCode } from '@modern-js/plugin-bff/hono-server';
490
+ import { Api, Query, Data, HttpCode } from '@modern-js/plugin-bff/server';
491
491
  import { z } from 'zod';
492
492
 
493
493
  const UserSchema = z.object({
@@ -523,7 +523,7 @@ export const post = Api(
523
523
  <BFFOperatorCode>
524
524
 
525
525
  ```typescript
526
- import { Api, Get, SetHeaders } from '@modern-js/plugin-bff/hono-server';
526
+ import { Api, Get, SetHeaders } from '@modern-js/plugin-bff/server';
527
527
 
528
528
  export default Api(
529
529
  Get('/hello'),
@@ -543,7 +543,7 @@ export default Api(
543
543
  <BFFOperatorCode>
544
544
 
545
545
  ```typescript
546
- import { Api, Get, Redirect } from '@modern-js/plugin-bff/hono-server';
546
+ import { Api, Get, Redirect } from '@modern-js/plugin-bff/server';
547
547
 
548
548
  export default Api(
549
549
  Get('/hello'),
@@ -561,7 +561,7 @@ export default Api(
561
561
  <BFFOperatorCode>
562
562
 
563
563
  ```typescript title="api/lambda/user.ts"
564
- import { Api, Get, Query } from '@modern-js/plugin-bff/hono-server';
564
+ import { Api, Get, Query } from '@modern-js/plugin-bff/server';
565
565
  import { useHonoContext } from '@modern-js/server-runtime';
566
566
  import { z } from 'zod';
567
567
 
@@ -610,7 +610,7 @@ export const queryUser = Api(
610
610
  <BFFOperatorCode>
611
611
 
612
612
  ```typescript
613
- import { Api, SetHeaders } from '@modern-js/plugin-bff/hono-server';
613
+ import { Api, SetHeaders } from '@modern-js/plugin-bff/server';
614
614
 
615
615
  export const get = Api(
616
616
  // 缓存使用一体化调用或者 fetch 进行请求才会生效
@@ -39,7 +39,7 @@ export interface CacheControl {
39
39
  }
40
40
  ```
41
41
 
42
- 其中 `customKey` 为自定义缓存 key。默认情况下 Modern.js 将会把请求 `pathname` 作为 key 进行缓存,但在某些情况下这不能满足你的需求,开发者可以进行自定义。
42
+ 其中 `customKey` 为自定义缓存 key。默认情况下,缓存 key 由请求的 origin、归一化后的 pathname(去除末尾斜杠)以及 query 组成。当设置了 `customKey` 后,它会完全替代默认 key:`customKey` 回调函数接收的是归一化后的 pathname(而非 `CacheOptionProvider` 的 `req`),并需要自行负责整个分区(例如按租户、host 或 query 区分)——不能将原始 cookie 或其他凭证嵌入返回的 key 中。
43
43
 
44
44
  **Function 类型**
45
45
 
@@ -111,6 +111,19 @@ export const cacheOption: CacheOption = {
111
111
 
112
112
  上述 `/home` 和 `/about` 将会作为模式进行匹配,这意味着 `/home/abc` 也会匹配上该规则。同时,你也可以在其中编写正则语法:`/home/.+`
113
113
 
114
+ ### 缓存策略
115
+
116
+ 以下策略由框架自动执行,返回的 `CacheControl` 无法覆盖:
117
+
118
+ - 只有 `GET` 请求会被缓存。
119
+ - 请求携带 `Cookie` 或 `Authorization` 请求头时会跳过缓存,除非设置了 `customKey`(自定义 key 被视为一份明确的分区约定)。
120
+ - 请求的 `Cache-Control` 为 `private`、`no-cache` 或 `no-store` 时会跳过缓存。
121
+ - 只有当响应为 `200`、`Cache-Control` 中不含 `private`/`no-cache`/`no-store`、不含 `Set-Cookie`、也不含非空的 `Vary` 头时才会被写入缓存;否则会删除该 key 已有的缓存条目,而不是写入新的。
122
+ - 当 `stale` 状态触发的重新渲染结果不再可缓存时(例如变为按用户区分),本次已返回的 stale 响应不受影响,但缓存条目会被清除,后续请求将不再命中该缓存。
123
+ - `CacheOptionProvider` 返回 `false` 依然会完全禁用该请求的缓存,不受上述策略影响。
124
+
125
+ 缓存 key 带有命名空间(`__ssr__cache:v2:...`)。旧版本未加命名空间的缓存条目(包括自定义 `Container` 中已存储的条目)不会再被读取,只会依据容器自身的淘汰/TTL 机制自然过期;不存在全局缓存清空操作。
126
+
114
127
  ### 缓存容器
115
128
 
116
129
  默认情况下,Server 将会使用内存进行缓存。但通常情况下服务将会部署在 Serverless 容器上。每一次的服务访问可能都是一个新的进程,这样每次访问都不能应用缓存。
@@ -16,15 +16,11 @@ UltraModern.js 3.0 是我们从 Modern.js 分叉出来的 SuperApp 框架。它
16
16
  - 仅在跨团队稳定性场景增加平台契约。
17
17
  - 保留显式 escape hatch,但它们必须位于生成 HTTP API 模块之外。
18
18
 
19
- ## 兼容性承诺
19
+ ## 当前 Workspace 契约
20
20
 
21
- UltraModern.js 3.0 保持以下不变:
22
-
23
- - Modern.js 的应用/配置/插件心智模型。
24
- - 既有项目结构与命令使用方式。
25
- - 渐进式接入路径(应用无需大规模重构)。
26
-
27
- UltraModern.js 的增强能力是新 SuperApp 的默认产品面。框架方向是 Effect + TanStack + SSR + Micro Verticals,生成的 HTTP API 只走严格 Effect HttpApi surface。既有 Modern.js 应用可以渐进迁移;一旦某个 API surface 被生成为或迁移为 UltraModern HTTP API,就必须使用严格 Effect HttpApi 路径。
21
+ UltraModern.js 使用 Effect HttpApi、TanStack Router、SSR 和可独立部署的 Micro Verticals。
22
+ 生成的 workspace 使用 `api/index.ts`、`shared/api.ts`、`src/api/*` 和具体的 Effect schema。
23
+ CLI 按当前契约创建、扩展和验证 workspace。
28
24
 
29
25
  ## 有意引入的差异(3.0 线)
30
26
 
@@ -50,20 +46,9 @@ UltraModern.js 的增强能力是新 SuperApp 的默认产品面。框架方向
50
46
  - 生成的 UltraModern HTTP API 只走严格 Effect HttpApi surface;原始 request handler 和 Hono/file-function API 不属于生成架构。
51
47
  - 除非稳定性硬需求,否则避免引入破坏性 API 变更。
52
48
 
53
- ## 迁移指南
54
-
55
- 对于已经在使用 Modern.js 3.0 或旧版 BleedingDev UltraModern scaffold 的团队,迁移路径是把 API 工作迁到严格 Effect HttpApi surface,而不是保留旧的原始 handler 布局。
56
-
57
- 1. 既有 Modern.js 应用在尚未进入生成 UltraModern surface 前可以继续按现状运行。TanStack Router 是新脚手架与增量迁移的优先路径,团队可以按自己的节奏迁移路由层。
58
- 2. 新建或迁移中的 BFF 能力使用 `bff.runtimeFramework: 'effect'`、`bff.effect.entry: './api/index'` 和 `bff.effect.strictEffectApproach: true`。接口先改 `shared/api.ts` 的 `HttpApi` 契约,再在 `api/index.ts` 用 `defineEffectBff(...)` / `HttpApiBuilder` 实现。
59
- 3. 在生成或已迁移的 UltraModern workspace 中,把 raw handler、`api/lambda/**`、手写 `Response` 和手写 request parsing 视为迁移缺陷。
60
- 4. 这套公开预设现在已经附带显式的发布 / 认证 gate。生成 workspace 会自带 `.github/workflows/ultramodern-workspace-gates.yml`,因此 `pnpm check` 与 `pnpm build` 从第一天开始就是本地接入契约的一部分;CI 会以并行矩阵运行这些基础 gate。
61
-
62
- 旧版生成 workspace 迁移时,以已发布的包 cohort 作为事实来源:
49
+ ## 创建并验证 Workspace
63
50
 
64
51
  ```bash
65
- pnpm dlx @bleedingdev/modern-js-ultramodern-create@latest --help
66
- pnpm dlx @bleedingdev/modern-js-ultramodern-create@latest catalog --vertical --dry-run
67
52
  pnpm dlx @bleedingdev/modern-js-ultramodern-create@latest catalog --vertical
68
53
  mise install
69
54
  mise exec -- pnpm install
@@ -71,62 +56,14 @@ mise exec -- pnpm check
71
56
  mise exec -- pnpm build
72
57
  ```
73
58
 
74
- ### 迁移本地化 Cloudflare SSR workspace
75
-
76
- 当前 cohort 均支持本地化 Cloudflare SSR workspace。框架将裸 locale root 重定向移动到框架拥有的 Cloudflare Worker entry 和 i18n
77
- server runtime 中。请求 `/` 时,框架会按语言协商在服务端返回 `302`,例如
78
- `Accept-Language: cs-CZ` 会跳转到 `/cs`,英语或 fallback 流量会跳转到 `/en`。
79
-
80
- 既有生成 workspace 应整体升级同一个 BleedingDev Modern package cohort。
81
- 先解析当前 cohort 版本,再运行同版本迁移命令:
82
-
83
- ```bash
84
- COHORT="$(npm view @bleedingdev/modern-js-ultramodern-create version)"
85
- pnpm dlx "@bleedingdev/modern-js-ultramodern-create@$COHORT" ultramodern \
86
- migrate-strict-effect --version "$COHORT"
87
- pnpm install
88
- pnpm check
89
- pnpm build
90
- pnpm cloudflare:build
91
- ```
92
-
93
- Cloudflare Workers 已部署环境需要在 cohort 更新后重新部署,并在接受迁移前验证 root
94
- 响应:
59
+ 本地化 Cloudflare SSR workspace 在服务端将 `/` 重定向到协商的语言路径。
60
+ `Accept-Language: cs-CZ` 返回 `302`、`Location: /cs`、
61
+ `Cache-Control: private, no-store`,以及覆盖语言检测 Header 的 `Vary`。
95
62
 
96
63
  ```bash
97
64
  curl -I -H 'Accept-Language: cs-CZ,cs;q=0.9,en;q=0.1' https://<worker-host>/
98
65
  ```
99
66
 
100
- 期望响应是 `302`,并带有 `Location: /cs` 或匹配的 locale、
101
- `Cache-Control: private, no-store`,以及覆盖语言检测 Header 的 `Vary`。继续跟随该
102
- redirect 后,应返回匹配语言的 SSR 页面,文档语言和 i18n SSR 数据也应一致。
103
-
104
- 不要通过 app 自有 root route、客户端 redirect、自定义导航 wrapper、Cloudflare Worker
105
- postprocess、生成产物编辑或本地 redirect shim 来修复旧版本的 root `404`。如果升级后
106
- `/` 仍然返回 `404`,先确认生产构建日志显示的
107
- `Modern.js Framework v<cohort-version>` 就是迁移目标 cohort,然后重新部署 Worker。
108
-
109
- 严格生成 API 迁移是当前每个 cohort 的组成部分:直接 `api/index.ts` 生成器、生成的
110
- `.mts` 检查、严格 Oxlint 边界规则、Effect 版本组 overrides 和严格 Effect 迁移命令。
111
- 还不能安装 BleedingDev cohort 的 agent 应使用本地 Modern.js workspace 做迁移校验;
112
- 否则用 `--ultramodern-package-version` 固定目标 cohort。
113
-
114
- 手写 package alias 或生成 metadata 之前,先在目标 workspace 运行框架迁移命令:
115
-
116
- ```bash
117
- COHORT="$(npm view @bleedingdev/modern-js-ultramodern-create version)"
118
- pnpm dlx "@bleedingdev/modern-js-ultramodern-create@$COHORT" ultramodern \
119
- migrate-strict-effect --version "$COHORT"
120
- pnpm api:check
121
- pnpm contract:check
122
- ```
123
-
124
- 该命令会更新 `.modernjs/ultramodern.json`、根 `modernjs.packageSource`、生成的
125
- Modern package alias、框架拥有的 toolchain pin、直接 topology API metadata、严格 Effect
126
- pnpm overrides/trust policy 和 pnpm lockfile。剩余失败就是源码迁移:把代码移到
127
- `shared/api.ts`、`api/index.ts` 和 `src/api/*-client.ts`,再删除 `api/effect`、
128
- `api/lambda`、`shared/effect` 和 `src/effect`。
129
-
130
67
  严格 Effect 生成 workspace 会通过 pnpm overrides 固定兼容版本组:
131
68
  `effect@4.0.0-rc.112`、`@effect/opentelemetry@4.0.0-rc.112` 和
132
69
  `@effect/vitest@4.0.0-rc.112`。不要添加与这些 overrides 冲突的 app 本地直接
@@ -141,12 +78,12 @@ provenance attestation 的迁移;它不属于 release-age 审批。
141
78
  | 范围 | 固定版本 |
142
79
  | --- | --- |
143
80
  | Effect runtime 与测试版本组 | `effect`、`@effect/opentelemetry`、`@effect/vitest`:`4.0.0-rc.112` |
144
- | Effect compiler | `@effect/tsgo@0.37.0` |
145
- | Oxc 与 Ultracite | `oxlint@1.80.0`、`oxfmt@0.65.0`、`ultracite@7.10.6` |
146
- | TanStack Router | `@tanstack/react-router@1.170.32`、`@tanstack/router-core@1.171.27`、`@tanstack/history@1.162.1` |
81
+ | Effect compiler | `@effect/tsgo@0.41.0` |
82
+ | Oxc 与 Ultracite | `oxlint@1.81.0`、`oxfmt@0.66.0`、`ultracite@7.11.0` |
83
+ | TanStack Router | `@tanstack/react-router@1.170.33`、`@tanstack/router-core@1.171.28`、`@tanstack/history@1.162.2` |
147
84
  | Module Federation | `bridge-react`、`manifest`、`modern-js-v3`、`rspack`:`2.9.0`;`@module-federation/node@2.7.50` |
148
85
  | Tailwind CSS | `tailwindcss@4.3.3` |
149
- | Node 与包管理工具 | Node `26.7.0`、`@types/node@^26.2.0`、pnpm `11.24.0` |
86
+ | Node 与包管理工具 | Node `26.7.0`、`@types/node@^26.4.1`、pnpm `11.24.0` |
150
87
 
151
88
  生成的 workspace 默认使用 `@module-federation/bridge-react` 的
152
89
  router-free base entry(`bridge.enableBridgeRouter: false`),应用路由由
@@ -190,7 +127,7 @@ Effect RPC、WebSockets 和其他传输方式应在需要时作为显式 transpo
190
127
  它们不能作为在生成 HTTP API 模块中重新引入原始 request handler 的理由。
191
128
 
192
129
  严格 API 测试应执行 `HttpApi` 契约。Edge 兼容 proof 测试使用
193
- `@modern-js/plugin-bff/effect-edge` 的 `createEffectBffTestHandler`;如果必须手动组合
130
+ `@modern-js/bff-effect/effect-edge` 的 `createEffectBffTestHandler`;如果必须手动组合
194
131
  web handler,在调用 `HttpRouter.toWebHandler` 前要把 `HttpServer.layerServices` 和
195
132
  API group layer 一起提供。
196
133
 
package/package.json CHANGED
@@ -19,27 +19,27 @@
19
19
  "modern.js",
20
20
  "ultramodern.js"
21
21
  ],
22
- "version": "3.9.0-ultramodern.1",
22
+ "version": "3.9.0-ultramodern.11",
23
23
  "publishConfig": {
24
24
  "access": "public"
25
25
  },
26
26
  "dependencies": {
27
- "@modern-js/sandpack-react": "npm:@bleedingdev/modern-js-sandpack-react@3.9.0-ultramodern.1",
28
- "@modern-js/ultramodern-sandpack-profile": "npm:@bleedingdev/modern-js-ultramodern-sandpack-profile@3.9.0-ultramodern.1",
29
- "mermaid": "^11.16.0"
27
+ "@modern-js/sandpack-react": "npm:@bleedingdev/modern-js-sandpack-react@3.9.0-ultramodern.11",
28
+ "@modern-js/ultramodern-sandpack-profile": "npm:@bleedingdev/modern-js-ultramodern-sandpack-profile@3.9.0-ultramodern.11",
29
+ "mermaid": "^11.17.2"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@rsbuild/plugin-sass": "2.0.1",
33
- "@rspress/core": "2.0.17",
34
- "@rspress/plugin-llms": "2.0.17",
35
- "@rspress/shared": "2.0.17",
36
- "@shikijs/transformers": "^4.3.1",
33
+ "@rspress/core": "2.0.21",
34
+ "@rspress/plugin-llms": "2.0.21",
35
+ "@rspress/shared": "2.0.21",
36
+ "@shikijs/transformers": "^4.4.3",
37
37
  "@types/fs-extra": "11.0.4",
38
- "@types/node": "^26.2.0",
38
+ "@types/node": "^26.4.1",
39
39
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
40
40
  "classnames": "^2.5.1",
41
41
  "clsx": "^2.1.1",
42
- "fs-extra": "^11.3.6",
42
+ "fs-extra": "^11.4.0",
43
43
  "react": "^19.2.8",
44
44
  "react-dom": "^19.2.8",
45
45
  "rsbuild-plugin-open-graph": "1.1.3",
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Navigate,
3
- useLocation,
3
+ useMatch,
4
4
  useNavigate,
5
5
  } from '@modern-js/plugin-tanstack/runtime';
6
6
  import React from 'react';
@@ -52,8 +52,9 @@ export function AuthStatus() {
52
52
  Welcome {auth.user}!{' '}
53
53
  <button
54
54
  type="button"
55
- onClick={() => {
56
- auth.signout(() => void navigate({ to: '/' }));
55
+ onClick={async () => {
56
+ await navigate({ to: '/' });
57
+ auth.signout(() => {});
57
58
  }}
58
59
  >
59
60
  Sign out
@@ -62,9 +63,12 @@ export function AuthStatus() {
62
63
  );
63
64
  }
64
65
 
65
- export function RequireAuth({ children }: { children: JSX.Element }) {
66
+ export function RequireAuth({ children }: { children: React.JSX.Element }) {
66
67
  const auth = useAuth();
67
- const location = useLocation();
68
+ const pathname = useMatch({
69
+ strict: false,
70
+ select: match => match.pathname,
71
+ });
68
72
 
69
73
  if (!auth.user) {
70
74
  // Redirect them to the /login page, but save the current location they were
@@ -74,7 +78,7 @@ export function RequireAuth({ children }: { children: JSX.Element }) {
74
78
  return (
75
79
  <Navigate
76
80
  replace
77
- search={{ redirect: location.pathname }}
81
+ search={{ redirect: pathname }}
78
82
  to="/login"
79
83
  />
80
84
  );
@@ -1,4 +1,5 @@
1
1
  import { Helmet } from '@modern-js/runtime/head';
2
+ import type { JSX } from 'react';
2
3
  import './index.css';
3
4
 
4
5
  const PublicPage = (): JSX.Element => (
@@ -13,7 +13,7 @@
13
13
  "rsbuild-plugin-open-graph": "1.1.3"
14
14
  },
15
15
  "devDependencies": {
16
- "@rspress/core": "2.0.17",
17
- "@types/node": "^26.2.0"
16
+ "@rspress/core": "2.0.21",
17
+ "@types/node": "^26.4.1"
18
18
  }
19
19
  }