@route-forge/core 2.2.1 → 3.0.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/dist/index.cjs CHANGED
@@ -143,30 +143,56 @@ var ForgeError = class extends Error {
143
143
  if (opts.cause !== void 0) this.cause = opts.cause;
144
144
  }
145
145
  };
146
+ function formatCandidates(candidates) {
147
+ if (!candidates || candidates.length === 0) return "";
148
+ const MAX = 5;
149
+ const shown = candidates.slice(0, MAX).join(", ");
150
+ const more = candidates.length > MAX ? ` (+${candidates.length - MAX} more)` : "";
151
+ return ` Available: ${shown}${more}`;
152
+ }
146
153
  var UnknownRouteError = class extends ForgeError {
147
- constructor(route, level) {
148
- super(`Route "${route}" not found${level ? ` in level "${level}"` : ""}`, {
149
- code: "RF_FE_001",
150
- route,
151
- level
152
- });
154
+ constructor(route, level, candidates) {
155
+ super(
156
+ `Route "${route}" not found${level ? ` in level "${level}"` : ""}.${formatCandidates(candidates)}`,
157
+ {
158
+ code: "RF_FE_001",
159
+ route,
160
+ level,
161
+ context: candidates && candidates.length > 0 ? { candidates } : void 0
162
+ }
163
+ );
153
164
  }
154
165
  };
155
166
  var UnknownLevelError = class extends ForgeError {
156
- constructor(level) {
157
- super(`Level "${level}" not declared in options.levels`, {
158
- code: "RF_FE_002",
159
- level
160
- });
167
+ constructor(level, candidates) {
168
+ super(
169
+ `Level "${level}" not declared in options.levels.${formatCandidates(candidates)}`,
170
+ {
171
+ code: "RF_FE_002",
172
+ level,
173
+ context: candidates && candidates.length > 0 ? { candidates } : void 0
174
+ }
175
+ );
161
176
  }
162
177
  };
163
178
  var MissingRouteParamError = class extends ForgeError {
164
- constructor(route, missingParams) {
165
- super(`Missing path parameter(s) ${missingParams.join(", ")} for route "${route}"`, {
166
- code: "RF_FE_003",
167
- route,
168
- context: { missingParams }
169
- });
179
+ constructor(route, missingParams, uri) {
180
+ super(
181
+ `Missing path parameter(s) ${missingParams.join(", ")} for route "${route}"${uri ? ` (${uri})` : ""}`,
182
+ {
183
+ code: "RF_FE_003",
184
+ route,
185
+ context: { missingParams, ...uri !== void 0 ? { uri } : {} }
186
+ }
187
+ );
188
+ }
189
+ };
190
+ var InvalidPathParamError = class extends ForgeError {
191
+ constructor(route, param, value) {
192
+ super(
193
+ `Path parameter "${param}" must be a primitive value (string, number, boolean), got ${typeof value}`,
194
+ { code: "RF_FE_003", route, context: { param, value } }
195
+ );
170
196
  }
171
197
  };
172
198
  var AdapterNotFoundError = class extends ForgeError {
@@ -199,6 +225,7 @@ var HTTPError = class extends ForgeError {
199
225
  context: { status: opts.status, url: opts.url, method: opts.method },
200
226
  cause: opts.cause
201
227
  });
228
+ if (opts.response !== void 0) this.response = opts.response;
202
229
  }
203
230
  };
204
231
  var RequestAbortedError = class extends ForgeError {
@@ -211,6 +238,14 @@ var RequestAbortedError = class extends ForgeError {
211
238
  });
212
239
  }
213
240
  };
241
+ var DiscoveryNotReadyError = class extends ForgeError {
242
+ constructor() {
243
+ super(
244
+ "Route data not available. Auto-discovery has not completed. Use forge.ready() or forge.use(level) first, or await ready() before calling route()/hasRoute().",
245
+ { code: "RF_FE_010" }
246
+ );
247
+ }
248
+ };
214
249
 
215
250
  // src/interceptors.ts
216
251
  var InterceptorManagerImpl = class {
@@ -277,6 +312,43 @@ async function runResponseInterceptors(manager, source) {
277
312
  function createInterceptorManager() {
278
313
  return new InterceptorManagerImpl();
279
314
  }
315
+ function normalizeInterceptorDeclaration(value) {
316
+ if (value === null || value === void 0) return {};
317
+ let rawResolve;
318
+ let rawReject;
319
+ if (typeof value === "function") {
320
+ rawResolve = value;
321
+ } else if (Array.isArray(value)) {
322
+ rawResolve = value[0];
323
+ rawReject = value[1];
324
+ } else if (typeof value === "object") {
325
+ const obj = value;
326
+ rawResolve = obj.resolve;
327
+ rawReject = obj.reject;
328
+ } else {
329
+ throw new TypeError(
330
+ `Interceptor declaration must be a function, a [resolve, reject] tuple, or a { resolve, reject } object; received ${typeof value}.`
331
+ );
332
+ }
333
+ const handler = {};
334
+ if (rawResolve !== void 0 && rawResolve !== null) {
335
+ if (typeof rawResolve !== "function") {
336
+ throw new TypeError(
337
+ `Interceptor "resolve" (onFulfilled) must be a function; received ${typeof rawResolve}.`
338
+ );
339
+ }
340
+ handler.onFulfilled = rawResolve;
341
+ }
342
+ if (rawReject !== void 0 && rawReject !== null) {
343
+ if (typeof rawReject !== "function") {
344
+ throw new TypeError(
345
+ `Interceptor "reject" (onRejected) must be a function; received ${typeof rawReject}.`
346
+ );
347
+ }
348
+ handler.onRejected = rawReject;
349
+ }
350
+ return handler;
351
+ }
280
352
 
281
353
  // src/adapters/fetch-core.ts
282
354
  function isAbortError(err, signal) {
@@ -377,7 +449,10 @@ async function rawFetch(config) {
377
449
  level: config.level,
378
450
  status: res.status,
379
451
  url,
380
- method: config.method
452
+ method: config.method,
453
+ // 完整 ResponseData 随错误逐段传递(响应拦截器 onRejected 链 → 最终 catch),
454
+ // 供调用方检查响应体,如 Laravel 422 校验错误 err.response.data.errors
455
+ response: responseData
381
456
  }
382
457
  );
383
458
  }
@@ -589,7 +664,7 @@ function buildRequestUrl(meta, params, ctx) {
589
664
  }
590
665
  }
591
666
  if (missingRequired.length > 0) {
592
- throw new MissingRouteParamError(meta.name, missingRequired);
667
+ throw new MissingRouteParamError(meta.name, missingRequired, meta.uri);
593
668
  }
594
669
  let uri = meta.uri.replace(/\{([^{}]+)\}/g, (match, raw) => {
595
670
  const optional = raw.endsWith("?");
@@ -597,10 +672,7 @@ function buildRequestUrl(meta, params, ctx) {
597
672
  if (values[name] !== void 0) {
598
673
  const val = values[name];
599
674
  if (typeof val === "object") {
600
- throw new ForgeError(
601
- `Path parameter "${name}" must be a primitive value (string, number, boolean), got ${typeof val}`,
602
- { code: "RF_FE_003", route: meta.name, context: { param: name, value: val } }
603
- );
675
+ throw new InvalidPathParamError(meta.name, name, val);
604
676
  }
605
677
  return encodeURIComponent(String(val));
606
678
  }
@@ -673,24 +745,31 @@ function resolveApiParams(input) {
673
745
  }
674
746
 
675
747
  // src/auto-discovery.ts
748
+ var DEFAULT_ENDPOINT = "/_forge/routes";
676
749
  async function fetchSummary(inputs, baseURL, fetchMeta) {
677
- const { explicitLevels, explicitEndpoint } = inputs;
678
- if (!explicitEndpoint) {
679
- throw new UnknownLevelError("(auto-discovery)");
680
- }
750
+ const { explicitLevels, explicitEndpoint, warnings } = inputs;
751
+ const endpoint = explicitEndpoint ?? DEFAULT_ENDPOINT;
752
+ const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
753
+ const ep = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
754
+ const url = `${base}${ep}`;
681
755
  try {
682
- const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
683
- const ep = explicitEndpoint.startsWith("/") ? explicitEndpoint : `/${explicitEndpoint}`;
684
- const data = await fetchMeta("__forge__.summary", `${base}${ep}`);
756
+ const data = await fetchMeta("__forge__.summary", url);
685
757
  return data;
686
758
  } catch (e) {
687
759
  if (explicitLevels && explicitLevels.length > 0) {
688
- console.warn(
689
- `[route-forge] summary endpoint unreachable: ${e.message}; using explicit levels`
690
- );
760
+ if (warnings) {
761
+ console.warn(
762
+ `[route-forge] summary endpoint unreachable: ${e.message}; using explicit levels`
763
+ );
764
+ }
691
765
  return null;
692
766
  }
693
- throw new UnknownLevelError("(auto-discovery)");
767
+ throw new NetworkError(
768
+ `Failed to fetch route summary from "${url}": ${e?.message ?? String(e)}; check the backend manifest endpoint or options.endpoint`,
769
+ void 0,
770
+ void 0,
771
+ e
772
+ );
694
773
  }
695
774
  }
696
775
  function normalizeCacheTtl(raw) {
@@ -698,15 +777,15 @@ function normalizeCacheTtl(raw) {
698
777
  return raw;
699
778
  }
700
779
  function applySummaryToState(summary, state, inputs) {
701
- const { explicitLevels, explicitEager, explicitEndpoint } = inputs;
780
+ const { explicitLevels, explicitEager, explicitEndpoint, warnings } = inputs;
702
781
  const schemeVersion = summary.schemeVersion ?? 1;
703
- if (schemeVersion > 1) {
782
+ if (schemeVersion > 1 && warnings) {
704
783
  console.warn(
705
784
  `[route-forge] backend schemeVersion=${schemeVersion} > client supported 1; some features may be unavailable`
706
785
  );
707
786
  }
708
787
  if (summary.config.endpoint_prefix && summary.config.endpoint_prefix !== explicitEndpoint) {
709
- if (explicitEndpoint) {
788
+ if (explicitEndpoint && warnings) {
710
789
  console.warn(
711
790
  `[route-forge] backend endpoint_prefix "${summary.config.endpoint_prefix}" overrides frontend endpoint "${explicitEndpoint}"`
712
791
  );
@@ -729,7 +808,7 @@ function applySummaryToState(summary, state, inputs) {
729
808
  if (explicitLevels && explicitLevels.length > 0) {
730
809
  const intersection = explicitLevels.filter((l) => backendLevels.includes(l));
731
810
  const removed = explicitLevels.filter((l) => !backendLevels.includes(l));
732
- if (removed.length > 0) {
811
+ if (removed.length > 0 && warnings) {
733
812
  console.warn(
734
813
  `[route-forge] levels not in backend summary and dropped: ${removed.join(", ")}`
735
814
  );
@@ -781,7 +860,7 @@ var RouteStore = class {
781
860
  }
782
861
  assertLevelDeclared(level) {
783
862
  if (!this.state.levels.includes(level)) {
784
- throw new UnknownLevelError(level);
863
+ throw new UnknownLevelError(level, this.state.levels);
785
864
  }
786
865
  }
787
866
  async fetchLevel(level) {
@@ -855,6 +934,7 @@ var RouteStore = class {
855
934
  }
856
935
  getRoutes(level) {
857
936
  if (level !== void 0) {
937
+ this.assertLevelDeclared(level);
858
938
  const entry = this.cache.get(level);
859
939
  const routes = entry?.routes ?? {};
860
940
  const result2 = {};
@@ -886,6 +966,7 @@ function createHttpRunner(deps) {
886
966
  responseInterceptors,
887
967
  load,
888
968
  findRouteMeta,
969
+ getRouteNames,
889
970
  baseURL,
890
971
  state,
891
972
  timeout,
@@ -937,7 +1018,10 @@ function createHttpRunner(deps) {
937
1018
  level: resp.level,
938
1019
  status: resp.status,
939
1020
  url: resp.url,
940
- method: resp.method
1021
+ method: resp.method,
1022
+ // 完整 ResponseData 随错误逐段传递(响应拦截器 onRejected 链 → 最终 catch),
1023
+ // 供调用方检查响应体,如 Laravel 422 校验错误 err.response.data.errors
1024
+ response: resp
941
1025
  }
942
1026
  );
943
1027
  }
@@ -975,7 +1059,7 @@ function createHttpRunner(deps) {
975
1059
  await load(level);
976
1060
  const meta = findRouteMeta(level, name);
977
1061
  if (!meta) {
978
- throw new UnknownRouteError(name, level);
1062
+ throw new UnknownRouteError(name, level, getRouteNames(level));
979
1063
  }
980
1064
  return doApiCall(meta, params, ctrl.signal);
981
1065
  })();
@@ -992,22 +1076,31 @@ function createHttpRunner(deps) {
992
1076
  }
993
1077
 
994
1078
  // src/resolveRouteName.ts
1079
+ function stripTrailingSeparator(prefix, separator) {
1080
+ let normalized = prefix;
1081
+ while (normalized.endsWith(separator)) {
1082
+ normalized = normalized.slice(0, -separator.length);
1083
+ }
1084
+ return normalized;
1085
+ }
995
1086
  async function resolveRouteName(forge, level, prefix, suffix, separator = ".") {
996
- if (!suffix) return prefix;
997
- const joined = `${prefix}${separator}${suffix}`;
998
- if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
1087
+ const normalized = stripTrailingSeparator(prefix, separator);
1088
+ if (!suffix) return normalized;
1089
+ const joined = `${normalized}${separator}${suffix}`;
1090
+ if (!suffix.startsWith(`${normalized}${separator}`)) return joined;
999
1091
  await forge.load(level);
1000
1092
  if (forge.hasRoute(level, joined)) return joined;
1001
1093
  if (forge.hasRoute(level, suffix)) return suffix;
1002
- throw new UnknownRouteError(joined, level);
1094
+ throw new UnknownRouteError(joined, level, forge.getRouteNames?.(level));
1003
1095
  }
1004
1096
  function resolveRouteNameSync(forge, level, prefix, suffix, separator = ".") {
1005
- if (!suffix) return prefix;
1006
- const joined = `${prefix}${separator}${suffix}`;
1007
- if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
1097
+ const normalized = stripTrailingSeparator(prefix, separator);
1098
+ if (!suffix) return normalized;
1099
+ const joined = `${normalized}${separator}${suffix}`;
1100
+ if (!suffix.startsWith(`${normalized}${separator}`)) return joined;
1008
1101
  if (forge.hasRoute(level, joined)) return joined;
1009
1102
  if (forge.hasRoute(level, suffix)) return suffix;
1010
- throw new UnknownRouteError(joined, level);
1103
+ throw new UnknownRouteError(joined, level, forge.getRouteNames?.(level));
1011
1104
  }
1012
1105
 
1013
1106
  // src/defineImmutableProps.ts
@@ -1027,7 +1120,7 @@ function defineImmutableProps(target, props) {
1027
1120
  // src/bound-forge.ts
1028
1121
  function createBoundForgeFactory(deps) {
1029
1122
  const { load, api, route, hasRoute, getRoutes, invalidate, isLoaded, loadingTracker } = deps;
1030
- const resolver = { load, hasRoute };
1123
+ const resolver = { load, hasRoute, getRouteNames: (lvl) => Object.keys(getRoutes(lvl)) };
1031
1124
  function createBoundForge(level, prefix) {
1032
1125
  const levelLoadedPromise = load(level);
1033
1126
  levelLoadedPromise.catch(() => {
@@ -1093,11 +1186,6 @@ var DEFAULT_TIMEOUT = 3e4;
1093
1186
  var DEFAULT_CACHE_TTL = 3600;
1094
1187
  function createRouteForge(options = {}) {
1095
1188
  const bootstrapSummary = readEmbeddedSummary() ?? options.summary ?? null;
1096
- if (!bootstrapSummary && !options.endpoint) {
1097
- throw new TypeError(
1098
- "createRouteForge: \u9700\u8981 options.endpoint\uFF0C\u6216 options.summary\uFF0C\u6216\u9875\u9762\u5185\u5D4C window.__ROUTE_FORGE__"
1099
- );
1100
- }
1101
1189
  const {
1102
1190
  adapter = "auto",
1103
1191
  timeout = DEFAULT_TIMEOUT,
@@ -1105,6 +1193,7 @@ function createRouteForge(options = {}) {
1105
1193
  interceptors: declarativeInterceptors,
1106
1194
  cache: cacheOpts = {}
1107
1195
  } = options;
1196
+ const warnings = options.warnings ?? true;
1108
1197
  const loadingTracker = new LoadingTracker();
1109
1198
  const explicitLevels = options.levels;
1110
1199
  const explicitEager = options.eager;
@@ -1112,12 +1201,12 @@ function createRouteForge(options = {}) {
1112
1201
  const discoveryState = {
1113
1202
  levels: explicitLevels ?? [],
1114
1203
  eager: explicitEager ?? [],
1115
- endpoint: explicitEndpoint ?? bootstrapSummary?.config?.endpoint_prefix ?? "",
1204
+ endpoint: explicitEndpoint ?? bootstrapSummary?.config?.endpoint_prefix ?? DEFAULT_ENDPOINT,
1116
1205
  urlPrefix: "",
1117
1206
  cacheTtl: void 0,
1118
1207
  levelRoutes: {}
1119
1208
  };
1120
- const discoveryInputs = { explicitLevels, explicitEager, explicitEndpoint };
1209
+ const discoveryInputs = { explicitLevels, explicitEager, explicitEndpoint, warnings };
1121
1210
  let autoDiscoveryCompleted = false;
1122
1211
  let resolveReady;
1123
1212
  let rejectReady;
@@ -1127,30 +1216,30 @@ function createRouteForge(options = {}) {
1127
1216
  });
1128
1217
  readyPromise.catch(() => {
1129
1218
  });
1219
+ let readySettledOk = false;
1220
+ readyPromise.then(
1221
+ () => {
1222
+ readySettledOk = true;
1223
+ },
1224
+ () => {
1225
+ }
1226
+ );
1130
1227
  const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
1131
1228
  const cacheStorage = cacheOpts.storage ?? "memory";
1132
1229
  const cache = new RouteCache({ storage: cacheStorage, ttl: cacheTtl });
1133
1230
  const requestInterceptors = new InterceptorManagerImpl();
1134
1231
  const responseInterceptors = new InterceptorManagerImpl();
1135
- if (declarativeInterceptors?.request) {
1136
- for (const entry of declarativeInterceptors.request) {
1137
- if (typeof entry === "function") {
1138
- requestInterceptors.use(entry);
1139
- } else {
1140
- const [onFulfilled, onRejected] = entry;
1141
- requestInterceptors.use(onFulfilled, onRejected);
1142
- }
1143
- }
1232
+ const reqDecl = normalizeInterceptorDeclaration(
1233
+ declarativeInterceptors?.request
1234
+ );
1235
+ if (reqDecl.onFulfilled || reqDecl.onRejected) {
1236
+ requestInterceptors.use(reqDecl.onFulfilled, reqDecl.onRejected);
1144
1237
  }
1145
- if (declarativeInterceptors?.response) {
1146
- for (const entry of declarativeInterceptors.response) {
1147
- if (typeof entry === "function") {
1148
- responseInterceptors.use(entry);
1149
- } else {
1150
- const [onFulfilled, onRejected] = entry;
1151
- responseInterceptors.use(onFulfilled, onRejected);
1152
- }
1153
- }
1238
+ const resDecl = normalizeInterceptorDeclaration(
1239
+ declarativeInterceptors?.response
1240
+ );
1241
+ if (resDecl.onFulfilled || resDecl.onRejected) {
1242
+ responseInterceptors.use(resDecl.onFulfilled, resDecl.onRejected);
1154
1243
  }
1155
1244
  const adapterPromise = resolveAdapter({
1156
1245
  adapter,
@@ -1162,6 +1251,11 @@ function createRouteForge(options = {}) {
1162
1251
  if (!adapterResolved) {
1163
1252
  adapterObj = await adapterPromise.catch((e) => {
1164
1253
  if (e instanceof AdapterNotFoundError) throw e;
1254
+ if (warnings) {
1255
+ console.warn(
1256
+ `[route-forge] adapter initialization failed (${e?.message ?? String(e)}); falling back to builtin`
1257
+ );
1258
+ }
1165
1259
  return resolveAdapter({
1166
1260
  adapter: "builtin",
1167
1261
  forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
@@ -1173,10 +1267,7 @@ function createRouteForge(options = {}) {
1173
1267
  }
1174
1268
  function assertDiscoveryReady() {
1175
1269
  if (!autoDiscoveryCompleted && !explicitLevels?.length) {
1176
- throw new ForgeError(
1177
- "Route data not available. Auto-discovery has not completed. Use forge.ready() or forge.use(level) first.",
1178
- { code: "RF_FE_010" }
1179
- );
1270
+ throw new DiscoveryNotReadyError();
1180
1271
  }
1181
1272
  }
1182
1273
  async function fetchMeta(routeTag, url, level = "") {
@@ -1201,8 +1292,8 @@ function createRouteForge(options = {}) {
1201
1292
  const resp = await doRawRequest(config);
1202
1293
  if (!resp || resp.status < 200 || resp.status >= 300) {
1203
1294
  throw new HTTPError(
1204
- `Failed to fetch "${routeTag}": HTTP ${resp?.status}`,
1205
- { level, status: resp?.status, url, method: "GET" }
1295
+ `Failed to fetch "${routeTag}" (${url}): HTTP ${resp?.status}`,
1296
+ { level, status: resp?.status, url, method: "GET", response: resp ?? void 0 }
1206
1297
  );
1207
1298
  }
1208
1299
  return resp.data;
@@ -1239,11 +1330,14 @@ function createRouteForge(options = {}) {
1239
1330
  function getRoutes(level) {
1240
1331
  return level === void 0 ? store.getRoutes() : store.getRoutes(level);
1241
1332
  }
1333
+ function getLevels() {
1334
+ return [...discoveryState.levels];
1335
+ }
1242
1336
  function route(level, name, params) {
1243
1337
  assertDiscoveryReady();
1244
1338
  const meta = findRouteMeta(level, name);
1245
1339
  if (!meta) {
1246
- throw new UnknownRouteError(name, level);
1340
+ throw new UnknownRouteError(name, level, Object.keys(store.getRoutes(level)));
1247
1341
  }
1248
1342
  return buildRequestUrl(meta, params ?? {}, { baseURL, urlPrefix: discoveryState.urlPrefix });
1249
1343
  }
@@ -1253,6 +1347,7 @@ function createRouteForge(options = {}) {
1253
1347
  responseInterceptors,
1254
1348
  load,
1255
1349
  findRouteMeta,
1350
+ getRouteNames: (lvl) => Object.keys(store.getRoutes(lvl)),
1256
1351
  baseURL,
1257
1352
  state: discoveryState,
1258
1353
  timeout,
@@ -1308,7 +1403,10 @@ function createRouteForge(options = {}) {
1308
1403
  isLoaded,
1309
1404
  hasRoute,
1310
1405
  getRoutes,
1406
+ getLevels,
1407
+ warnings,
1311
1408
  isLoading: () => loadingTracker.isLoading(),
1409
+ isReady: () => readySettledOk,
1312
1410
  onLoadingChange: (cb) => loadingTracker.subscribe(cb),
1313
1411
  interceptors: {
1314
1412
  request: requestInterceptors,
@@ -1324,10 +1422,12 @@ function createRouteForge(options = {}) {
1324
1422
  }
1325
1423
 
1326
1424
  exports.AdapterNotFoundError = AdapterNotFoundError;
1425
+ exports.DiscoveryNotReadyError = DiscoveryNotReadyError;
1327
1426
  exports.ForgeError = ForgeError;
1328
1427
  exports.HTTPError = HTTPError;
1329
1428
  exports.InterceptorManagerImpl = InterceptorManagerImpl;
1330
1429
  exports.InvalidInterceptorReturnError = InvalidInterceptorReturnError;
1430
+ exports.InvalidPathParamError = InvalidPathParamError;
1331
1431
  exports.LoadingTracker = LoadingTracker;
1332
1432
  exports.MissingRouteParamError = MissingRouteParamError;
1333
1433
  exports.NetworkError = NetworkError;