@route-forge/core 2.0.0 → 2.2.0

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.
package/README_zh.md ADDED
@@ -0,0 +1,408 @@
1
+ # @route-forge/core
2
+
3
+ [English](./README.md) | **中文**
4
+
5
+ 框架无关的 Laravel 命名路由前端客户端核心:从后端 manifest 端点拉取路由元信息,按层级(level)懒加载与隔离缓存,用**路由名**调用 API、生成 URL,全程 TypeScript 类型保护,拦截器行为与 axios 一致。
6
+
7
+ ## 它能做什么
8
+
9
+ - **按路由名调用 API**:`forge.api('admin', 'users.show', { user: 123 })`,不再硬编码路径
10
+ - **分级懒加载**:路由元信息按层级(如 `public` / `admin`)分组拉取,首屏只加载需要的层级
11
+ - **隔离缓存 + 并发去重**:每层级独立缓存(memory / sessionStorage / localStorage,TTL 过期),同层级并发拉取自动合并为一次请求
12
+ - **自动发现**:启动时拉取摘要端点,自动发现层级列表、eager 预加载层级、URL 前缀等配置
13
+ - **拦截器**:请求 / 响应拦截链,`use` / `eject` / `clear` 与 axios API 一致(请求 LIFO、响应 FIFO)
14
+ - **请求取消**:`forge.api()` 返回的 `ForgeRequest` 自带 `abort()`,与超时协同工作
15
+ - **加载状态跟踪**:并发请求计数 + 订阅,可直接驱动全局加载指示
16
+ - **类型安全**:`ForgeRouteMap` 二级映射(codegen 生成或模块增强),路由名 → 参数 → 响应编译期校验
17
+ - **多传输适配**:内置零依赖 `fetch` 实现(默认),可复用宿主 axios,或传入自定义 `Fetcher`
18
+ - **浏览器直接引入**:提供 IIFE 构建,`<script>` 标签即用,无需打包工具
19
+
20
+ ## 安装
21
+
22
+ ```bash
23
+ pnpm add @route-forge/core
24
+ # 可选:宿主安装了 axios 且 adapter 为 'auto'(默认)时会自动复用;
25
+ # 也可显式安装以便强制使用('axios' 模式)
26
+ pnpm add axios
27
+ ```
28
+
29
+ ## 快速开始
30
+
31
+ ```ts
32
+ import { createRouteForge } from '@route-forge/core'
33
+
34
+ const forge = createRouteForge({
35
+ endpoint: '/_forge/routes', // 后端 manifest 端点
36
+ })
37
+
38
+ // 调用 API(自动发现层级 → 自动加载层级 → 填充路径参数 → 发送请求)
39
+ const user = await forge.api('admin', 'users.show', { user: 123 })
40
+
41
+ // 生成 URL(仅拼路径,不发请求)
42
+ const url = forge.route('public', 'login.show') // → '/login'
43
+ const url2 = forge.url('public', 'login.show') // url() 是 route() 的语义别名
44
+
45
+ // 检查路由是否存在 / 获取路由元信息
46
+ forge.hasRoute('admin', 'users.show') // true / false
47
+ forge.getRoutes('admin') // 指定层级的路由表快照(深拷贝)
48
+ forge.getRoutes() // 全部已加载层级(按 level 分组)
49
+
50
+ // 层级加载与缓存管理
51
+ await forge.load('admin') // 加载层级(并发自动去重)
52
+ forge.isLoaded('admin') // 层级是否已缓存
53
+ forge.invalidate('admin') // 失效指定层级
54
+ forge.invalidate(['admin', 'manage']) // 批量失效
55
+ forge.invalidate() // 失效全部
56
+ ```
57
+
58
+ ## 初始化时序与 `ready()`
59
+
60
+ `createRouteForge()` 返回后立即在后台启动 **auto-discovery**(拉取摘要端点),随后预加载 **eager** 层级。
61
+ `ready()` 在两者全部完成后 resolve(resolve 值为 forge 自身,支持链式调用):
62
+
63
+ ```ts
64
+ const forge = createRouteForge({ endpoint: '/_forge/routes' })
65
+
66
+ // 推荐:ready() 后再挂载应用(此时 route()/hasRoute() 等同步方法即刻可用)
67
+ forge.ready()
68
+ .then(() => app.mount('#app'))
69
+ .catch((err) => {
70
+ // 失败必须接住:摘要端点不可达且未显式传 levels 时 ready() 会 reject,
71
+ // 否则用户面对静默白屏
72
+ console.error('[route-forge] init failed', err)
73
+ })
74
+
75
+ // 回调模式:onFulfilled / onRejected(仍返回 Promise)
76
+ forge.ready(
77
+ (f) => console.log('ready!', f),
78
+ (err) => console.error(err),
79
+ )
80
+
81
+ // async/await 风格
82
+ await forge.ready()
83
+ ```
84
+
85
+ 三种加载状态及其跟踪方式:
86
+
87
+ | 阶段 | 说明 | 跟踪方式 |
88
+ |------|------|----------|
89
+ | Auto-discovery | 拉取摘要端点,发现 levels/config | `forge.ready()` |
90
+ | Level load | 拉取某层级路由元数据 | `forge.isLoaded(level)` / `bound.onLevelLoaded()` |
91
+ | API request | 业务接口请求 | `forge.isLoading()` / `forge.onLoadingChange()` |
92
+
93
+ **降级规则**:显式传了 `levels` 时,摘要端点不可达会 `console.warn` 并降级使用显式配置;未传 `levels` 则无降级可用,`ready()` reject(错误为 `HTTPError` / `NetworkError` / `UnknownLevelError`)。
94
+
95
+ **守卫**:auto-discovery 未完成且无显式 `levels` 时,`route()` / `hasRoute()` 抛 `ForgeError (RF_FE_010)`,防止在路由数据未就绪时返回错误结果;`api()` 不受影响(内部自动等待发现完成)。
96
+
97
+ ## 配置选项(`createRouteForge(options)`)
98
+
99
+ | 选项 | 类型 | 默认值 | 说明 |
100
+ |------|------|--------|------|
101
+ | `endpoint` | `string` | — | 摘要/manifest 端点路径(网络来源)。可选:`endpoint`、`summary`、页面内嵌 `window.__ROUTE_FORGE__` 三者必有一,否则 `createRouteForge` 抛 `TypeError` |
102
+ | `summary` | `SummaryResponse` | — | 直接提供摘要数据(测试 / 非全局引导),跳过摘要 HTTP;优先级低于页面内嵌 `window.__ROUTE_FORGE__` |
103
+ | `levels` | `string[]` | 自动发现 | 不传时从摘要自动发现;显式传入时取与后端摘要的**交集**(前端不能声明后端不存在的层级) |
104
+ | `eager` | `string[]` | 后端 `load:'eager'` 层级 | 预加载层级;显式传入时与后端标记取**并集** |
105
+ | `adapter` | `'auto' \| 'axios' \| 'builtin' \| Fetcher` | `'auto'` | 见下方「Adapter 适配」 |
106
+ | `cache.ttl` | `number`(秒) | `3600` | 前端兜底 TTL;后端全局 `config.cache_ttl` 为上限,实际取 `min(后端, 前端)`(前端只能缩短不能延长,`0` 永久,`config.cache_ttl: null` 不缓存) |
107
+ | `cache.storage` | `'memory' \| 'sessionStorage' \| 'localStorage'` | `'memory'` | 缓存介质;storage 模式维护内存镜像并通过 `storage` 事件感知跨 tab 失效 |
108
+ | `interceptors.request` | 数组 | 无 | 声明式请求拦截器:单函数(视为 `onFulfilled`)或 `[onFulfilled?, onRejected?]` 元组 |
109
+ | `interceptors.response` | 数组 | 无 | 声明式响应拦截器,形式同上 |
110
+ | `timeout` | `number`(毫秒) | `30000` | 全局超时;单次请求可用 `params.timeout` 覆盖 |
111
+ | `baseURL` | `string` | `''` | 拼接在所有生成 URL 之前的基础地址 |
112
+ | `strict` | `boolean` | — | **已废弃,传入无效**。前端校验始终开启(层级未声明抛 `UnknownLevelError`、路由名不存在抛 `UnknownRouteError`、必填参数缺失抛 `MissingRouteParamError`),静默忽略会掩盖拼写错误。后端的 `strict_mode` 是 manifest 生成侧语义,与前端无关 |
113
+
114
+ ## 内嵌引导(可选 hydration)
115
+
116
+ 摘要发现按级联只取一个来源:**页面内嵌 `window.__ROUTE_FORGE__` → `createRouteForge({ summary })` → 网络 `GET {endpoint}`**,三者投递的是同一份 `SummaryResponse`。
117
+
118
+ 对 Laravel/Blade 服务端直出的首页,后端 `@forgeSummary` 指令把摘要内联为一个一次性、不可枚举、读后自删的 `window.__ROUTE_FORGE__` 访问器。core 命中它时**跳过摘要 HTTP 往返、同步完成 discovery**——`createRouteForge()` 返回后 `route()` / `ready()` 立即可用,消除首屏"路由未就绪"闪烁。层级路由表**仍按 level 走 HTTP 懒加载**(受保护路由不进公开 HTML)。core 的 module 级 memo 让第二个实例(React StrictMode / 第二个 Provider)在全局自删后仍能复用摘要。
119
+
120
+ 若页面无内嵌(SPA 独立部署 / Vite dev),core 自动回落网络摘要。`createRouteForge({ summary })` 是显式、便于测试/SSR 的入口。
121
+
122
+ > 诚实边界:一次性自删只缩小摘要在 `window` 上的运行时驻留面,数据仍随 HTML 源码可见;这是延迟/闪烁优化,**不是**抗 XSS 或抗网络窃取的硬边界。
123
+
124
+ ## 参数智能解析
125
+
126
+ `forge.api(level, name, params)` 的第三个参数 `params` 支持四类数据:路径参数(平铺)、`query`(查询参数)、`body`(请求体)、`headers`(请求头),外加 `timeout`(单次超时)与 `params`(显式路径参数固定 key):
127
+
128
+ ```ts
129
+ // 平铺路径参数 + 查询参数
130
+ forge.api('admin', 'users.show', { user: 1, query: { include: 'posts' } })
131
+
132
+ // 冲突消解:路由 /search/{query} —— query 为 string 时自动识别为路径参数
133
+ forge.api('admin', 'search.show', { query: 'keyword' })
134
+ // → URL: /search/keyword
135
+
136
+ // 显式 params:同时需要路径参数和 query string(params 优先级最高)
137
+ forge.api('admin', 'search.show', {
138
+ params: { query: 'keyword' }, // → 替换 {query} 占位符
139
+ query: { page: 1 }, // → query string
140
+ body: { detailed: true }, // → 请求体
141
+ headers: { 'X-Trace': 'a1' }, // → 请求头
142
+ timeout: 120_000, // → 单次超时覆盖(默认 30s)
143
+ })
144
+ ```
145
+
146
+ 解析规则(按优先级):
147
+
148
+ 1. `params` 显式指定 → 路径参数,优先级最高
149
+ 2. 其余平铺 key → 路径参数(不覆盖 `params` 中已有的 key)
150
+ 3. 固定 key 按值类型消解:`query` / `headers` 为对象 → 固定用途,为 `string|number` → 路径参数;`body` 为非 `string|number` → 请求体,为 `string|number` → 路径参数
151
+ 4. 可选参数(URI 中 `{param?}`)缺省时替换为空并清理多余 `/`;后端下发的 `parameter_defaults` 在参数缺省时自动兜底
152
+
153
+ ## URL 前缀(`url_prefix`)
154
+
155
+ 后端可在摘要端点 `config.url_prefix` 中下发 URL 前缀,前端生成路由 URL 时自动拼接,无需手动配置:
156
+
157
+ ```ts
158
+ // 1. 路径前缀 — 拼接在 baseURL 之后、路由 URI 之前
159
+ // 后端返回 { "config": { "url_prefix": "/api/v1" } }
160
+ forge.route('public', 'users.show', { user: 1 }) // → '/api/v1/users/1'
161
+
162
+ // 2. 完整 URL(含协议+域名)— 直接作为基础 URL,忽略客户端 baseURL
163
+ // 适用于前后端不同域名的场景
164
+ // 后端返回 { "config": { "url_prefix": "https://api.example.com" } }
165
+ forge.route('public', 'users.show', { user: 1 }) // → 'https://api.example.com/users/1'
166
+ ```
167
+
168
+ > `url_prefix` 为后端权威,前端不能覆盖。不下发或为空字符串时不影响 URL 生成。
169
+
170
+ ## 层级绑定:`forge.use(level, prefix?)`
171
+
172
+ `use()` 是唯一的 level 绑定入口,返回 `BoundForge`——Vue / React / IIFE 共享同一套 API 表面:
173
+
174
+ ```ts
175
+ // 绑定层级 — 自动触发 load,提供快捷方法
176
+ const bound = forge.use('admin')
177
+ bound('users.show', { user: 1 }) // 可直接调用(= bound.api())
178
+ bound.route('users.show') // URL 生成
179
+ bound.level // → 'admin'
180
+ bound.levelLoaded // Promise<void>(core 层;Vue/React 各自特化)
181
+
182
+ // 绑定层级 + 前缀 — 路由名自动拼接(歧义时智能消解:优先 prefix.suffix,回退 suffix 本身)
183
+ const users = forge.use('admin', 'users')
184
+ users('show', { user: 1 }) // → forge.api('admin', 'users.show', ...)
185
+
186
+ // BoundForge 其余方法
187
+ await bound.onLevelLoaded() // 等待 level 加载完成(支持回调形式)
188
+ bound.hasRoute('users.show') // 绑定层级内的路由检查
189
+ bound.useRoutePrefix('posts') // 以新前缀返回新的 BoundForge(原绑定不变)
190
+ // 通用方法均作用于绑定层级:bound.load() / bound.invalidate() / bound.isLoaded()
191
+ // 全局方法照常可用:bound.isLoading() / bound.onLoadingChange()
192
+ ```
193
+
194
+ > `use()` 每次调用都返回新的 `BoundForge`(不缓存);`forge.use()` 不传参时返回 forge 自身。
195
+
196
+ ## 请求取消
197
+
198
+ `forge.api()` 返回 `ForgeRequest`——继承 `Promise`,附加 `abort()` 方法,内部自动管理 `AbortController`:
199
+
200
+ ```ts
201
+ const req = forge.api('admin', 'reports.export', { timeout: 120_000 })
202
+ req.abort() // 请求被中止,Promise reject 为 RequestAbortedError(RF_FE_009)
203
+ ```
204
+
205
+ `abort()` 与超时(`AbortSignal.timeout`)互不冲突,任一触发都会取消请求。拦截器中可通过 `config.signal` 读取 AbortSignal。
206
+
207
+ ## 拦截器与认证
208
+
209
+ 拦截器 API 与 axios 一致(`use` / `eject` / `clear`);请求拦截器 **LIFO**(后注册先执行),响应拦截器 **FIFO**。Route Forge 不内置登录态管理,认证通过拦截器实现:
210
+
211
+ ```ts
212
+ // 声明式(初始化时配置)
213
+ const forge = createRouteForge({
214
+ endpoint: '/_forge/routes',
215
+ interceptors: {
216
+ request: [
217
+ (config) => {
218
+ const token = authStore.getToken()
219
+ if (token) config.headers.Authorization = `Bearer ${token}`
220
+ return config // 必须返回 RequestConfig 对象,否则抛 RF_FE_006
221
+ },
222
+ ],
223
+ response: [
224
+ (resp) => resp.data, // 统一解包:api() 直接 resolve 业务数据
225
+ [undefined, (err) => { // 元组形式:[onFulfilled?, onRejected?]
226
+ if (err instanceof HTTPError && err.context?.status === 401) {
227
+ authStore.logout()
228
+ window.location.href = '/login'
229
+ }
230
+ return Promise.reject(err)
231
+ }],
232
+ ],
233
+ },
234
+ })
235
+
236
+ // 运行时动态注册 / 移除 / 清空
237
+ const id = forge.interceptors.request.use((config) => { /* ... */ return config })
238
+ forge.interceptors.request.eject(id)
239
+ forge.interceptors.request.clear()
240
+ forge.interceptors.response.clear()
241
+ ```
242
+
243
+ **登出清理**示例:
244
+
245
+ ```ts
246
+ function logout() {
247
+ authStore.clearToken()
248
+ forge.invalidate() // 清空路由缓存
249
+ forge.interceptors.request.clear() // 清空拦截器
250
+ forge.interceptors.response.clear()
251
+ }
252
+ ```
253
+
254
+ > `adapter: 'auto'` 复用宿主 axios 时,宿主已注册的 axios 拦截器会先执行,Route Forge 拦截器在其后执行。
255
+ > 元信息拉取(摘要 / 层级路由表)走 adapter 原始通道,不经过业务拦截链,避免被解包类拦截器干扰。
256
+
257
+ ## 加载状态跟踪
258
+
259
+ 核心始终跟踪并发 API 请求的加载状态,无需配置;不需要时不调用相关 API 即可:
260
+
261
+ ```ts
262
+ forge.isLoading() // boolean:是否仍有在途请求
263
+
264
+ const unsub = forge.onLoadingChange((event) => {
265
+ console.log(event.loading) // true / false
266
+ console.log(event.count) // 当前并发请求数
267
+ })
268
+ unsub() // 取消订阅
269
+ ```
270
+
271
+ Vue / React 包可基于 `onLoadingChange` 驱动组件级加载指示。
272
+
273
+ ## 类型安全(可选但推荐)
274
+
275
+ `ForgeRouteMap` 是「层级 → 路由名 → 元信息」的二级映射接口。定义后,`useForge` / `useForgeApi` / `bound()` 等调用的 **level / 路由名 / params 全部自动推断**,拼错路由名在编译期即报错。
276
+
277
+ 两种定义方式:
278
+
279
+ ```bash
280
+ # 方式一:codegen CLI(拉取后端 manifest 生成 .d.ts)
281
+ npx route-forge-codegen \
282
+ --endpoint http://localhost/_forge/routes \
283
+ --out src/types/forge-routes.d.ts \
284
+ [--levels public,admin] [--responseTypes path/to/map.json]
285
+ ```
286
+
287
+ ```ts
288
+ // 方式二:TypeScript 模块增强(手写或配合后端 Artisan 命令 route:forge:types 的产物)
289
+ declare module '@route-forge/core' {
290
+ interface ForgeRouteMap {
291
+ admin: {
292
+ 'users.show': { method: 'GET'; params: { user: string | number }; response: User }
293
+ 'users.index': { method: 'GET'; params: {}; response: User[] }
294
+ }
295
+ }
296
+ }
297
+ ```
298
+
299
+ 后端 Laravel 包([route-forge/route-forge-laravel](https://github.com/route-forge/route-forge-laravel))另提供 `php artisan route:forge:types` 生成同一结构的类型文件。
300
+
301
+ ## 未分配层级(`unassigned`)
302
+
303
+ 后端未标记层级的路由归属一个特殊的 `unassigned` 层级——后端**恒在摘要 `levels` 中注入它**。前端把它与普通层级一视同仁,按 `levels.unassigned.route.uri` 走 HTTP 懒加载:
304
+
305
+ ```ts
306
+ await forge.load('unassigned')
307
+ const data = await forge.api('unassigned', 'some.route')
308
+ ```
309
+
310
+ ## Adapter 适配
311
+
312
+ | `adapter` 取值 | 行为 |
313
+ |----------------|------|
314
+ | `'auto'`(默认) | 动态 `import('axios')` 探测宿主:检测到则复用(继承宿主拦截器 / defaults 配置),否则使用内置 `builtin` |
315
+ | `'axios'` | 强制宿主 axios,未安装抛 `AdapterNotFoundError`(RF_FE_005) |
316
+ | `'builtin'` | 强制内置 fetch 实现(零依赖、min+gzip < 3KB、拦截器行为与 axios 一致) |
317
+ | 自定义 `Fetcher` | 传入实现 `request(config): Promise<ResponseData>` 的对象,完全自定义 |
318
+
319
+ 请求体为 `FormData` / `Blob` / `ArrayBuffer` / `URLSearchParams` / `ReadableStream` 时自动跳过 JSON 序列化(`string` 也原样透传)。
320
+
321
+ ## IIFE 浏览器用法
322
+
323
+ 通过 `<script>` 标签引入后,全局变量 `RouteForge` 可用:
324
+
325
+ ```html
326
+ <!-- 生产版(压缩,约 19 KB / gzip 约 7 KB) -->
327
+ <script src="https://unpkg.com/@route-forge/core/dist/route-forge.global.min.js"></script>
328
+ <script>
329
+ const forge = RouteForge.createRouteForge({ endpoint: '/_forge/routes' })
330
+ forge.ready().then(function (f) {
331
+ const admin = f.use('admin')
332
+ return admin.onLevelLoaded().then(function () {
333
+ return admin('users.show', { user: 1 })
334
+ })
335
+ }).then(function (data) {
336
+ console.log(data)
337
+ })
338
+ </script>
339
+ ```
340
+
341
+ > 必须引用 `dist/` 下的 IIFE 产物;unpkg 裸包名会解析到 CJS 主入口,浏览器无法直接执行。
342
+
343
+ ## 错误参考
344
+
345
+ 所有错误均为 `ForgeError` 子类,携带稳定的 `code` 字段(`ForgeErrorCode` 字面量联合),可按 `code` 分支处理(`switch` 可获穷尽检查):
346
+
347
+ | 错误类 | code | 触发场景 |
348
+ |--------|------|----------|
349
+ | `UnknownRouteError` | `RF_FE_001` | 路由名不存在于已加载层级中 |
350
+ | `UnknownLevelError` | `RF_FE_002` | 层级未在 levels 声明(前端校验始终开启) |
351
+ | `MissingRouteParamError` | `RF_FE_003` | 必填路径参数缺失(无后端默认值);路径参数传入对象同样报此码 |
352
+ | `AdapterNotFoundError` | `RF_FE_005` | `adapter: 'axios'` 但宿主未安装 / 无有效 axios |
353
+ | `InvalidInterceptorReturnError` | `RF_FE_006` | 请求拦截器未返回 RequestConfig 对象 |
354
+ | `NetworkError` | `RF_FE_007` | 网络层失败(DNS、连接被拒等),`cause` 保留原始错误 |
355
+ | `HTTPError` | `RF_FE_008` | HTTP 非 2xx,`context.status` 为状态码 |
356
+ | `RequestAbortedError` | `RF_FE_009` | 请求被 `abort()` / AbortSignal 取消 |
357
+ | `ForgeError`(守卫) | `RF_FE_010` | auto-discovery 未完成时调用 `route()` / `hasRoute()` |
358
+
359
+ 错误对象结构:
360
+
361
+ ```ts
362
+ {
363
+ code: 'RF_FE_008', // 稳定错误码
364
+ route?: string, // 关联路由名
365
+ level?: string, // 关联层级
366
+ context?: Record<string, unknown>, // 附加上下文(如 HTTP 状态码、url、method)
367
+ cause?: unknown, // 原始底层错误
368
+ }
369
+ ```
370
+
371
+ ## 工具导出
372
+
373
+ 除 `createRouteForge` 外,core 包还导出以下工具件,供高级场景按需使用:
374
+
375
+ | 导出 | 说明 |
376
+ |------|------|
377
+ | `createInterceptorManager` | 创建拦截器管理器(`use`/`eject`/`clear`),供自定义 Fetcher 复用统一拦截器实现 |
378
+ | `RouteCache` | 按层级隔离的路由缓存类(memory / sessionStorage / localStorage,TTL 过期),可独立使用 |
379
+ | `LoadingTracker` | 加载状态跟踪器(引用计数 + 订阅),框架适配层可基于它实现全局加载指示 |
380
+ | `resolveRouteName` | 前缀歧义异步消解(`prefix.suffix` 优先,回退后缀本身),`api()` 调用路径使用 |
381
+ | `resolveRouteNameSync` | 前缀歧义同步消解(基于已加载缓存),`route()` / `url()` 调用路径使用 |
382
+
383
+ 类型导出:`RouteForge` / `RouteForgeOptions` / `BoundForge` / `ApiCallParams` / `RequestConfig` / `ResponseData` / `ForgeRequest` / `Fetcher` / `RouteMeta` / `SummaryResponse` / `ForgeRouteMap` / `ForgeErrorCode` 等(完整清单见 `dist/index.d.ts`)。
384
+
385
+ ## 常见问题
386
+
387
+ **`route()` / `hasRoute()` 抛 `RF_FE_010`?**
388
+ auto-discovery 尚未完成。等待 `await forge.ready()` 后再调用,或改用框架包的 `useForgeRoute`(内部处理加载态,未加载时返回 `''`)。
389
+
390
+ **`ready()` reject 了怎么办?**
391
+ 摘要端点不可达且未显式传 `levels` 时 `ready()` 会 reject。要么修复端点连通性,要么显式传 `levels` 获得降级能力(摘要失败时退回显式配置)。
392
+
393
+ **响应没有被 `resp.data` 解包?**
394
+ 解包是响应拦截器行为,需要自行注册 `(resp) => resp.data`;core 默认 resolve 完整的 `ResponseData` 经拦截链后的末段返回值。
395
+
396
+ **跨标签页缓存不同步?**
397
+ `storage` 模式(sessionStorage / localStorage)通过 `storage` 事件自动失效其他 tab 写入的缓存镜像;`memory` 模式仅当前页可见。
398
+
399
+ ## 文档
400
+
401
+ - 仓库主页: <https://github.com/route-forge/route-forge>
402
+ - 设计文档: <https://github.com/route-forge/route-forge/blob/main/.docs/DESIGN.md>
403
+ - 规范: <https://github.com/route-forge/route-forge/blob/main/.docs/SPEC.md>
404
+ - 后端包(Laravel): <https://github.com/route-forge/route-forge-laravel>
405
+
406
+ ## License
407
+
408
+ MIT
package/dist/codegen.cjs CHANGED
@@ -133,38 +133,17 @@ async function main(argv = process.argv.slice(2)) {
133
133
  process.exit(1);
134
134
  }
135
135
  }
136
- const UNASSIGNED_LEVEL = "unassigned";
137
- let unassignedRoutes = null;
138
- const needsUnassigned = levels.includes(UNASSIGNED_LEVEL) || summaryData && summaryData.unassigned && summaryData.unassigned.length > 0;
139
- if (needsUnassigned) {
140
- if (!summaryData) {
141
- try {
142
- summaryData = await fetchSummary(opts.endpoint);
143
- } catch (e) {
144
- console.warn(`[route-forge/codegen] failed to fetch summary for unassigned routes: ${e.message}`);
145
- }
146
- }
147
- if (summaryData?.unassigned && summaryData.unassigned.length > 0) {
148
- unassignedRoutes = {};
149
- for (const r of summaryData.unassigned) {
150
- unassignedRoutes[r.name] = { ...r, level: UNASSIGNED_LEVEL };
151
- }
152
- if (!levels.includes(UNASSIGNED_LEVEL)) {
153
- levels.push(UNASSIGNED_LEVEL);
154
- }
155
- }
156
- }
157
136
  if (levels.length === 0) {
158
137
  console.error("[route-forge/codegen] no levels found; pass --levels explicitly");
159
138
  process.exit(1);
160
139
  }
161
140
  const levelFetches = await Promise.allSettled(
162
- levels.filter((lvl) => lvl !== UNASSIGNED_LEVEL).map((lvl) => fetchLevel(opts.endpoint, lvl))
141
+ levels.map((lvl) => fetchLevel(opts.endpoint, lvl))
163
142
  );
164
143
  const routesByLevel = {};
165
144
  let failedLevels = 0;
166
145
  levelFetches.forEach((res, idx) => {
167
- const lvl = levels.filter((l) => l !== UNASSIGNED_LEVEL)[idx];
146
+ const lvl = levels[idx];
168
147
  if (res.status === "fulfilled") {
169
148
  routesByLevel[lvl] = res.value.routes;
170
149
  } else {
@@ -172,9 +151,6 @@ async function main(argv = process.argv.slice(2)) {
172
151
  console.warn(`[route-forge/codegen] failed to fetch level "${lvl}": ${res.reason.message}`);
173
152
  }
174
153
  });
175
- if (unassignedRoutes) {
176
- routesByLevel[UNASSIGNED_LEVEL] = unassignedRoutes;
177
- }
178
154
  const totalRoutes = Object.values(routesByLevel).reduce((sum, r) => sum + Object.keys(r).length, 0);
179
155
  if (totalRoutes === 0) {
180
156
  console.error("[route-forge/codegen] no routes collected from any level");
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/codegen/index.ts"],"names":[],"mappings":"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","file":"codegen.cjs","sourcesContent":["#!/usr/bin/env node\n/**\n * @route-forge/core codegen CLI\n * @see .docs/SPEC.md §4.2\n *\n * 用法:\n * npx @route-forge/core codegen \\\n * --endpoint http://localhost/_forge/routes \\\n * --levels public,client,manage,admin \\\n * --out src/types/forge-routes.d.ts\n */\n\nimport type { RouteMeta } from '../types.js';\n\nexport interface CodegenOptions {\n endpoint: string;\n levels: string[];\n out: string;\n responseTypes?: string;\n}\n\nconst BODY_METHODS = new Set(['POST', 'PUT', 'PATCH']);\n\n/**\n * 解析 argv(最小手写实现,不引入 commander/yargs)\n * 支持:--endpoint VALUE / --endpoint=VALUE / --levels a,b,c / --out PATH / --responseTypes PATH\n */\nexport function parseArgs(argv: string[]): CodegenOptions {\n const opts: Partial<CodegenOptions> = {};\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n const next = (): string => {\n const v = argv[++i];\n if (v === undefined) {\n throw new Error(`[route-forge/codegen] missing value for ${arg}`);\n }\n return v;\n };\n\n if (arg === '--endpoint') {\n opts.endpoint = next();\n } else if (arg?.startsWith('--endpoint=')) {\n opts.endpoint = arg.slice('--endpoint='.length);\n } else if (arg === '--levels') {\n opts.levels = next().split(',').map((s) => s.trim()).filter(Boolean);\n } else if (arg?.startsWith('--levels=')) {\n opts.levels = arg.slice('--levels='.length).split(',').map((s) => s.trim()).filter(Boolean);\n } else if (arg === '--out') {\n opts.out = next();\n } else if (arg?.startsWith('--out=')) {\n opts.out = arg.slice('--out='.length);\n } else if (arg === '--responseTypes') {\n opts.responseTypes = next();\n } else if (arg?.startsWith('--responseTypes=')) {\n opts.responseTypes = arg.slice('--responseTypes='.length);\n } else if (arg === '--help' || arg === '-h') {\n printHelp();\n process.exit(0);\n }\n }\n\n if (!opts.endpoint) {\n console.error('[route-forge/codegen] --endpoint is required');\n printHelp();\n process.exit(1);\n }\n if (!opts.out) {\n console.error('[route-forge/codegen] --out is required');\n printHelp();\n process.exit(1);\n }\n\n return {\n endpoint: opts.endpoint,\n levels: opts.levels ?? [],\n out: opts.out,\n responseTypes: opts.responseTypes,\n };\n}\n\nfunction printHelp(): void {\n console.log(`\nroute-forge codegen - generate TS route types from backend summary endpoint\n\nUsage:\n npx @route-forge/core codegen --endpoint URL --out PATH [--levels a,b,c] [--responseTypes PATH]\n\nOptions:\n --endpoint URL Backend summary endpoint (e.g. http://localhost/_forge/routes)\n --levels a,b,c Optional: explicit level list (skip auto-discovery)\n --out PATH Output .d.ts file path\n --responseTypes PATH Optional: JSON file mapping route names to response types\n -h, --help Show this help\n`);\n}\n\n/**\n * 拉取摘要端点\n */\nexport async function fetchSummary(endpoint: string): Promise<{\n levels: Record<string, unknown>;\n unassigned?: Array<{\n name: string;\n uri: string;\n methods: string[];\n parameters: string[];\n parameter_defaults?: Record<string, unknown>;\n }>;\n}> {\n const resp = await fetch(endpoint, { method: 'GET' });\n if (!resp.ok) {\n throw new Error(`summary endpoint ${endpoint} returned ${resp.status}`);\n }\n return (await resp.json()) as {\n levels: Record<string, unknown>;\n unassigned?: Array<{\n name: string;\n uri: string;\n methods: string[];\n parameters: string[];\n parameter_defaults?: Record<string, unknown>;\n }>;\n };\n}\n\n/**\n * 拉取单个层级的路由元信息\n */\nexport async function fetchLevel(endpoint: string, level: string): Promise<{ routes: Record<string, RouteMeta> }> {\n const base = endpoint.replace(/\\/$/, '');\n const url = `${base}/${encodeURIComponent(level)}`;\n const resp = await fetch(url, { method: 'GET' });\n if (!resp.ok) {\n throw new Error(`level endpoint ${url} returned ${resp.status}`);\n }\n const data = (await resp.json()) as { routes?: Record<string, RouteMeta> };\n return { routes: data.routes ?? {} };\n}\n\n/**\n * 生成 d.ts 内容\n * @param routesByLevel 按层级分组的路由元信息:{ level: { routeName: meta } }\n */\nexport function generateRouteTypes(routesByLevel: Record<string, Record<string, RouteMeta>>): string {\n const levelEntries = Object.entries(routesByLevel).map(([level, routes]) => {\n const routeEntries = Object.entries(routes).map(([name, meta]) => {\n const defaults = meta.parameter_defaults ?? {};\n const params = (meta.parameters ?? []).map((p) => {\n // URI 中的 {p?} 或有后端默认值的参数 → 生成可选字段,避免调用方被迫传参\n const optional = meta.uri?.includes(`{${p}?}`) || p in defaults;\n return `${p}${optional ? '?' : ''}: string | number;`;\n }).join(' ');\n const method = meta.methods?.find((m) => m.toUpperCase() !== 'HEAD') ?? 'GET';\n const methodUpper = method.toUpperCase();\n const bodyField = BODY_METHODS.has(methodUpper) ? '\\n body: unknown;' : '';\n return ` ${JSON.stringify(name)}: {\\n method: ${JSON.stringify(methodUpper)};\\n params: { ${params} };${bodyField}\\n response: unknown;\\n };`;\n });\n return ` ${JSON.stringify(level)}: {\\n${routeEntries.join('\\n')}\\n };`;\n });\n return `// AUTO-GENERATED by @route-forge/core codegen. Do not edit.\n\n/**\n * 二级路由类型映射:level → routeName → routeMeta\n * 可通过 module augmentation 增强:\n * declare module '@route-forge/core' {\n * interface ForgeRouteMap { admin: { 'users.show': { method: 'GET'; params: { user: string | number }; response: User } } }\n * }\n */\nexport interface ForgeRouteMap {\n${levelEntries.join('\\n')}\n}\n`;\n}\n\n/**\n * CLI 主入口\n */\nexport async function main(argv: string[] = process.argv.slice(2)): Promise<void> {\n const opts = parseArgs(argv);\n\n let levels = opts.levels;\n // 摘要端点数据(用于自动发现层级 + 获取 unassigned 路由)\n let summaryData: Awaited<ReturnType<typeof fetchSummary>> | null = null;\n if (levels.length === 0) {\n try {\n summaryData = await fetchSummary(opts.endpoint);\n levels = Object.keys(summaryData.levels);\n } catch (e) {\n console.error(`[route-forge/codegen] failed to auto-discover levels from summary endpoint: ${(e as Error).message}`);\n console.error('hint: pass --levels explicitly to skip auto-discovery');\n process.exit(1);\n }\n }\n\n // 未指定 --levels 或显式包含 unassigned 时,从摘要端点获取未分配路由\n // @see .docs/SPEC.md §3.1.6\n const UNASSIGNED_LEVEL = 'unassigned';\n let unassignedRoutes: Record<string, RouteMeta> | null = null;\n // 自动发现时检查摘要数据中是否有 unassigned 路由;显式指定 --levels 时仅当包含 unassigned 才处理\n const needsUnassigned =\n levels.includes(UNASSIGNED_LEVEL) || (summaryData && summaryData.unassigned && summaryData.unassigned.length > 0);\n if (needsUnassigned) {\n if (!summaryData) {\n try {\n summaryData = await fetchSummary(opts.endpoint);\n } catch (e) {\n console.warn(`[route-forge/codegen] failed to fetch summary for unassigned routes: ${(e as Error).message}`);\n }\n }\n if (summaryData?.unassigned && summaryData.unassigned.length > 0) {\n unassignedRoutes = {};\n for (const r of summaryData.unassigned) {\n unassignedRoutes[r.name] = { ...r, level: UNASSIGNED_LEVEL };\n }\n if (!levels.includes(UNASSIGNED_LEVEL)) {\n levels.push(UNASSIGNED_LEVEL);\n }\n }\n }\n\n if (levels.length === 0) {\n console.error('[route-forge/codegen] no levels found; pass --levels explicitly');\n process.exit(1);\n }\n\n const levelFetches = await Promise.allSettled(\n levels\n .filter((lvl) => lvl !== UNASSIGNED_LEVEL) // unassigned 不走 HTTP 拉取\n .map((lvl) => fetchLevel(opts.endpoint, lvl)),\n );\n\n const routesByLevel: Record<string, Record<string, RouteMeta>> = {};\n let failedLevels = 0;\n levelFetches.forEach((res, idx) => {\n const lvl = levels.filter((l) => l !== UNASSIGNED_LEVEL)[idx]!;\n if (res.status === 'fulfilled') {\n routesByLevel[lvl] = res.value.routes;\n } else {\n failedLevels++;\n console.warn(`[route-forge/codegen] failed to fetch level \"${lvl}\": ${(res.reason as Error).message}`);\n }\n });\n // unassigned 路由直接从摘要数据填充\n if (unassignedRoutes) {\n routesByLevel[UNASSIGNED_LEVEL] = unassignedRoutes;\n }\n\n const totalRoutes = Object.values(routesByLevel).reduce((sum, r) => sum + Object.keys(r).length, 0);\n if (totalRoutes === 0) {\n console.error('[route-forge/codegen] no routes collected from any level');\n process.exit(1);\n }\n\n const dts = generateRouteTypes(routesByLevel);\n\n const fs = await import('node:fs/promises');\n const path = await import('node:path');\n const outPath = path.resolve(opts.out);\n const dir = path.dirname(outPath);\n await fs.mkdir(dir, { recursive: true });\n await fs.writeFile(outPath, dts, 'utf8');\n\n console.log(`[route-forge/codegen] written ${totalRoutes} routes across ${Object.keys(routesByLevel).length} level(s) to ${outPath}`);\n if (failedLevels > 0) {\n console.warn(`[route-forge/codegen] ${failedLevels} level(s) failed; output may be incomplete`);\n }\n}\n\nconst invokedFromCli = (() => {\n try {\n if (!process.argv[1]) return false;\n const argv1 = process.argv[1].replace(/\\\\/g, '/');\n return argv1.endsWith('/codegen.js') || argv1.endsWith('/codegen.cjs') || argv1.endsWith('/codegen');\n } catch {\n return false;\n }\n})();\n\nif (invokedFromCli) {\n main().catch((err) => {\n console.error(err);\n process.exit(1);\n });\n}\n\nexport { main as runCodegen };\n"]}
1
+ {"version":3,"sources":["../src/codegen/index.ts"],"names":[],"mappings":"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","file":"codegen.cjs","sourcesContent":["#!/usr/bin/env node\n/**\n * @route-forge/core codegen CLI\n * @see .docs/SPEC.md §4.2\n *\n * 用法:\n * npx @route-forge/core codegen \\\n * --endpoint http://localhost/_forge/routes \\\n * --levels public,client,manage,admin \\\n * --out src/types/forge-routes.d.ts\n */\n\nimport type { RouteMeta } from '../types.js';\n\nexport interface CodegenOptions {\n endpoint: string;\n levels: string[];\n out: string;\n responseTypes?: string;\n}\n\nconst BODY_METHODS = new Set(['POST', 'PUT', 'PATCH']);\n\n/**\n * 解析 argv(最小手写实现,不引入 commander/yargs)\n * 支持:--endpoint VALUE / --endpoint=VALUE / --levels a,b,c / --out PATH / --responseTypes PATH\n */\nexport function parseArgs(argv: string[]): CodegenOptions {\n const opts: Partial<CodegenOptions> = {};\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n const next = (): string => {\n const v = argv[++i];\n if (v === undefined) {\n throw new Error(`[route-forge/codegen] missing value for ${arg}`);\n }\n return v;\n };\n\n if (arg === '--endpoint') {\n opts.endpoint = next();\n } else if (arg?.startsWith('--endpoint=')) {\n opts.endpoint = arg.slice('--endpoint='.length);\n } else if (arg === '--levels') {\n opts.levels = next().split(',').map((s) => s.trim()).filter(Boolean);\n } else if (arg?.startsWith('--levels=')) {\n opts.levels = arg.slice('--levels='.length).split(',').map((s) => s.trim()).filter(Boolean);\n } else if (arg === '--out') {\n opts.out = next();\n } else if (arg?.startsWith('--out=')) {\n opts.out = arg.slice('--out='.length);\n } else if (arg === '--responseTypes') {\n opts.responseTypes = next();\n } else if (arg?.startsWith('--responseTypes=')) {\n opts.responseTypes = arg.slice('--responseTypes='.length);\n } else if (arg === '--help' || arg === '-h') {\n printHelp();\n process.exit(0);\n }\n }\n\n if (!opts.endpoint) {\n console.error('[route-forge/codegen] --endpoint is required');\n printHelp();\n process.exit(1);\n }\n if (!opts.out) {\n console.error('[route-forge/codegen] --out is required');\n printHelp();\n process.exit(1);\n }\n\n return {\n endpoint: opts.endpoint,\n levels: opts.levels ?? [],\n out: opts.out,\n responseTypes: opts.responseTypes,\n };\n}\n\nfunction printHelp(): void {\n console.log(`\nroute-forge codegen - generate TS route types from backend summary endpoint\n\nUsage:\n npx @route-forge/core codegen --endpoint URL --out PATH [--levels a,b,c] [--responseTypes PATH]\n\nOptions:\n --endpoint URL Backend summary endpoint (e.g. http://localhost/_forge/routes)\n --levels a,b,c Optional: explicit level list (skip auto-discovery)\n --out PATH Output .d.ts file path\n --responseTypes PATH Optional: JSON file mapping route names to response types\n -h, --help Show this help\n`);\n}\n\n/**\n * 拉取摘要端点(仅用于自动发现层级名;unassigned 现是 levels 中的真实层级,与其余层级一样按 HTTP 懒加载拉取)\n */\nexport async function fetchSummary(endpoint: string): Promise<{\n levels: Record<string, unknown>;\n}> {\n const resp = await fetch(endpoint, { method: 'GET' });\n if (!resp.ok) {\n throw new Error(`summary endpoint ${endpoint} returned ${resp.status}`);\n }\n return (await resp.json()) as { levels: Record<string, unknown> };\n}\n\n/**\n * 拉取单个层级的路由元信息\n */\nexport async function fetchLevel(endpoint: string, level: string): Promise<{ routes: Record<string, RouteMeta> }> {\n const base = endpoint.replace(/\\/$/, '');\n const url = `${base}/${encodeURIComponent(level)}`;\n const resp = await fetch(url, { method: 'GET' });\n if (!resp.ok) {\n throw new Error(`level endpoint ${url} returned ${resp.status}`);\n }\n const data = (await resp.json()) as { routes?: Record<string, RouteMeta> };\n return { routes: data.routes ?? {} };\n}\n\n/**\n * 生成 d.ts 内容\n * @param routesByLevel 按层级分组的路由元信息:{ level: { routeName: meta } }\n */\nexport function generateRouteTypes(routesByLevel: Record<string, Record<string, RouteMeta>>): string {\n const levelEntries = Object.entries(routesByLevel).map(([level, routes]) => {\n const routeEntries = Object.entries(routes).map(([name, meta]) => {\n const defaults = meta.parameter_defaults ?? {};\n const params = (meta.parameters ?? []).map((p) => {\n // URI 中的 {p?} 或有后端默认值的参数 → 生成可选字段,避免调用方被迫传参\n const optional = meta.uri?.includes(`{${p}?}`) || p in defaults;\n return `${p}${optional ? '?' : ''}: string | number;`;\n }).join(' ');\n const method = meta.methods?.find((m) => m.toUpperCase() !== 'HEAD') ?? 'GET';\n const methodUpper = method.toUpperCase();\n const bodyField = BODY_METHODS.has(methodUpper) ? '\\n body: unknown;' : '';\n return ` ${JSON.stringify(name)}: {\\n method: ${JSON.stringify(methodUpper)};\\n params: { ${params} };${bodyField}\\n response: unknown;\\n };`;\n });\n return ` ${JSON.stringify(level)}: {\\n${routeEntries.join('\\n')}\\n };`;\n });\n return `// AUTO-GENERATED by @route-forge/core codegen. Do not edit.\n\n/**\n * 二级路由类型映射:level → routeName → routeMeta\n * 可通过 module augmentation 增强:\n * declare module '@route-forge/core' {\n * interface ForgeRouteMap { admin: { 'users.show': { method: 'GET'; params: { user: string | number }; response: User } } }\n * }\n */\nexport interface ForgeRouteMap {\n${levelEntries.join('\\n')}\n}\n`;\n}\n\n/**\n * CLI 主入口\n */\nexport async function main(argv: string[] = process.argv.slice(2)): Promise<void> {\n const opts = parseArgs(argv);\n\n // 未指定 --levels 时从摘要端点自动发现层级(unassigned 现是 levels 中的真实层级,一并纳入)\n let levels = opts.levels;\n // 摘要端点数据(用于自动发现层级)\n let summaryData: Awaited<ReturnType<typeof fetchSummary>> | null = null;\n if (levels.length === 0) {\n try {\n summaryData = await fetchSummary(opts.endpoint);\n levels = Object.keys(summaryData.levels);\n } catch (e) {\n console.error(`[route-forge/codegen] failed to auto-discover levels from summary endpoint: ${(e as Error).message}`);\n console.error('hint: pass --levels explicitly to skip auto-discovery');\n process.exit(1);\n }\n }\n\n if (levels.length === 0) {\n console.error('[route-forge/codegen] no levels found; pass --levels explicitly');\n process.exit(1);\n }\n\n // 所有层级(含 unassigned 真实层级)统一按 HTTP 懒加载拉取(SPEC §3.1.6)\n const levelFetches = await Promise.allSettled(\n levels.map((lvl) => fetchLevel(opts.endpoint, lvl)),\n );\n\n const routesByLevel: Record<string, Record<string, RouteMeta>> = {};\n let failedLevels = 0;\n levelFetches.forEach((res, idx) => {\n const lvl = levels[idx]!;\n if (res.status === 'fulfilled') {\n routesByLevel[lvl] = res.value.routes;\n } else {\n failedLevels++;\n console.warn(`[route-forge/codegen] failed to fetch level \"${lvl}\": ${(res.reason as Error).message}`);\n }\n });\n\n const totalRoutes = Object.values(routesByLevel).reduce((sum, r) => sum + Object.keys(r).length, 0);\n if (totalRoutes === 0) {\n console.error('[route-forge/codegen] no routes collected from any level');\n process.exit(1);\n }\n\n const dts = generateRouteTypes(routesByLevel);\n\n const fs = await import('node:fs/promises');\n const path = await import('node:path');\n const outPath = path.resolve(opts.out);\n const dir = path.dirname(outPath);\n await fs.mkdir(dir, { recursive: true });\n await fs.writeFile(outPath, dts, 'utf8');\n\n console.log(`[route-forge/codegen] written ${totalRoutes} routes across ${Object.keys(routesByLevel).length} level(s) to ${outPath}`);\n if (failedLevels > 0) {\n console.warn(`[route-forge/codegen] ${failedLevels} level(s) failed; output may be incomplete`);\n }\n}\n\nconst invokedFromCli = (() => {\n try {\n if (!process.argv[1]) return false;\n const argv1 = process.argv[1].replace(/\\\\/g, '/');\n return argv1.endsWith('/codegen.js') || argv1.endsWith('/codegen.cjs') || argv1.endsWith('/codegen');\n } catch {\n return false;\n }\n})();\n\nif (invokedFromCli) {\n main().catch((err) => {\n console.error(err);\n process.exit(1);\n });\n}\n\nexport { main as runCodegen };\n"]}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { R as RouteMeta } from './types-BvLdl02f.cjs';
2
+ import { R as RouteMeta } from './types-CDKE8rw-.cjs';
3
3
 
4
4
  /**
5
5
  * @route-forge/core codegen CLI
@@ -24,17 +24,10 @@ interface CodegenOptions {
24
24
  */
25
25
  declare function parseArgs(argv: string[]): CodegenOptions;
26
26
  /**
27
- * 拉取摘要端点
27
+ * 拉取摘要端点(仅用于自动发现层级名;unassigned 现是 levels 中的真实层级,与其余层级一样按 HTTP 懒加载拉取)
28
28
  */
29
29
  declare function fetchSummary(endpoint: string): Promise<{
30
30
  levels: Record<string, unknown>;
31
- unassigned?: Array<{
32
- name: string;
33
- uri: string;
34
- methods: string[];
35
- parameters: string[];
36
- parameter_defaults?: Record<string, unknown>;
37
- }>;
38
31
  }>;
39
32
  /**
40
33
  * 拉取单个层级的路由元信息
package/dist/codegen.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { R as RouteMeta } from './types-BvLdl02f.js';
2
+ import { R as RouteMeta } from './types-CDKE8rw-.js';
3
3
 
4
4
  /**
5
5
  * @route-forge/core codegen CLI
@@ -24,17 +24,10 @@ interface CodegenOptions {
24
24
  */
25
25
  declare function parseArgs(argv: string[]): CodegenOptions;
26
26
  /**
27
- * 拉取摘要端点
27
+ * 拉取摘要端点(仅用于自动发现层级名;unassigned 现是 levels 中的真实层级,与其余层级一样按 HTTP 懒加载拉取)
28
28
  */
29
29
  declare function fetchSummary(endpoint: string): Promise<{
30
30
  levels: Record<string, unknown>;
31
- unassigned?: Array<{
32
- name: string;
33
- uri: string;
34
- methods: string[];
35
- parameters: string[];
36
- parameter_defaults?: Record<string, unknown>;
37
- }>;
38
31
  }>;
39
32
  /**
40
33
  * 拉取单个层级的路由元信息
package/dist/codegen.js CHANGED
@@ -131,38 +131,17 @@ async function main(argv = process.argv.slice(2)) {
131
131
  process.exit(1);
132
132
  }
133
133
  }
134
- const UNASSIGNED_LEVEL = "unassigned";
135
- let unassignedRoutes = null;
136
- const needsUnassigned = levels.includes(UNASSIGNED_LEVEL) || summaryData && summaryData.unassigned && summaryData.unassigned.length > 0;
137
- if (needsUnassigned) {
138
- if (!summaryData) {
139
- try {
140
- summaryData = await fetchSummary(opts.endpoint);
141
- } catch (e) {
142
- console.warn(`[route-forge/codegen] failed to fetch summary for unassigned routes: ${e.message}`);
143
- }
144
- }
145
- if (summaryData?.unassigned && summaryData.unassigned.length > 0) {
146
- unassignedRoutes = {};
147
- for (const r of summaryData.unassigned) {
148
- unassignedRoutes[r.name] = { ...r, level: UNASSIGNED_LEVEL };
149
- }
150
- if (!levels.includes(UNASSIGNED_LEVEL)) {
151
- levels.push(UNASSIGNED_LEVEL);
152
- }
153
- }
154
- }
155
134
  if (levels.length === 0) {
156
135
  console.error("[route-forge/codegen] no levels found; pass --levels explicitly");
157
136
  process.exit(1);
158
137
  }
159
138
  const levelFetches = await Promise.allSettled(
160
- levels.filter((lvl) => lvl !== UNASSIGNED_LEVEL).map((lvl) => fetchLevel(opts.endpoint, lvl))
139
+ levels.map((lvl) => fetchLevel(opts.endpoint, lvl))
161
140
  );
162
141
  const routesByLevel = {};
163
142
  let failedLevels = 0;
164
143
  levelFetches.forEach((res, idx) => {
165
- const lvl = levels.filter((l) => l !== UNASSIGNED_LEVEL)[idx];
144
+ const lvl = levels[idx];
166
145
  if (res.status === "fulfilled") {
167
146
  routesByLevel[lvl] = res.value.routes;
168
147
  } else {
@@ -170,9 +149,6 @@ async function main(argv = process.argv.slice(2)) {
170
149
  console.warn(`[route-forge/codegen] failed to fetch level "${lvl}": ${res.reason.message}`);
171
150
  }
172
151
  });
173
- if (unassignedRoutes) {
174
- routesByLevel[UNASSIGNED_LEVEL] = unassignedRoutes;
175
- }
176
152
  const totalRoutes = Object.values(routesByLevel).reduce((sum, r) => sum + Object.keys(r).length, 0);
177
153
  if (totalRoutes === 0) {
178
154
  console.error("[route-forge/codegen] no routes collected from any level");