@route-forge/core 1.2.2 → 1.3.1

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
@@ -210,26 +210,69 @@ const forge = createRouteForge({
210
210
  })
211
211
  ```
212
212
 
213
- ### `forge.ready` Promise
213
+ ### `forge.ready()` 方法
214
214
 
215
- auto-discovery + eager load 完成后 resolve,适合 async/await 风格:
215
+ auto-discovery + eager load 完成后 resolve,返回 `Promise<RouteForge>`(resolve 值为 forge 自身),适合 async/await 风格:
216
216
 
217
217
  ```ts
218
218
  const forge = createRouteForge({ endpoint: '/_forge/routes' })
219
- await forge.ready
219
+
220
+ // 无参模式:直接 await
221
+ await forge.ready()
220
222
  // 路由数据已就绪,可安全调用 route() / hasRoute()
223
+
224
+ // 回调模式:onFulfilled / onRejected
225
+ forge.ready(
226
+ (f) => { console.log('ready!', f) },
227
+ (err) => { console.error(err) }
228
+ )
229
+
230
+ // 链式调用:ready 返回 forge 自身
231
+ const bound = await forge.ready().then(f => f.use('admin'))
221
232
  ```
222
233
 
223
- ### `onLevelLoaded` 订阅
234
+ ### `forge.use(level?, prefix?)` 层级绑定
224
235
 
225
- 订阅指定 level 加载完成事件:
236
+ `forge.use()` 是 core 层唯一的 level 绑定入口,返回 `BoundForge` 对象:
226
237
 
227
238
  ```ts
228
- const unsub = forge.onLevelLoaded('admin', () => {
229
- console.log('admin level loaded')
230
- })
231
- // 取消订阅
232
- unsub()
239
+ // 绑定层级 自动触发 load,提供快捷方法
240
+ const bound = forge.use('admin')
241
+ bound('users.show', { user: 1 }) // 可直接调用(= bound.api())
242
+ bound.route('users.show') // URL 生成
243
+ bound.level // → 'admin'
244
+ bound.levelLoaded // Promise<void>
245
+
246
+ // 绑定层级 + 前缀 — 路由名自动拼接
247
+ const bound = forge.use('admin', 'users')
248
+ bound('show', { user: 1 }) // → forge.api('admin', 'users.show', ...)
249
+
250
+ // BoundForge 独有方法
251
+ await bound.onLevelLoaded() // 等待 level 加载完成
252
+ const prefixed = bound.useRoutePrefix('posts') // 追加前缀
253
+ ```
254
+
255
+ ### IIFE 浏览器用法
256
+
257
+ 通过 `<script>` 标签引入后,`RouteForge` 全局可用:
258
+
259
+ ```html
260
+ <script src="https://unpkg.com/@route-forge/core"></script>
261
+ <script>
262
+ const forge = RouteForge.createRouteForge({
263
+ endpoint: '/_forge/routes',
264
+ })
265
+
266
+ // 等待就绪后绑定层级
267
+ forge.ready().then(function(f) {
268
+ const admin = f.use('admin')
269
+ return admin.onLevelLoaded()
270
+ }).then(function(bound) {
271
+ return bound('users.show', { user: 1 })
272
+ }).then(function(data) {
273
+ console.log(data)
274
+ })
275
+ </script>
233
276
  ```
234
277
 
235
278
  ### Auto-discovery 守卫
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { R as RouteMeta } from './types-BcaNLA_l.cjs';
2
+ import { R as RouteMeta } from './types-Cd1c1OdJ.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-BcaNLA_l.js';
2
+ import { R as RouteMeta } from './types-Cd1c1OdJ.js';
3
3
 
4
4
  /**
5
5
  * @route-forge/core codegen CLI
package/dist/index.cjs CHANGED
@@ -467,6 +467,39 @@ async function resolveAdapter(opts) {
467
467
  return createBuiltinHttp(opts.forgeInterceptors);
468
468
  }
469
469
 
470
+ // src/resolveRouteName.ts
471
+ async function resolveRouteName(forge, level, prefix, suffix, separator = ".") {
472
+ if (!suffix) return prefix;
473
+ const joined = `${prefix}${separator}${suffix}`;
474
+ if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
475
+ await forge.load(level);
476
+ if (forge.hasRoute(level, joined)) return joined;
477
+ if (forge.hasRoute(level, suffix)) return suffix;
478
+ throw new UnknownRouteError(joined, level);
479
+ }
480
+ function resolveRouteNameSync(forge, level, prefix, suffix, separator = ".") {
481
+ if (!suffix) return prefix;
482
+ const joined = `${prefix}${separator}${suffix}`;
483
+ if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
484
+ if (forge.hasRoute(level, joined)) return joined;
485
+ if (forge.hasRoute(level, suffix)) return suffix;
486
+ throw new UnknownRouteError(joined, level);
487
+ }
488
+
489
+ // src/defineImmutableProps.ts
490
+ function defineImmutableProps(target, props) {
491
+ for (const key of Object.keys(props)) {
492
+ const val = props[key];
493
+ Object.defineProperty(target, key, {
494
+ value: val !== null && typeof val === "object" ? Object.freeze(val) : val,
495
+ writable: false,
496
+ enumerable: false,
497
+ configurable: false
498
+ });
499
+ }
500
+ return target;
501
+ }
502
+
470
503
  // src/loading.ts
471
504
  var LoadingTracker = class {
472
505
  constructor() {
@@ -633,7 +666,6 @@ function createRouteForge(options) {
633
666
  const readyPromise = new Promise((resolve) => {
634
667
  resolveReady = resolve;
635
668
  });
636
- const levelLoadedListeners = /* @__PURE__ */ new Map();
637
669
  const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
638
670
  const cacheStorage = cacheOpts.storage ?? "memory";
639
671
  const cache = new RouteCache({ storage: cacheStorage, ttl: cacheTtl });
@@ -685,7 +717,7 @@ function createRouteForge(options) {
685
717
  function assertDiscoveryReady() {
686
718
  if (!autoDiscoveryCompleted && !explicitLevels?.length) {
687
719
  throw new ForgeError(
688
- "Route data not available. Auto-discovery has not completed. Use onSummaryReady callback to mount app, or await forge.ready / forge.load(level) first.",
720
+ "Route data not available. Auto-discovery has not completed. Use forge.ready() or forge.use(level) first.",
689
721
  { code: "RF_FE_010" }
690
722
  );
691
723
  }
@@ -738,15 +770,11 @@ function createRouteForge(options) {
738
770
  routes[r.name] = { ...r, level: UNASSIGNED_LEVEL };
739
771
  }
740
772
  cache.set({ level: UNASSIGNED_LEVEL, routes, cache: null });
741
- const listeners = levelLoadedListeners.get(level);
742
- if (listeners) listeners.forEach((cb) => cb());
743
773
  return;
744
774
  }
745
775
  const resp = await fetchLevel(level);
746
776
  if ((invalidationGens.get(level) ?? 0) === gen) {
747
777
  cache.set(resp);
748
- const listeners = levelLoadedListeners.get(level);
749
- if (listeners) listeners.forEach((cb) => cb());
750
778
  }
751
779
  } finally {
752
780
  inflight.delete(level);
@@ -820,23 +848,40 @@ function createRouteForge(options) {
820
848
  }
821
849
  return void 0;
822
850
  }
823
- async function api(level, name, params = {}) {
824
- await autoDiscoveryPromise;
825
- await load(level);
826
- const meta = findRouteMeta(level, name);
827
- if (!meta) {
828
- throw new UnknownRouteError(name, level);
829
- }
830
- return doApiCall(meta, params);
851
+ function api(level, name, params = {}) {
852
+ let ctrl;
853
+ let abortedBeforeInit = false;
854
+ let abortReason;
855
+ const work = (async () => {
856
+ ctrl = new AbortController();
857
+ if (abortedBeforeInit) {
858
+ ctrl.abort(abortReason);
859
+ }
860
+ await autoDiscoveryPromise;
861
+ await load(level);
862
+ const meta = findRouteMeta(level, name);
863
+ if (!meta) {
864
+ throw new UnknownRouteError(name, level);
865
+ }
866
+ return doApiCall(meta, params, ctrl.signal);
867
+ })();
868
+ const request = work;
869
+ request.abort = () => {
870
+ if (ctrl) {
871
+ ctrl.abort();
872
+ } else {
873
+ abortedBeforeInit = true;
874
+ }
875
+ };
876
+ return request;
831
877
  }
832
- async function doApiCall(meta, params) {
878
+ async function doApiCall(meta, params, signal) {
833
879
  const {
834
880
  pathParams,
835
881
  query,
836
882
  body,
837
883
  headers,
838
- timeout: perCallTimeout,
839
- signal
884
+ timeout: perCallTimeout
840
885
  } = resolveApiParams(params);
841
886
  if (signal?.aborted) {
842
887
  throw new RequestAbortedError(meta.name, meta.level, signal.reason);
@@ -880,7 +925,7 @@ function createRouteForge(options) {
880
925
  },
881
926
  (err) => {
882
927
  if (err instanceof ForgeError) throw err;
883
- if (isAbortError2(err, finalConfig.signal)) {
928
+ if (isAbortError2(err, signal)) {
884
929
  throw new RequestAbortedError(meta.name, meta.level, err);
885
930
  }
886
931
  throw new NetworkError(
@@ -948,6 +993,7 @@ function createRouteForge(options) {
948
993
  return result;
949
994
  }
950
995
  void autoDiscoveryPromise.then(() => {
996
+ autoDiscoveryCompleted = true;
951
997
  options.onSummaryReady?.();
952
998
  if (effectiveEager.length > 0) {
953
999
  return Promise.all(effectiveEager.map((lvl) => load(lvl))).catch((e) => {
@@ -955,11 +1001,74 @@ function createRouteForge(options) {
955
1001
  });
956
1002
  }
957
1003
  }).then(() => {
958
- autoDiscoveryCompleted = true;
959
- resolveReady();
1004
+ resolveReady(forgeInstance);
960
1005
  }).catch(() => {
961
1006
  });
962
- return {
1007
+ function ready(onFulfilled, onRejected) {
1008
+ if (onFulfilled) {
1009
+ const p = readyPromise.then(onFulfilled, onRejected);
1010
+ return p.then(() => forgeInstance);
1011
+ }
1012
+ return readyPromise;
1013
+ }
1014
+ const forgeResolver = { load, hasRoute };
1015
+ function createBoundForge(level, prefix) {
1016
+ const levelLoadedPromise = load(level).catch(() => {
1017
+ });
1018
+ const apiFn = prefix ? (name, params) => resolveRouteName(forgeResolver, level, prefix, name).then(
1019
+ (resolved) => api(level, resolved, params)
1020
+ ) : (name, params) => api(level, name, params);
1021
+ const callable = apiFn;
1022
+ defineImmutableProps(callable, {
1023
+ level,
1024
+ ...prefix !== void 0 ? { prefix } : {},
1025
+ api: apiFn,
1026
+ route: prefix ? (name, params) => route(level, resolveRouteNameSync(forgeResolver, level, prefix, name), params) : (name, params) => route(level, name, params),
1027
+ url: prefix ? (name, params) => route(level, resolveRouteNameSync(forgeResolver, level, prefix, name), params) : (name, params) => route(level, name, params),
1028
+ hasRoute: (name) => hasRoute(level, name),
1029
+ getRoutes: () => getRoutes(level),
1030
+ load: () => load(level),
1031
+ invalidate: () => invalidate(level),
1032
+ isLoaded: () => isLoaded(level),
1033
+ isLoading: () => loadingTracker.isLoading(),
1034
+ onLoadingChange: (cb) => loadingTracker.subscribe(cb)
1035
+ });
1036
+ Object.defineProperty(callable, "levelLoaded", {
1037
+ value: levelLoadedPromise,
1038
+ writable: false,
1039
+ enumerable: true,
1040
+ configurable: true
1041
+ });
1042
+ return callable;
1043
+ }
1044
+ function boundOnLevelLoaded(bound, levelLoadedPromise, onFulfilled, onRejected) {
1045
+ const p = levelLoadedPromise.then(() => bound);
1046
+ if (onFulfilled) {
1047
+ return p.then(onFulfilled, onRejected).then(() => bound);
1048
+ }
1049
+ return p;
1050
+ }
1051
+ function attachBoundMethods(bound, levelLoadedPromise, level) {
1052
+ Object.defineProperty(bound, "onLevelLoaded", {
1053
+ value: (onFulfilled, onRejected) => boundOnLevelLoaded(bound, levelLoadedPromise, onFulfilled, onRejected),
1054
+ writable: false,
1055
+ enumerable: false,
1056
+ configurable: false
1057
+ });
1058
+ Object.defineProperty(bound, "useRoutePrefix", {
1059
+ value: (prefix) => createBoundForgeWithMethods(level, prefix),
1060
+ writable: false,
1061
+ enumerable: false,
1062
+ configurable: false
1063
+ });
1064
+ }
1065
+ function createBoundForgeWithMethods(level, prefix) {
1066
+ const bound = createBoundForge(level, prefix);
1067
+ const levelLoadedPromise = bound.levelLoaded;
1068
+ attachBoundMethods(bound, levelLoadedPromise, level);
1069
+ return bound;
1070
+ }
1071
+ const forgeInstance = {
963
1072
  api,
964
1073
  load,
965
1074
  route,
@@ -974,15 +1083,13 @@ function createRouteForge(options) {
974
1083
  request: requestInterceptors,
975
1084
  response: responseInterceptors
976
1085
  },
977
- ready: readyPromise,
978
- onLevelLoaded(level, cb) {
979
- if (!levelLoadedListeners.has(level)) {
980
- levelLoadedListeners.set(level, /* @__PURE__ */ new Set());
981
- }
982
- levelLoadedListeners.get(level).add(cb);
983
- return () => levelLoadedListeners.get(level)?.delete(cb);
1086
+ ready,
1087
+ use(level, prefix) {
1088
+ if (level === void 0) return forgeInstance;
1089
+ return createBoundForgeWithMethods(level, prefix);
984
1090
  }
985
1091
  };
1092
+ return forgeInstance;
986
1093
  }
987
1094
  function pickMethod(meta) {
988
1095
  const m = meta.methods.find((x) => x.toUpperCase() !== "HEAD");
@@ -1006,7 +1113,6 @@ function resolveApiParams(input) {
1006
1113
  body: rawBody,
1007
1114
  headers: rawHeaders,
1008
1115
  timeout: perCallTimeout,
1009
- signal,
1010
1116
  ...flatRest
1011
1117
  } = input;
1012
1118
  const pathParams = explicitParams ? { ...explicitParams } : {};
@@ -1039,7 +1145,7 @@ function resolveApiParams(input) {
1039
1145
  pathParams.headers = rawHeaders;
1040
1146
  }
1041
1147
  }
1042
- return { pathParams, query, body, headers, timeout: perCallTimeout, signal };
1148
+ return { pathParams, query, body, headers, timeout: perCallTimeout };
1043
1149
  }
1044
1150
  function isAbortError2(err, signal) {
1045
1151
  if (signal?.aborted) return true;
@@ -1050,25 +1156,6 @@ function isAbortError2(err, signal) {
1050
1156
  return false;
1051
1157
  }
1052
1158
 
1053
- // src/resolveRouteName.ts
1054
- async function resolveRouteName(forge, level, prefix, suffix, separator = ".") {
1055
- if (!suffix) return prefix;
1056
- const joined = `${prefix}${separator}${suffix}`;
1057
- if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
1058
- await forge.load(level);
1059
- if (forge.hasRoute(level, joined)) return joined;
1060
- if (forge.hasRoute(level, suffix)) return suffix;
1061
- throw new UnknownRouteError(joined, level);
1062
- }
1063
- function resolveRouteNameSync(forge, level, prefix, suffix, separator = ".") {
1064
- if (!suffix) return prefix;
1065
- const joined = `${prefix}${separator}${suffix}`;
1066
- if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
1067
- if (forge.hasRoute(level, joined)) return joined;
1068
- if (forge.hasRoute(level, suffix)) return suffix;
1069
- throw new UnknownRouteError(joined, level);
1070
- }
1071
-
1072
1159
  exports.AdapterNotFoundError = AdapterNotFoundError;
1073
1160
  exports.ForgeError = ForgeError;
1074
1161
  exports.HTTPError = HTTPError;