@route-forge/core 2.2.1 → 3.1.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.md +31 -23
- package/README_zh.md +30 -23
- package/dist/codegen.cjs +96 -69
- package/dist/codegen.cjs.map +1 -1
- package/dist/codegen.d.cts +23 -15
- package/dist/codegen.d.ts +23 -15
- package/dist/codegen.js +96 -69
- package/dist/codegen.js.map +1 -1
- package/dist/index.cjs +465 -210
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +522 -24
- package/dist/index.d.ts +522 -24
- package/dist/index.js +463 -211
- package/dist/index.js.map +1 -1
- package/dist/manifest-bjy4P_B4.d.cts +76 -0
- package/dist/manifest-bjy4P_B4.d.ts +76 -0
- package/dist/route-forge.global.js +465 -210
- package/dist/route-forge.global.js.map +1 -1
- package/dist/route-forge.global.min.js +2 -2
- package/package.json +4 -1
- package/dist/types-CDKE8rw-.d.cts +0 -462
- package/dist/types-CDKE8rw-.d.ts +0 -462
package/README.md
CHANGED
|
@@ -44,8 +44,9 @@ const url2 = forge.url('public', 'login.show') // url() is a semantic alias o
|
|
|
44
44
|
|
|
45
45
|
// Route existence / metadata inspection
|
|
46
46
|
forge.hasRoute('admin', 'users.show') // true / false
|
|
47
|
-
forge.getRoutes('admin') // snapshot of one level (deep copy)
|
|
47
|
+
forge.getRoutes('admin') // snapshot of one level (deep copy; throws UnknownLevelError if the level is not declared)
|
|
48
48
|
forge.getRoutes() // all loaded levels, grouped by level
|
|
49
|
+
forge.getLevels() // declared levels (incl. unassigned); [] until ready on network bootstrap
|
|
49
50
|
|
|
50
51
|
// Level loading & cache management
|
|
51
52
|
await forge.load('admin') // load a level (concurrent calls deduplicated)
|
|
@@ -97,17 +98,18 @@ The three loading phases and how to track them:
|
|
|
97
98
|
|
|
98
99
|
| Option | Type | Default | Description |
|
|
99
100
|
|--------|------|---------|-------------|
|
|
100
|
-
| `endpoint` | `string` |
|
|
101
|
+
| `endpoint` | `string` | `/_forge/routes` | summary/manifest endpoint path (network source). Optional: when all three sources (`endpoint` / `summary` / embedded `window.__ROUTE_FORGE__`) are absent, network bootstrap falls back to the backend-agreed default summary endpoint `/_forge/routes` (no throw) |
|
|
101
102
|
| `summary` | `SummaryResponse` | — | Provide the summary directly (tests / non-global bootstrap), skipping the summary HTTP request. Takes lower priority than an embedded `window.__ROUTE_FORGE__` |
|
|
102
103
|
| `levels` | `string[]` | auto-discovered | discovered from the summary when omitted; when given, intersected with the backend summary (the frontend cannot declare levels the backend doesn't know) |
|
|
103
104
|
| `eager` | `string[]` | backend `load:'eager'` levels | levels preloaded after discovery; union with the backend marks when given |
|
|
104
105
|
| `adapter` | `'auto' \| 'axios' \| 'builtin' \| Fetcher` | `'auto'` | see "Adapters" below |
|
|
105
106
|
| `cache.ttl` | `number` (seconds) | `3600` | frontend fallback TTL; the backend's global `config.cache_ttl` is the ceiling — the effective TTL is `min(backend, frontend)` (frontend may shorten but never extend; `0` = forever; `config.cache_ttl: null` = don't cache) |
|
|
106
107
|
| `cache.storage` | `'memory' \| 'sessionStorage' \| 'localStorage'` | `'memory'` | cache backend; storage modes keep an in-memory mirror and invalidate cross-tab writes via `storage` events |
|
|
107
|
-
| `interceptors.request` |
|
|
108
|
-
| `interceptors.response` |
|
|
108
|
+
| `interceptors.request` | `Fn \| [resolve?, reject?] \| { resolve?, reject? }` | none | declarative **single** request interceptor: a function (→ `resolve`), a `[resolve?, reject?]` tuple, or a `{ resolve?, reject? }` object; register multiple via runtime `use()` |
|
|
109
|
+
| `interceptors.response` | `Fn \| [resolve?, reject?] \| { resolve?, reject? }` | none | declarative response interceptor, same shapes (first `resolve` receives `ResponseData`) |
|
|
109
110
|
| `timeout` | `number` (ms) | `30000` | global timeout; a single call can override it via `params.timeout` |
|
|
110
111
|
| `baseURL` | `string` | `''` | base prepended to every generated URL |
|
|
112
|
+
| `warnings` | `boolean` | `true` | silences non-fatal `console.warn` output when `false` (`console.error` is never silenced); vue/react render-degrade warnings follow the same switch via `forge.warnings` |
|
|
111
113
|
| `strict` | `boolean` | — | **Deprecated, ignored.** Frontend validation is always on (unknown level → `UnknownLevelError`, unknown route → `UnknownRouteError`, missing required param → `MissingRouteParamError`); silently ignoring typos hides bugs. The backend's `strict_mode` is a manifest-generation concern and unrelated to the frontend |
|
|
112
114
|
|
|
113
115
|
## Embedded bootstrap (optional hydration)
|
|
@@ -191,7 +193,7 @@ bound.useRoutePrefix('posts') // returns a NEW BoundForge with the new p
|
|
|
191
193
|
// Global methods still work: bound.isLoading() / bound.onLoadingChange()
|
|
192
194
|
```
|
|
193
195
|
|
|
194
|
-
> Every `use()` call returns a fresh `BoundForge` (not cached); `forge.use()` without arguments returns the forge itself.
|
|
196
|
+
> Every `use()` call returns a fresh `BoundForge` (not cached); `forge.use()` without arguments returns the forge itself. A `prefix` with trailing separators is normalized before joining (`use('admin', 'users.')` + `'show'` resolves to `admin.users.show`, not `admin.users..show`); the exposed `prefix` keeps its original value.
|
|
195
197
|
|
|
196
198
|
## Request cancellation
|
|
197
199
|
|
|
@@ -213,27 +215,28 @@ The interceptor API matches axios (`use` / `eject` / `clear`); request intercept
|
|
|
213
215
|
const forge = createRouteForge({
|
|
214
216
|
endpoint: '/_forge/routes',
|
|
215
217
|
interceptors: {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
218
|
+
// Declarative config describes ONE interceptor: a function (→ resolve),
|
|
219
|
+
// a [resolve?, reject?] tuple, or a { resolve?, reject? } object.
|
|
220
|
+
// To register several, call forge.interceptors.*.use() at runtime.
|
|
221
|
+
request: (config) => {
|
|
222
|
+
const token = authStore.getToken()
|
|
223
|
+
if (token) config.headers.Authorization = `Bearer ${token}`
|
|
224
|
+
return config // must return a RequestConfig object, otherwise RF_FE_006 is thrown
|
|
225
|
+
},
|
|
226
|
+
response: {
|
|
227
|
+
resolve: (resp) => resp.data, // unwrap: api() resolves with business data directly
|
|
228
|
+
reject: (err) => { // { resolve, reject } object form
|
|
226
229
|
if (err instanceof HTTPError && err.context?.status === 401) {
|
|
227
230
|
authStore.logout()
|
|
228
231
|
window.location.href = '/login'
|
|
229
232
|
}
|
|
230
233
|
return Promise.reject(err)
|
|
231
|
-
}
|
|
232
|
-
|
|
234
|
+
},
|
|
235
|
+
},
|
|
233
236
|
},
|
|
234
237
|
})
|
|
235
238
|
|
|
236
|
-
// Runtime registration / removal / clearing
|
|
239
|
+
// Runtime registration / removal / clearing — call use() repeatedly to register multiple interceptors
|
|
237
240
|
const id = forge.interceptors.request.use((config) => { /* ... */ return config })
|
|
238
241
|
forge.interceptors.request.eject(id)
|
|
239
242
|
forge.interceptors.request.clear()
|
|
@@ -251,7 +254,7 @@ function logout() {
|
|
|
251
254
|
}
|
|
252
255
|
```
|
|
253
256
|
|
|
254
|
-
> With `adapter: 'auto'` reusing host axios, interceptors
|
|
257
|
+
> With `adapter: 'auto'` reusing host axios, the ordering differs per chain: Route Forge request interceptors run **before** host axios request interceptors (core finishes the forge request chain, then calls `axios.request()`), while host response interceptors run inside axios, **before** the Route Forge response chain.
|
|
255
258
|
> Metadata fetching (summary / level tables) goes through the adapter's raw channel and never passes the business interceptor chains, so unwrapping interceptors can't corrupt it.
|
|
256
259
|
|
|
257
260
|
## Loading-state tracking
|
|
@@ -281,7 +284,7 @@ Two ways to define it:
|
|
|
281
284
|
npx route-forge-codegen \
|
|
282
285
|
--endpoint http://localhost/_forge/routes \
|
|
283
286
|
--out src/types/forge-routes.d.ts \
|
|
284
|
-
[--levels public,admin]
|
|
287
|
+
[--levels public,admin]
|
|
285
288
|
```
|
|
286
289
|
|
|
287
290
|
```ts
|
|
@@ -348,13 +351,14 @@ All errors extend `ForgeError` and carry a stable `code` field (the `ForgeErrorC
|
|
|
348
351
|
|-------------|------|---------|
|
|
349
352
|
| `UnknownRouteError` | `RF_FE_001` | route name not found in the loaded level |
|
|
350
353
|
| `UnknownLevelError` | `RF_FE_002` | level not declared (frontend validation is always on) |
|
|
351
|
-
| `MissingRouteParamError` | `RF_FE_003` | required path parameter missing (no backend default)
|
|
354
|
+
| `MissingRouteParamError` | `RF_FE_003` | required path parameter missing (no backend default) |
|
|
355
|
+
| `InvalidPathParamError` | `RF_FE_003` | path parameter received a non-primitive value (object/array) |
|
|
352
356
|
| `AdapterNotFoundError` | `RF_FE_005` | `adapter: 'axios'` but no usable host axios |
|
|
353
357
|
| `InvalidInterceptorReturnError` | `RF_FE_006` | a request interceptor did not return a RequestConfig object |
|
|
354
358
|
| `NetworkError` | `RF_FE_007` | network-layer failure (DNS, refused connection…); `cause` keeps the original error |
|
|
355
|
-
| `HTTPError` | `RF_FE_008` | non-2xx HTTP response; `context.status` holds the status code |
|
|
359
|
+
| `HTTPError` | `RF_FE_008` | non-2xx HTTP response; `context.status` holds the status code; `response` carries the full ResponseData (e.g. Laravel 422 validation errors via `err.response.data.errors`) |
|
|
356
360
|
| `RequestAbortedError` | `RF_FE_009` | request cancelled via `abort()` / AbortSignal |
|
|
357
|
-
| `
|
|
361
|
+
| `DiscoveryNotReadyError` | `RF_FE_010` | `route()` / `hasRoute()` called before auto-discovery completed (sync guard; `api()` awaits discovery internally) |
|
|
358
362
|
|
|
359
363
|
Error object shape:
|
|
360
364
|
|
|
@@ -365,9 +369,13 @@ Error object shape:
|
|
|
365
369
|
level?: string, // related level
|
|
366
370
|
context?: Record<string, unknown>, // extra context (HTTP status, url, method…)
|
|
367
371
|
cause?: unknown, // original underlying error
|
|
372
|
+
response?: ResponseData, // HTTPError only: full response (status/headers/data/config),
|
|
373
|
+
// inspectable step by step in the onRejected chain and final catch
|
|
368
374
|
}
|
|
369
375
|
```
|
|
370
376
|
|
|
377
|
+
> Typical use — Laravel 422 field-level error echo: `err.response.data.errors` is directly available in both the response interceptor and the final catch, no extra request needed.
|
|
378
|
+
|
|
371
379
|
## Utility exports
|
|
372
380
|
|
|
373
381
|
Besides `createRouteForge`, the core package exports these building blocks for advanced scenarios:
|
package/README_zh.md
CHANGED
|
@@ -44,8 +44,9 @@ const url2 = forge.url('public', 'login.show') // url() 是 route() 的语义
|
|
|
44
44
|
|
|
45
45
|
// 检查路由是否存在 / 获取路由元信息
|
|
46
46
|
forge.hasRoute('admin', 'users.show') // true / false
|
|
47
|
-
forge.getRoutes('admin') //
|
|
47
|
+
forge.getRoutes('admin') // 指定层级的路由表快照(深拷贝;层级未声明抛 UnknownLevelError)
|
|
48
48
|
forge.getRoutes() // 全部已加载层级(按 level 分组)
|
|
49
|
+
forge.getLevels() // 已声明层级(含 unassigned);网络引导就绪前为 []
|
|
49
50
|
|
|
50
51
|
// 层级加载与缓存管理
|
|
51
52
|
await forge.load('admin') // 加载层级(并发自动去重)
|
|
@@ -98,17 +99,18 @@ await forge.ready()
|
|
|
98
99
|
|
|
99
100
|
| 选项 | 类型 | 默认值 | 说明 |
|
|
100
101
|
|------|------|--------|------|
|
|
101
|
-
| `endpoint` | `string` |
|
|
102
|
+
| `endpoint` | `string` | `/_forge/routes` | 摘要/manifest 端点路径(网络来源)。可省略:`endpoint` / `summary` / 页面内嵌 `window.__ROUTE_FORGE__` 三源皆无时,网络引导回退到与后端约定的默认摘要端点 `/_forge/routes`(不再抛错) |
|
|
102
103
|
| `summary` | `SummaryResponse` | — | 直接提供摘要数据(测试 / 非全局引导),跳过摘要 HTTP;优先级低于页面内嵌 `window.__ROUTE_FORGE__` |
|
|
103
104
|
| `levels` | `string[]` | 自动发现 | 不传时从摘要自动发现;显式传入时取与后端摘要的**交集**(前端不能声明后端不存在的层级) |
|
|
104
105
|
| `eager` | `string[]` | 后端 `load:'eager'` 层级 | 预加载层级;显式传入时与后端标记取**并集** |
|
|
105
106
|
| `adapter` | `'auto' \| 'axios' \| 'builtin' \| Fetcher` | `'auto'` | 见下方「Adapter 适配」 |
|
|
106
107
|
| `cache.ttl` | `number`(秒) | `3600` | 前端兜底 TTL;后端全局 `config.cache_ttl` 为上限,实际取 `min(后端, 前端)`(前端只能缩短不能延长,`0` 永久,`config.cache_ttl: null` 不缓存) |
|
|
107
108
|
| `cache.storage` | `'memory' \| 'sessionStorage' \| 'localStorage'` | `'memory'` | 缓存介质;storage 模式维护内存镜像并通过 `storage` 事件感知跨 tab 失效 |
|
|
108
|
-
| `interceptors.request` |
|
|
109
|
-
| `interceptors.response` |
|
|
109
|
+
| `interceptors.request` | `Fn \| [resolve?, reject?] \| { resolve?, reject? }` | 无 | 声明式**单个**请求拦截器:函数(→ `resolve`)、`[resolve?, reject?]` 元组或 `{ resolve?, reject? }` 对象;需要多个改用运行时 `use()` |
|
|
110
|
+
| `interceptors.response` | `Fn \| [resolve?, reject?] \| { resolve?, reject? }` | 无 | 声明式响应拦截器,形式同上(首段 `resolve` 接收 `ResponseData`) |
|
|
110
111
|
| `timeout` | `number`(毫秒) | `30000` | 全局超时;单次请求可用 `params.timeout` 覆盖 |
|
|
111
112
|
| `baseURL` | `string` | `''` | 拼接在所有生成 URL 之前的基础地址 |
|
|
113
|
+
| `warnings` | `boolean` | `true` | 设为 `false` 时静音非致命 `console.warn`(`console.error` 永不静音);vue/react 渲染期降级警告经 `forge.warnings` 遵循同一开关 |
|
|
112
114
|
| `strict` | `boolean` | — | **已废弃,传入无效**。前端校验始终开启(层级未声明抛 `UnknownLevelError`、路由名不存在抛 `UnknownRouteError`、必填参数缺失抛 `MissingRouteParamError`),静默忽略会掩盖拼写错误。后端的 `strict_mode` 是 manifest 生成侧语义,与前端无关 |
|
|
113
115
|
|
|
114
116
|
## 内嵌引导(可选 hydration)
|
|
@@ -191,7 +193,7 @@ bound.useRoutePrefix('posts') // 以新前缀返回新的 BoundForge(
|
|
|
191
193
|
// 全局方法照常可用:bound.isLoading() / bound.onLoadingChange()
|
|
192
194
|
```
|
|
193
195
|
|
|
194
|
-
> `use()` 每次调用都返回新的 `BoundForge`(不缓存);`forge.use()` 不传参时返回 forge 自身。
|
|
196
|
+
> `use()` 每次调用都返回新的 `BoundForge`(不缓存);`forge.use()` 不传参时返回 forge 自身。prefix 尾部的 separator(含连续多个)会先归一化再拼接(`use('admin', 'users.')` + `'show'` 解析为 `admin.users.show` 而非 `admin.users..show`);`bound.prefix` 暴露值保持原样。
|
|
195
197
|
|
|
196
198
|
## 请求取消
|
|
197
199
|
|
|
@@ -213,27 +215,27 @@ req.abort() // 请求被中止,Promise reject 为 RequestAbortedError(RF_F
|
|
|
213
215
|
const forge = createRouteForge({
|
|
214
216
|
endpoint: '/_forge/routes',
|
|
215
217
|
interceptors: {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
response:
|
|
224
|
-
(resp) => resp.data,
|
|
225
|
-
|
|
218
|
+
// 声明式配置每个键只描述「一个」拦截器:函数(→ resolve)、[resolve?, reject?] 元组、或 { resolve?, reject? } 对象。
|
|
219
|
+
// 需要注册多个?改用运行时 forge.interceptors.*.use()。
|
|
220
|
+
request: (config) => {
|
|
221
|
+
const token = authStore.getToken()
|
|
222
|
+
if (token) config.headers.Authorization = `Bearer ${token}`
|
|
223
|
+
return config // 必须返回 RequestConfig 对象,否则抛 RF_FE_006
|
|
224
|
+
},
|
|
225
|
+
response: {
|
|
226
|
+
resolve: (resp) => resp.data, // 统一解包:api() 直接 resolve 业务数据
|
|
227
|
+
reject: (err) => { // { resolve, reject } 对象形式
|
|
226
228
|
if (err instanceof HTTPError && err.context?.status === 401) {
|
|
227
229
|
authStore.logout()
|
|
228
230
|
window.location.href = '/login'
|
|
229
231
|
}
|
|
230
232
|
return Promise.reject(err)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
+
},
|
|
234
|
+
},
|
|
233
235
|
},
|
|
234
236
|
})
|
|
235
237
|
|
|
236
|
-
// 运行时动态注册 / 移除 / 清空
|
|
238
|
+
// 运行时动态注册 / 移除 / 清空 —— 可多次 use() 注册多个拦截器
|
|
237
239
|
const id = forge.interceptors.request.use((config) => { /* ... */ return config })
|
|
238
240
|
forge.interceptors.request.eject(id)
|
|
239
241
|
forge.interceptors.request.clear()
|
|
@@ -251,7 +253,7 @@ function logout() {
|
|
|
251
253
|
}
|
|
252
254
|
```
|
|
253
255
|
|
|
254
|
-
> `adapter: 'auto'` 复用宿主 axios
|
|
256
|
+
> `adapter: 'auto'` 复用宿主 axios 时,执行顺序按链路方向区分:Route Forge 请求拦截链先执行(core 先跑完 forge 请求链,再调用 `axios.request()`),宿主请求拦截器在其后;响应链则宿主响应拦截器在 axios 内部先行,Route Forge 响应拦截链在其后执行。
|
|
255
257
|
> 元信息拉取(摘要 / 层级路由表)走 adapter 原始通道,不经过业务拦截链,避免被解包类拦截器干扰。
|
|
256
258
|
|
|
257
259
|
## 加载状态跟踪
|
|
@@ -281,7 +283,7 @@ Vue / React 包可基于 `onLoadingChange` 驱动组件级加载指示。
|
|
|
281
283
|
npx route-forge-codegen \
|
|
282
284
|
--endpoint http://localhost/_forge/routes \
|
|
283
285
|
--out src/types/forge-routes.d.ts \
|
|
284
|
-
[--levels public,admin]
|
|
286
|
+
[--levels public,admin]
|
|
285
287
|
```
|
|
286
288
|
|
|
287
289
|
```ts
|
|
@@ -348,13 +350,14 @@ const data = await forge.api('unassigned', 'some.route')
|
|
|
348
350
|
|--------|------|----------|
|
|
349
351
|
| `UnknownRouteError` | `RF_FE_001` | 路由名不存在于已加载层级中 |
|
|
350
352
|
| `UnknownLevelError` | `RF_FE_002` | 层级未在 levels 声明(前端校验始终开启) |
|
|
351
|
-
| `MissingRouteParamError` | `RF_FE_003` |
|
|
353
|
+
| `MissingRouteParamError` | `RF_FE_003` | 必填路径参数缺失(无后端默认值) |
|
|
354
|
+
| `InvalidPathParamError` | `RF_FE_003` | 路径参数收到非原始值(对象/数组) |
|
|
352
355
|
| `AdapterNotFoundError` | `RF_FE_005` | `adapter: 'axios'` 但宿主未安装 / 无有效 axios |
|
|
353
356
|
| `InvalidInterceptorReturnError` | `RF_FE_006` | 请求拦截器未返回 RequestConfig 对象 |
|
|
354
357
|
| `NetworkError` | `RF_FE_007` | 网络层失败(DNS、连接被拒等),`cause` 保留原始错误 |
|
|
355
|
-
| `HTTPError` | `RF_FE_008` | HTTP 非 2xx,`context.status`
|
|
358
|
+
| `HTTPError` | `RF_FE_008` | HTTP 非 2xx,`context.status` 为状态码;`response` 携带完整 ResponseData(如 Laravel 422 校验错误 `err.response.data.errors`) |
|
|
356
359
|
| `RequestAbortedError` | `RF_FE_009` | 请求被 `abort()` / AbortSignal 取消 |
|
|
357
|
-
| `
|
|
360
|
+
| `DiscoveryNotReadyError` | `RF_FE_010` | auto-discovery 未完成时调用 `route()` / `hasRoute()`(同步守卫;`api()` 内部自动 await discovery) |
|
|
358
361
|
|
|
359
362
|
错误对象结构:
|
|
360
363
|
|
|
@@ -365,9 +368,13 @@ const data = await forge.api('unassigned', 'some.route')
|
|
|
365
368
|
level?: string, // 关联层级
|
|
366
369
|
context?: Record<string, unknown>, // 附加上下文(如 HTTP 状态码、url、method)
|
|
367
370
|
cause?: unknown, // 原始底层错误
|
|
371
|
+
response?: ResponseData, // 仅 HTTPError:完整响应(status/headers/data/config),
|
|
372
|
+
// 供响应拦截器 onRejected 与最终 catch 逐段检查响应体
|
|
368
373
|
}
|
|
369
374
|
```
|
|
370
375
|
|
|
376
|
+
> 典型场景——Laravel 422 字段级错误回显:`err.response.data.errors` 直接可用,响应拦截器与最终 catch 都能读到,无需再发请求。
|
|
377
|
+
|
|
371
378
|
## 工具导出
|
|
372
379
|
|
|
373
380
|
除 `createRouteForge` 外,core 包还导出以下工具件,供高级场景按需使用:
|
package/dist/codegen.cjs
CHANGED
|
@@ -1,8 +1,81 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
// src/
|
|
4
|
+
// src/url/utils.ts
|
|
5
|
+
function trimTrailingSlash(s) {
|
|
6
|
+
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
7
|
+
}
|
|
8
|
+
function withLeadingSlash(s) {
|
|
9
|
+
return s.startsWith("/") ? s : `/${s}`;
|
|
10
|
+
}
|
|
11
|
+
function joinBaseAndPath(base, path) {
|
|
12
|
+
return `${trimTrailingSlash(base)}${withLeadingSlash(path)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/codegen/summary-client.ts
|
|
16
|
+
async function fetchSummary(endpoint) {
|
|
17
|
+
const resp = await fetch(endpoint, { method: "GET" });
|
|
18
|
+
if (!resp.ok) {
|
|
19
|
+
throw new Error(`summary endpoint ${endpoint} returned ${resp.status}`);
|
|
20
|
+
}
|
|
21
|
+
return await resp.json();
|
|
22
|
+
}
|
|
23
|
+
function levelUrlFromUri(endpoint, uri) {
|
|
24
|
+
try {
|
|
25
|
+
return joinBaseAndPath(new URL(endpoint).origin, uri);
|
|
26
|
+
} catch {
|
|
27
|
+
return uri;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function fetchLevel(endpoint, level, routeUri) {
|
|
31
|
+
const url = routeUri ? levelUrlFromUri(endpoint, routeUri) : `${endpoint.replace(/\/$/, "")}/${encodeURIComponent(level)}`;
|
|
32
|
+
const resp = await fetch(url, { method: "GET" });
|
|
33
|
+
if (!resp.ok) {
|
|
34
|
+
throw new Error(`level endpoint ${url} returned ${resp.status}`);
|
|
35
|
+
}
|
|
36
|
+
const data = await resp.json();
|
|
37
|
+
return { routes: data.routes ?? {} };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/codegen/emit.ts
|
|
5
41
|
var BODY_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
|
|
42
|
+
function generateRouteTypes(routesByLevel) {
|
|
43
|
+
const levelEntries = Object.entries(routesByLevel).map(([level, routes]) => {
|
|
44
|
+
const routeEntries = Object.entries(routes).map(([name, meta]) => {
|
|
45
|
+
const defaults = meta.parameter_defaults ?? {};
|
|
46
|
+
const params = (meta.parameters ?? []).map((p) => {
|
|
47
|
+
const optional = meta.uri?.includes(`{${p}?}`) || p in defaults;
|
|
48
|
+
return `${p}${optional ? "?" : ""}: string | number;`;
|
|
49
|
+
}).join(" ");
|
|
50
|
+
const method = meta.methods?.find((m) => m.toUpperCase() !== "HEAD") ?? "GET";
|
|
51
|
+
const methodUpper = method.toUpperCase();
|
|
52
|
+
const bodyField = BODY_METHODS.has(methodUpper) ? "\n body: unknown;" : "";
|
|
53
|
+
return ` ${JSON.stringify(name)}: {
|
|
54
|
+
method: ${JSON.stringify(methodUpper)};
|
|
55
|
+
params: { ${params} };${bodyField}
|
|
56
|
+
response: unknown;
|
|
57
|
+
};`;
|
|
58
|
+
});
|
|
59
|
+
return ` ${JSON.stringify(level)}: {
|
|
60
|
+
${routeEntries.join("\n")}
|
|
61
|
+
};`;
|
|
62
|
+
});
|
|
63
|
+
return `// AUTO-GENERATED by @route-forge/core codegen. Do not edit.
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* \u4E8C\u7EA7\u8DEF\u7531\u7C7B\u578B\u6620\u5C04\uFF1Alevel \u2192 routeName \u2192 routeMeta
|
|
67
|
+
* \u53EF\u901A\u8FC7 module augmentation \u589E\u5F3A\uFF1A
|
|
68
|
+
* declare module '@route-forge/core' {
|
|
69
|
+
* interface ForgeRouteMap { admin: { 'users.show': { method: 'GET'; params: { user: string | number }; response: User } } }
|
|
70
|
+
* }
|
|
71
|
+
*/
|
|
72
|
+
export interface ForgeRouteMap {
|
|
73
|
+
${levelEntries.join("\n")}
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/codegen/cli.ts
|
|
6
79
|
function parseArgs(argv) {
|
|
7
80
|
const opts = {};
|
|
8
81
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -26,10 +99,11 @@ function parseArgs(argv) {
|
|
|
26
99
|
opts.out = next();
|
|
27
100
|
} else if (arg?.startsWith("--out=")) {
|
|
28
101
|
opts.out = arg.slice("--out=".length);
|
|
29
|
-
} else if (arg === "--responseTypes") {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
102
|
+
} else if (arg === "--responseTypes" || arg?.startsWith("--responseTypes=")) {
|
|
103
|
+
console.error(
|
|
104
|
+
'[route-forge/codegen] --responseTypes has been removed (it was never implemented). Edit the "response" field in the generated d.ts directly, or use module augmentation on ForgeRouteMap.'
|
|
105
|
+
);
|
|
106
|
+
process.exit(1);
|
|
33
107
|
} else if (arg === "--help" || arg === "-h") {
|
|
34
108
|
printHelp();
|
|
35
109
|
process.exit(0);
|
|
@@ -48,8 +122,7 @@ function parseArgs(argv) {
|
|
|
48
122
|
return {
|
|
49
123
|
endpoint: opts.endpoint,
|
|
50
124
|
levels: opts.levels ?? [],
|
|
51
|
-
out: opts.out
|
|
52
|
-
responseTypes: opts.responseTypes
|
|
125
|
+
out: opts.out
|
|
53
126
|
};
|
|
54
127
|
}
|
|
55
128
|
function printHelp() {
|
|
@@ -57,88 +130,40 @@ function printHelp() {
|
|
|
57
130
|
route-forge codegen - generate TS route types from backend summary endpoint
|
|
58
131
|
|
|
59
132
|
Usage:
|
|
60
|
-
npx @route-forge/core codegen --endpoint URL --out PATH [--levels a,b,c]
|
|
133
|
+
npx @route-forge/core codegen --endpoint URL --out PATH [--levels a,b,c]
|
|
61
134
|
|
|
62
135
|
Options:
|
|
63
136
|
--endpoint URL Backend summary endpoint (e.g. http://localhost/_forge/routes)
|
|
64
137
|
--levels a,b,c Optional: explicit level list (skip auto-discovery)
|
|
65
138
|
--out PATH Output .d.ts file path
|
|
66
|
-
--responseTypes PATH Optional: JSON file mapping route names to response types
|
|
67
139
|
-h, --help Show this help
|
|
68
140
|
`);
|
|
69
141
|
}
|
|
70
|
-
async function fetchSummary(endpoint) {
|
|
71
|
-
const resp = await fetch(endpoint, { method: "GET" });
|
|
72
|
-
if (!resp.ok) {
|
|
73
|
-
throw new Error(`summary endpoint ${endpoint} returned ${resp.status}`);
|
|
74
|
-
}
|
|
75
|
-
return await resp.json();
|
|
76
|
-
}
|
|
77
|
-
async function fetchLevel(endpoint, level) {
|
|
78
|
-
const base = endpoint.replace(/\/$/, "");
|
|
79
|
-
const url = `${base}/${encodeURIComponent(level)}`;
|
|
80
|
-
const resp = await fetch(url, { method: "GET" });
|
|
81
|
-
if (!resp.ok) {
|
|
82
|
-
throw new Error(`level endpoint ${url} returned ${resp.status}`);
|
|
83
|
-
}
|
|
84
|
-
const data = await resp.json();
|
|
85
|
-
return { routes: data.routes ?? {} };
|
|
86
|
-
}
|
|
87
|
-
function generateRouteTypes(routesByLevel) {
|
|
88
|
-
const levelEntries = Object.entries(routesByLevel).map(([level, routes]) => {
|
|
89
|
-
const routeEntries = Object.entries(routes).map(([name, meta]) => {
|
|
90
|
-
const defaults = meta.parameter_defaults ?? {};
|
|
91
|
-
const params = (meta.parameters ?? []).map((p) => {
|
|
92
|
-
const optional = meta.uri?.includes(`{${p}?}`) || p in defaults;
|
|
93
|
-
return `${p}${optional ? "?" : ""}: string | number;`;
|
|
94
|
-
}).join(" ");
|
|
95
|
-
const method = meta.methods?.find((m) => m.toUpperCase() !== "HEAD") ?? "GET";
|
|
96
|
-
const methodUpper = method.toUpperCase();
|
|
97
|
-
const bodyField = BODY_METHODS.has(methodUpper) ? "\n body: unknown;" : "";
|
|
98
|
-
return ` ${JSON.stringify(name)}: {
|
|
99
|
-
method: ${JSON.stringify(methodUpper)};
|
|
100
|
-
params: { ${params} };${bodyField}
|
|
101
|
-
response: unknown;
|
|
102
|
-
};`;
|
|
103
|
-
});
|
|
104
|
-
return ` ${JSON.stringify(level)}: {
|
|
105
|
-
${routeEntries.join("\n")}
|
|
106
|
-
};`;
|
|
107
|
-
});
|
|
108
|
-
return `// AUTO-GENERATED by @route-forge/core codegen. Do not edit.
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* \u4E8C\u7EA7\u8DEF\u7531\u7C7B\u578B\u6620\u5C04\uFF1Alevel \u2192 routeName \u2192 routeMeta
|
|
112
|
-
* \u53EF\u901A\u8FC7 module augmentation \u589E\u5F3A\uFF1A
|
|
113
|
-
* declare module '@route-forge/core' {
|
|
114
|
-
* interface ForgeRouteMap { admin: { 'users.show': { method: 'GET'; params: { user: string | number }; response: User } } }
|
|
115
|
-
* }
|
|
116
|
-
*/
|
|
117
|
-
export interface ForgeRouteMap {
|
|
118
|
-
${levelEntries.join("\n")}
|
|
119
|
-
}
|
|
120
|
-
`;
|
|
121
|
-
}
|
|
122
142
|
async function main(argv = process.argv.slice(2)) {
|
|
123
143
|
const opts = parseArgs(argv);
|
|
124
|
-
let
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
levels = Object.keys(summaryData.levels);
|
|
130
|
-
} catch (e) {
|
|
144
|
+
let summary = null;
|
|
145
|
+
try {
|
|
146
|
+
summary = await fetchSummary(opts.endpoint);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
if (opts.levels.length === 0) {
|
|
131
149
|
console.error(`[route-forge/codegen] failed to auto-discover levels from summary endpoint: ${e.message}`);
|
|
132
150
|
console.error("hint: pass --levels explicitly to skip auto-discovery");
|
|
133
151
|
process.exit(1);
|
|
134
152
|
}
|
|
153
|
+
console.warn(
|
|
154
|
+
`[route-forge/codegen] summary endpoint unreachable: ${e.message}; falling back to endpoint-derived level URLs`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
let levels = opts.levels;
|
|
158
|
+
if (levels.length === 0 && summary) {
|
|
159
|
+
levels = Object.keys(summary.levels);
|
|
135
160
|
}
|
|
136
161
|
if (levels.length === 0) {
|
|
137
162
|
console.error("[route-forge/codegen] no levels found; pass --levels explicitly");
|
|
138
163
|
process.exit(1);
|
|
139
164
|
}
|
|
140
165
|
const levelFetches = await Promise.allSettled(
|
|
141
|
-
levels.map((lvl) => fetchLevel(opts.endpoint, lvl))
|
|
166
|
+
levels.map((lvl) => fetchLevel(opts.endpoint, lvl, summary?.levels[lvl]?.route?.uri))
|
|
142
167
|
);
|
|
143
168
|
const routesByLevel = {};
|
|
144
169
|
let failedLevels = 0;
|
|
@@ -168,6 +193,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
168
193
|
console.warn(`[route-forge/codegen] ${failedLevels} level(s) failed; output may be incomplete`);
|
|
169
194
|
}
|
|
170
195
|
}
|
|
196
|
+
|
|
197
|
+
// src/codegen/index.ts
|
|
171
198
|
var invokedFromCli = (() => {
|
|
172
199
|
try {
|
|
173
200
|
if (!process.argv[1]) return false;
|
package/dist/codegen.cjs.map
CHANGED
|
@@ -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 * 拉取摘要端点(仅用于自动发现层级名;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
|
+
{"version":3,"sources":["../src/url/utils.ts","../src/codegen/summary-client.ts","../src/codegen/emit.ts","../src/codegen/cli.ts","../src/codegen/index.ts"],"names":[],"mappings":"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","file":"codegen.cjs","sourcesContent":["/**\n * URL 基础拼接工具:统一「base + path」的斜杠规范化。\n *\n * 语义:\n * - base 去掉一个结尾 `/`(若有)\n * - path 补一个开头 `/`(若缺)\n * - 结果 = 规范化 base + 规范化 path\n */\n\n/** 去掉结尾的一个 `/`(若有)。 */\nexport function trimTrailingSlash(s: string): string {\n return s.endsWith('/') ? s.slice(0, -1) : s;\n}\n\n/** 保证开头有一个 `/`(若缺则补)。 */\nexport function withLeadingSlash(s: string): string {\n return s.startsWith('/') ? s : `/${s}`;\n}\n\n/** 规范化 base(去尾斜杠)与 path(补头斜杠)后拼接。 */\nexport function joinBaseAndPath(base: string, path: string): string {\n return `${trimTrailingSlash(base)}${withLeadingSlash(path)}`;\n}\n","/**\n * codegen 后端摘要 / 层级明细拉取(Node 环境,直接用全局 fetch)。\n *\n * 与运行时懒加载对齐:层级明细 URL 优先取摘要 `levels[].route.uri`(routeUri),\n * 缺省时回退 `endpoint/{level}` 兜底组合(等价于运行时 RouteStore.fetchLevel 的两分支)。\n */\n\nimport type { RouteMeta, SummaryResponse } from '../types.js';\nimport { joinBaseAndPath } from '../url-utils.js';\n\n/** 拉取摘要端点(自动发现层级名 + 各层级 route.uri)。 */\nexport async function fetchSummary(endpoint: string): Promise<SummaryResponse> {\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 SummaryResponse;\n}\n\n/**\n * route.uri 是后端下发的绝对路径(如 `/_forge/routes/admin`):\n * - endpoint 为绝对 URL(http(s)://host/...)→ 取其 origin 再拼 route.uri\n * - endpoint 为相对路径(无 origin,测试常用)→ route.uri 本身即完整路径,直接沿用\n */\nfunction levelUrlFromUri(endpoint: string, uri: string): string {\n try {\n return joinBaseAndPath(new URL(endpoint).origin, uri);\n } catch {\n return uri;\n }\n}\n\n/**\n * 拉取单个层级的路由元信息。\n * @param routeUri 摘要 `levels[level].route.uri`(绝对路径);缺省时回退 endpoint 拼接\n */\nexport async function fetchLevel(\n endpoint: string,\n level: string,\n routeUri?: string,\n): Promise<{ routes: Record<string, RouteMeta> }> {\n const url = routeUri\n ? levelUrlFromUri(endpoint, routeUri)\n : `${endpoint.replace(/\\/$/, '')}/${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 * codegen d.ts 内容生成(纯字符串拼装,无副作用,最易单测)。\n */\n\nimport type { RouteMeta } from '../types.js';\n\nconst BODY_METHODS = new Set(['POST', 'PUT', 'PATCH']);\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 * @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 { fetchLevel, fetchSummary } from './summary-client.js';\nimport { generateRouteTypes } from './emit.js';\nimport type { RouteMeta } from '../types.js';\n\nexport interface CodegenOptions {\n endpoint: string;\n levels: string[];\n out: string;\n}\n\n/**\n * 解析 argv(最小手写实现,不引入 commander/yargs)\n * 支持:--endpoint VALUE / --endpoint=VALUE / --levels a,b,c / --out 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' || arg?.startsWith('--responseTypes=')) {\n // 该参数从未实现(解析后被静默忽略);现显式报错并给出迁移方式,不再无声吞掉\n console.error(\n '[route-forge/codegen] --responseTypes has been removed (it was never implemented). ' +\n 'Edit the \"response\" field in the generated d.ts directly, or use module augmentation on ForgeRouteMap.',\n );\n process.exit(1);\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 };\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]\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 -h, --help Show this help\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 // 尽力拉取摘要:既用于自动发现层级名,也用于取各层级 route.uri 拼明细端点\n // (与运行时 RouteStore.fetchLevel 对齐——优先 route.uri,缺省回退 endpoint 拼接)。\n // 未显式传 --levels 时:摘要拉取失败即致命(无从发现层级)。\n // 显式传 --levels 时:摘要拉取失败仅降级为「全部层级用 endpoint 兜底拼接」,不致命。\n let summary: Awaited<ReturnType<typeof fetchSummary>> | null = null;\n try {\n summary = await fetchSummary(opts.endpoint);\n } catch (e) {\n if (opts.levels.length === 0) {\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 console.warn(\n `[route-forge/codegen] summary endpoint unreachable: ${(e as Error).message}; falling back to endpoint-derived level URLs`,\n );\n }\n\n // 未指定 --levels 时从摘要自动发现层级(unassigned 现是 levels 中的真实层级,一并纳入)\n let levels = opts.levels;\n if (levels.length === 0 && summary) {\n levels = Object.keys(summary.levels);\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 // 优先用摘要 levels[].route.uri,缺省时回退 endpoint 拼接\n const levelFetches = await Promise.allSettled(\n levels.map((lvl) => fetchLevel(opts.endpoint, lvl, summary?.levels[lvl]?.route?.uri)),\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","#!/usr/bin/env node\n/**\n * @route-forge/core codegen 入口(tsup bin 入口 `src/codegen/index.ts`)。\n *\n * 实现按功能拆分:\n * - codegen/summary-client.ts 摘要 / 层级明细拉取(route.uri 优先,endpoint 兜底)\n * - codegen/emit.ts d.ts 内容生成(纯字符串)\n * - codegen/cli.ts 参数解析 / 帮助 / 主流程编排\n * 本文件仅做 re-export(保持 `parseArgs` / `main` / `generateRouteTypes` /\n * `fetchSummary` / `fetchLevel` / `runCodegen` / `CodegenOptions` 等导入路径稳定)\n * 并承载「作为 CLI 直接执行时」的自调用守卫。\n *\n * @see .docs/SPEC.md §4.2\n */\n\nexport {\n parseArgs,\n main,\n type CodegenOptions,\n} from './cli.js';\nexport { generateRouteTypes } from './emit.js';\nexport { fetchSummary, fetchLevel } from './summary-client.js';\nexport { main as runCodegen } from './cli.js';\n\nimport { main as runCodegenMain } from './cli.js';\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 runCodegenMain().catch((err) => {\n console.error(err);\n process.exit(1);\n });\n}\n"]}
|