@route-forge/core 1.3.0 → 1.4.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 CHANGED
@@ -281,6 +281,46 @@ const prefixed = bound.useRoutePrefix('posts') // 追加前缀
281
281
  `ForgeError (RF_FE_010)`, 防止在路由数据未就绪时返回错误结果。`api()` 不受影响(内部自动 await
282
282
  discovery)。
283
283
 
284
+ ## 工具导出
285
+
286
+ 除 `createRouteForge` 外,core 包还导出以下工具件,供高级场景按需使用:
287
+
288
+ | 导出 | 说明 |
289
+ |-----------------------------|----------------------------------------------------------------------------------------|
290
+ | `createInterceptorManager` | 创建拦截器管理器(`use`/`eject`/`clear`),供自定义 Fetcher 复用统一拦截器实现 |
291
+ | `RouteCache` | 按层级隔离的路由缓存类(memory / sessionStorage / localStorage,TTL 过期),可独立使用 |
292
+ | `LoadingTracker` | 加载状态跟踪器(引用计数 + 订阅),框架适配层可基于它实现全局加载指示 |
293
+ | `resolveRouteName` | 前缀歧义异步消解(`prefix.suffix` 优先,回退后缀本身),`api()` 调用路径使用 |
294
+ | `resolveRouteNameSync` | 前缀歧义同步消解(基于已加载缓存),`route()` / `url()` 调用路径使用 |
295
+
296
+ ## 错误参考
297
+
298
+ 所有错误均为 `ForgeError` 子类,携带稳定的 `code` 字段,可按 `code` 分支处理:
299
+
300
+ | 错误类 | code | 触发场景 |
301
+ |-------------------------------|-------------|------------------------------------------------------|
302
+ | `UnknownRouteError` | `RF_FE_001` | 路由名不存在于已加载层级中 |
303
+ | `UnknownLevelError` | `RF_FE_002` | 层级未在 levels 声明(前端校验始终开启) |
304
+ | `MissingRouteParamError` | `RF_FE_003` | 必填路径参数缺失(无后端默认值) |
305
+ | `AdapterNotFoundError` | `RF_FE_005` | `adapter: 'axios'` 但宿主未安装/无有效 axios |
306
+ | `InvalidInterceptorReturnError` | `RF_FE_006` | 请求拦截器未返回 RequestConfig 对象 |
307
+ | `NetworkError` | `RF_FE_007` | 网络层失败(DNS、连接被拒等),`cause` 保留原始错误 |
308
+ | `HTTPError` | `RF_FE_008` | HTTP 非 2xx,`context.status` 为状态码 |
309
+ | `RequestAbortedError` | `RF_FE_009` | 请求被 `abort()` / AbortSignal 取消 |
310
+ | `ForgeError`(守卫) | `RF_FE_010` | auto-discovery 未完成时调用 `route()`/`hasRoute()` |
311
+
312
+ 错误对象结构:
313
+
314
+ ```ts
315
+ {
316
+ code: 'RF_FE_008', // 稳定错误码
317
+ route?: string, // 关联路由名
318
+ level?: string, // 关联层级
319
+ context?: Record<string, unknown>, // 附加上下文(如 HTTP 状态码)
320
+ cause?: unknown, // 原始底层错误
321
+ }
322
+ ```
323
+
284
324
  ## 文档
285
325
 
286
326
  - 仓库主页: https://github.com/route-forge/route-forge
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { R as RouteMeta } from './types-D4PMbR4-.cjs';
2
+ import { R as RouteMeta } from './types-BFlrOTrN.cjs';
3
3
 
4
4
  /**
5
5
  * @route-forge/core codegen CLI
package/dist/codegen.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { R as RouteMeta } from './types-D4PMbR4-.js';
2
+ import { R as RouteMeta } from './types-BFlrOTrN.js';
3
3
 
4
4
  /**
5
5
  * @route-forge/core codegen CLI
package/dist/index.cjs CHANGED
@@ -280,30 +280,29 @@ function combineSignals(...signals) {
280
280
  function createBuiltinHttp(forgeInterceptors) {
281
281
  const requestMgr = forgeInterceptors?.request ?? createInterceptorManager();
282
282
  const responseMgr = forgeInterceptors?.response ?? createInterceptorManager();
283
- async function request(config) {
284
- const finalConfig = await runRequestInterceptors(requestMgr, config);
283
+ async function requestRaw(config) {
285
284
  const signal = combineSignals(
286
- finalConfig.signal,
287
- finalConfig.timeout && finalConfig.timeout > 0 ? AbortSignal.timeout(finalConfig.timeout) : void 0
285
+ config.signal,
286
+ config.timeout && config.timeout > 0 ? AbortSignal.timeout(config.timeout) : void 0
288
287
  );
289
- let url = finalConfig.url;
290
- if (finalConfig.paramsSerializer && finalConfig.params) {
291
- const qs = finalConfig.paramsSerializer(finalConfig.params);
288
+ let url = config.url;
289
+ if (config.paramsSerializer && config.params) {
290
+ const qs = config.paramsSerializer(config.params);
292
291
  if (qs) {
293
292
  url = url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
294
293
  }
295
294
  }
296
- const headers = new Headers(finalConfig.headers);
295
+ const headers = new Headers(config.headers);
297
296
  const fetchInit = {
298
- method: finalConfig.method,
297
+ method: config.method,
299
298
  headers
300
299
  };
301
300
  if (signal) fetchInit.signal = signal;
302
- if (finalConfig.body !== void 0 && !["GET", "HEAD"].includes(finalConfig.method.toUpperCase())) {
303
- if (typeof finalConfig.body === "string" || isPassthroughBody(finalConfig.body)) {
304
- fetchInit.body = finalConfig.body;
301
+ if (config.body !== void 0 && !["GET", "HEAD"].includes(config.method.toUpperCase())) {
302
+ if (typeof config.body === "string" || isPassthroughBody(config.body)) {
303
+ fetchInit.body = config.body;
305
304
  } else {
306
- fetchInit.body = JSON.stringify(finalConfig.body);
305
+ fetchInit.body = JSON.stringify(config.body);
307
306
  if (!headers.has("Content-Type")) {
308
307
  headers.set("Content-Type", "application/json");
309
308
  }
@@ -316,8 +315,8 @@ function createBuiltinHttp(forgeInterceptors) {
316
315
  if (isAbortError(e)) throw e;
317
316
  throw new NetworkError(
318
317
  e instanceof Error ? e.message : String(e),
319
- finalConfig.route,
320
- finalConfig.level,
318
+ config.route,
319
+ config.level,
321
320
  e
322
321
  );
323
322
  }
@@ -331,27 +330,32 @@ function createBuiltinHttp(forgeInterceptors) {
331
330
  }
332
331
  }
333
332
  const responseData = {
334
- route: finalConfig.route,
335
- level: finalConfig.level,
336
- method: finalConfig.method,
333
+ route: config.route,
334
+ level: config.level,
335
+ method: config.method,
337
336
  url,
338
337
  status: res.status,
339
338
  headers: res.headers,
340
339
  data,
341
- config: finalConfig
340
+ config
342
341
  };
343
- const source = res.status >= 200 && res.status < 300 ? Promise.resolve(responseData) : Promise.reject(
344
- new HTTPError(
345
- `HTTP ${res.status} for route "${finalConfig.route}" (${finalConfig.method} ${url})`,
346
- {
347
- route: finalConfig.route,
348
- level: finalConfig.level,
349
- status: res.status,
350
- url,
351
- method: finalConfig.method
352
- }
353
- )
342
+ if (res.status >= 200 && res.status < 300) {
343
+ return responseData;
344
+ }
345
+ throw new HTTPError(
346
+ `HTTP ${res.status} for route "${config.route}" (${config.method} ${url})`,
347
+ {
348
+ route: config.route,
349
+ level: config.level,
350
+ status: res.status,
351
+ url,
352
+ method: config.method
353
+ }
354
354
  );
355
+ }
356
+ async function request(config) {
357
+ const finalConfig = await runRequestInterceptors(requestMgr, config);
358
+ const source = Promise.resolve(finalConfig).then(requestRaw);
355
359
  return runResponseInterceptors(
356
360
  responseMgr,
357
361
  source
@@ -364,6 +368,7 @@ function createBuiltinHttp(forgeInterceptors) {
364
368
  const del = (url, config) => request({ ...config, url, method: "DELETE" });
365
369
  return {
366
370
  request,
371
+ requestRaw,
367
372
  interceptors: {
368
373
  request: requestMgr,
369
374
  response: responseMgr
@@ -575,7 +580,6 @@ function createRouteForge(options) {
575
580
  const loadingTracker = new LoadingTracker();
576
581
  const explicitLevels = options.levels;
577
582
  const explicitEager = options.eager;
578
- const explicitStrict = options.strict ?? false;
579
583
  const explicitEndpoint = options.endpoint;
580
584
  let effectiveLevels = explicitLevels ?? [];
581
585
  let effectiveEager = explicitEager ?? [];
@@ -623,11 +627,6 @@ function createRouteForge(options) {
623
627
  if (summary.config.url_prefix) {
624
628
  effectiveUrlPrefix = summary.config.url_prefix.endsWith("/") ? summary.config.url_prefix.slice(0, -1) : summary.config.url_prefix;
625
629
  }
626
- if (summary.config.strict_mode && !explicitStrict) {
627
- console.warn(
628
- "[route-forge] backend strict_mode=true overrides frontend strict=false; forcing strict=true"
629
- );
630
- }
631
630
  const backendLevels = Object.keys(summary.levels);
632
631
  backendHasUnassignedLevel = backendLevels.includes(UNASSIGNED_LEVEL);
633
632
  if (Array.isArray(summary.unassigned) && summary.unassigned.length > 0 && !backendHasUnassignedLevel) {
@@ -701,7 +700,10 @@ function createRouteForge(options) {
701
700
  if (!adapterResolved) {
702
701
  adapterObj = await adapterPromise.catch((e) => {
703
702
  if (e instanceof AdapterNotFoundError) throw e;
704
- return resolveAdapter({ adapter: "builtin" });
703
+ return resolveAdapter({
704
+ adapter: "builtin",
705
+ forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
706
+ });
705
707
  });
706
708
  adapterResolved = true;
707
709
  }
@@ -745,7 +747,8 @@ function createRouteForge(options) {
745
747
  level
746
748
  }
747
749
  };
748
- const resp = await adp.request(config);
750
+ const doRawRequest = adp.requestRaw ?? adp.request;
751
+ const resp = await doRawRequest(config);
749
752
  if (!resp || resp.status < 200 || resp.status >= 300) {
750
753
  throw new HTTPError(
751
754
  `Failed to load level "${level}": HTTP ${resp?.status}`,
@@ -848,23 +851,40 @@ function createRouteForge(options) {
848
851
  }
849
852
  return void 0;
850
853
  }
851
- async function api(level, name, params = {}) {
852
- await autoDiscoveryPromise;
853
- await load(level);
854
- const meta = findRouteMeta(level, name);
855
- if (!meta) {
856
- throw new UnknownRouteError(name, level);
857
- }
858
- return doApiCall(meta, params);
854
+ function api(level, name, params = {}) {
855
+ let ctrl;
856
+ let abortedBeforeInit = false;
857
+ let abortReason;
858
+ const work = (async () => {
859
+ ctrl = new AbortController();
860
+ if (abortedBeforeInit) {
861
+ ctrl.abort(abortReason);
862
+ }
863
+ await autoDiscoveryPromise;
864
+ await load(level);
865
+ const meta = findRouteMeta(level, name);
866
+ if (!meta) {
867
+ throw new UnknownRouteError(name, level);
868
+ }
869
+ return doApiCall(meta, params, ctrl.signal);
870
+ })();
871
+ const request = work;
872
+ request.abort = () => {
873
+ if (ctrl) {
874
+ ctrl.abort();
875
+ } else {
876
+ abortedBeforeInit = true;
877
+ }
878
+ };
879
+ return request;
859
880
  }
860
- async function doApiCall(meta, params) {
881
+ async function doApiCall(meta, params, signal) {
861
882
  const {
862
883
  pathParams,
863
884
  query,
864
885
  body,
865
886
  headers,
866
- timeout: perCallTimeout,
867
- signal
887
+ timeout: perCallTimeout
868
888
  } = resolveApiParams(params);
869
889
  if (signal?.aborted) {
870
890
  throw new RequestAbortedError(meta.name, meta.level, signal.reason);
@@ -908,7 +928,7 @@ function createRouteForge(options) {
908
928
  },
909
929
  (err) => {
910
930
  if (err instanceof ForgeError) throw err;
911
- if (isAbortError2(err, finalConfig.signal)) {
931
+ if (isAbortError2(err, signal)) {
912
932
  throw new RequestAbortedError(meta.name, meta.level, err);
913
933
  }
914
934
  throw new NetworkError(
@@ -996,7 +1016,8 @@ function createRouteForge(options) {
996
1016
  }
997
1017
  const forgeResolver = { load, hasRoute };
998
1018
  function createBoundForge(level, prefix) {
999
- const levelLoadedPromise = load(level).catch(() => {
1019
+ const levelLoadedPromise = load(level);
1020
+ levelLoadedPromise.catch(() => {
1000
1021
  });
1001
1022
  const apiFn = prefix ? (name, params) => resolveRouteName(forgeResolver, level, prefix, name).then(
1002
1023
  (resolved) => api(level, resolved, params)
@@ -1096,7 +1117,6 @@ function resolveApiParams(input) {
1096
1117
  body: rawBody,
1097
1118
  headers: rawHeaders,
1098
1119
  timeout: perCallTimeout,
1099
- signal,
1100
1120
  ...flatRest
1101
1121
  } = input;
1102
1122
  const pathParams = explicitParams ? { ...explicitParams } : {};
@@ -1129,7 +1149,7 @@ function resolveApiParams(input) {
1129
1149
  pathParams.headers = rawHeaders;
1130
1150
  }
1131
1151
  }
1132
- return { pathParams, query, body, headers, timeout: perCallTimeout, signal };
1152
+ return { pathParams, query, body, headers, timeout: perCallTimeout };
1133
1153
  }
1134
1154
  function isAbortError2(err, signal) {
1135
1155
  if (signal?.aborted) return true;