@route-forge/core 1.0.3 → 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 阿杰很厉害
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -186,6 +186,58 @@ unsub()
186
186
 
187
187
  > 加载状态始终跟踪,用户不使用则不订阅即可。Vue/React 包可通过 `onLoadingChange` 订阅状态变更驱动组件显隐。
188
188
 
189
+ ## 初始化合时序与推荐模式
190
+
191
+ ### 三种加载状态
192
+
193
+ | 类型 | 说明 | 跟踪方式 |
194
+ |----------------|--------------------------------|-----------------------------|
195
+ | Auto-discovery | 拉取摘要端点发现 levels/config | 内部 `autoDiscoveryPromise` |
196
+ | Level load | 拉取某层级路由元数据 | `forge.isLoaded(level)` |
197
+ | API request | 业务接口请求 | `forge.isLoading()` |
198
+
199
+ ### `onSummaryReady` 回调
200
+
201
+ 推荐在回调中挂载应用,确保路由数据就绪:
202
+
203
+ ```ts
204
+ const forge = createRouteForge({
205
+ endpoint: '/_forge/routes',
206
+ onSummaryReady: () => {
207
+ // 摘要端点完成,路由数据已可用
208
+ app.mount('#app')
209
+ },
210
+ })
211
+ ```
212
+
213
+ ### `forge.ready` Promise
214
+
215
+ auto-discovery + eager load 完成后 resolve,适合 async/await 风格:
216
+
217
+ ```ts
218
+ const forge = createRouteForge({ endpoint: '/_forge/routes' })
219
+ await forge.ready
220
+ // 路由数据已就绪,可安全调用 route() / hasRoute()
221
+ ```
222
+
223
+ ### `onLevelLoaded` 订阅
224
+
225
+ 订阅指定 level 加载完成事件:
226
+
227
+ ```ts
228
+ const unsub = forge.onLevelLoaded('admin', () => {
229
+ console.log('admin level loaded')
230
+ })
231
+ // 取消订阅
232
+ unsub()
233
+ ```
234
+
235
+ ### Auto-discovery 守卫
236
+
237
+ `route()` / `hasRoute()` 在 auto-discovery 未完成且无 explicit levels 时抛出
238
+ `ForgeError (RF_FE_010)`, 防止在路由数据未就绪时返回错误结果。`api()` 不受影响(内部自动 await
239
+ discovery)。
240
+
189
241
  ## 文档
190
242
 
191
243
  - 仓库主页: https://github.com/xyj2156/route-forge
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { R as RouteMeta } from './types-pHQLqpYF.cjs';
2
+ import { R as RouteMeta } from './types-CnZrdSEb.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-pHQLqpYF.js';
2
+ import { R as RouteMeta } from './types-CnZrdSEb.js';
3
3
 
4
4
  /**
5
5
  * @route-forge/core codegen CLI
package/dist/codegen.js CHANGED
File without changes
package/dist/index.cjs CHANGED
@@ -623,8 +623,16 @@ function createRouteForge(options) {
623
623
  effectiveEager = [...union];
624
624
  }
625
625
  });
626
- autoDiscoveryPromise.catch(() => {
626
+ let autoDiscoveryError = null;
627
+ autoDiscoveryPromise.catch((e) => {
628
+ autoDiscoveryError = e;
627
629
  });
630
+ let autoDiscoveryCompleted = false;
631
+ let resolveReady;
632
+ const readyPromise = new Promise((resolve) => {
633
+ resolveReady = resolve;
634
+ });
635
+ const levelLoadedListeners = /* @__PURE__ */ new Map();
628
636
  const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
629
637
  const cacheStorage = cacheOpts.storage ?? "memory";
630
638
  const cache = new RouteCache({ storage: cacheStorage, ttl: cacheTtl });
@@ -667,11 +675,20 @@ function createRouteForge(options) {
667
675
  return adapterObj;
668
676
  }
669
677
  const inflight = /* @__PURE__ */ new Map();
678
+ const invalidationGens = /* @__PURE__ */ new Map();
670
679
  function assertLevelDeclared(level) {
671
680
  if (!effectiveLevels.includes(level)) {
672
681
  throw new UnknownLevelError(level);
673
682
  }
674
683
  }
684
+ function assertDiscoveryReady() {
685
+ if (!autoDiscoveryCompleted && !explicitLevels?.length) {
686
+ throw new ForgeError(
687
+ "Route data not available. Auto-discovery has not completed. Use onSummaryReady callback to mount app, or await forge.ready / forge.load(level) first.",
688
+ { code: "RF_FE_010" }
689
+ );
690
+ }
691
+ }
675
692
  function buildUrl(level) {
676
693
  const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
677
694
  const ep = effectiveEndpoint.startsWith("/") ? effectiveEndpoint : `/${effectiveEndpoint}`;
@@ -706,10 +723,12 @@ function createRouteForge(options) {
706
723
  return data;
707
724
  }
708
725
  async function loadOne(level) {
726
+ if (autoDiscoveryError) throw autoDiscoveryError;
709
727
  assertLevelDeclared(level);
710
728
  if (cache.get(level)) return;
711
729
  const existing = inflight.get(level);
712
730
  if (existing) return existing;
731
+ const gen = invalidationGens.get(level) ?? 0;
713
732
  const p = (async () => {
714
733
  try {
715
734
  if (level === UNASSIGNED_LEVEL && !backendHasUnassignedLevel && summaryUnassigned) {
@@ -718,10 +737,16 @@ function createRouteForge(options) {
718
737
  routes[r.name] = { ...r, level: UNASSIGNED_LEVEL };
719
738
  }
720
739
  cache.set({ level: UNASSIGNED_LEVEL, routes, cache: null });
740
+ const listeners = levelLoadedListeners.get(level);
741
+ if (listeners) listeners.forEach((cb) => cb());
721
742
  return;
722
743
  }
723
744
  const resp = await fetchLevel(level);
724
- cache.set(resp);
745
+ if ((invalidationGens.get(level) ?? 0) === gen) {
746
+ cache.set(resp);
747
+ const listeners = levelLoadedListeners.get(level);
748
+ if (listeners) listeners.forEach((cb) => cb());
749
+ }
725
750
  } finally {
726
751
  inflight.delete(level);
727
752
  }
@@ -735,6 +760,7 @@ function createRouteForge(options) {
735
760
  await Promise.all(list.map(loadOne));
736
761
  }
737
762
  function route(level, name, params) {
763
+ assertDiscoveryReady();
738
764
  const meta = findRouteMeta(level, name);
739
765
  if (!meta) {
740
766
  throw new UnknownRouteError(name, level);
@@ -765,12 +791,19 @@ function createRouteForge(options) {
765
791
  const optional = raw.endsWith("?");
766
792
  const name = optional ? raw.slice(0, -1) : raw;
767
793
  if (values[name] !== void 0) {
768
- return encodeURIComponent(String(values[name]));
794
+ const val = values[name];
795
+ if (typeof val === "object") {
796
+ throw new ForgeError(
797
+ `Path parameter "${name}" must be a primitive value (string, number, boolean), got ${typeof val}`,
798
+ { code: "RF_FE_003", route: meta.name, context: { param: name, value: val } }
799
+ );
800
+ }
801
+ return encodeURIComponent(String(val));
769
802
  }
770
803
  return optional ? "" : match;
771
804
  });
772
805
  uri = uri.replace(/\/+/g, "/").replace(/\/$/, "");
773
- if (effectiveUrlPrefix.includes("://")) {
806
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(effectiveUrlPrefix)) {
774
807
  const prefix2 = effectiveUrlPrefix.endsWith("/") ? effectiveUrlPrefix.slice(0, -1) : effectiveUrlPrefix;
775
808
  return uri.startsWith("/") ? `${prefix2}${uri}` : `${prefix2}/${uri}`;
776
809
  }
@@ -866,10 +899,20 @@ function createRouteForge(options) {
866
899
  function invalidate(level) {
867
900
  if (level === void 0) {
868
901
  cache.clear();
902
+ inflight.clear();
903
+ for (const lvl of effectiveLevels) {
904
+ invalidationGens.set(lvl, (invalidationGens.get(lvl) ?? 0) + 1);
905
+ }
869
906
  } else if (Array.isArray(level)) {
870
- for (const lvl of level) cache.del(lvl);
907
+ for (const lvl of level) {
908
+ cache.del(lvl);
909
+ inflight.delete(lvl);
910
+ invalidationGens.set(lvl, (invalidationGens.get(lvl) ?? 0) + 1);
911
+ }
871
912
  } else {
872
913
  cache.del(level);
914
+ inflight.delete(level);
915
+ invalidationGens.set(level, (invalidationGens.get(level) ?? 0) + 1);
873
916
  }
874
917
  }
875
918
  function isLoaded(level) {
@@ -877,6 +920,7 @@ function createRouteForge(options) {
877
920
  return effectiveLevels.length > 0 && effectiveLevels.every((lvl) => cache.get(lvl) !== void 0);
878
921
  }
879
922
  function hasRoute(level, name) {
923
+ assertDiscoveryReady();
880
924
  return findRouteMeta(level, name) !== void 0;
881
925
  }
882
926
  function getRoutes(level) {
@@ -885,7 +929,7 @@ function createRouteForge(options) {
885
929
  const routes = entry?.routes ?? {};
886
930
  const result2 = {};
887
931
  for (const [k, v] of Object.entries(routes)) {
888
- result2[k] = { ...v };
932
+ result2[k] = JSON.parse(JSON.stringify(v));
889
933
  }
890
934
  return result2;
891
935
  }
@@ -895,7 +939,7 @@ function createRouteForge(options) {
895
939
  if (entry) {
896
940
  const levelRoutes = {};
897
941
  for (const [k, v] of Object.entries(entry.routes)) {
898
- levelRoutes[k] = { ...v };
942
+ levelRoutes[k] = JSON.parse(JSON.stringify(v));
899
943
  }
900
944
  result[lvl] = levelRoutes;
901
945
  }
@@ -903,11 +947,15 @@ function createRouteForge(options) {
903
947
  return result;
904
948
  }
905
949
  void autoDiscoveryPromise.then(() => {
950
+ options.onSummaryReady?.();
906
951
  if (effectiveEager.length > 0) {
907
- void Promise.all(effectiveEager.map((lvl) => load(lvl))).catch((e) => {
952
+ return Promise.all(effectiveEager.map((lvl) => load(lvl))).catch((e) => {
908
953
  console.warn(`[route-forge] eager load failed: ${e.message}`);
909
954
  });
910
955
  }
956
+ }).then(() => {
957
+ autoDiscoveryCompleted = true;
958
+ resolveReady();
911
959
  }).catch(() => {
912
960
  });
913
961
  return {
@@ -924,6 +972,14 @@ function createRouteForge(options) {
924
972
  interceptors: {
925
973
  request: requestInterceptors,
926
974
  response: responseInterceptors
975
+ },
976
+ ready: readyPromise,
977
+ onLevelLoaded(level, cb) {
978
+ if (!levelLoadedListeners.has(level)) {
979
+ levelLoadedListeners.set(level, /* @__PURE__ */ new Set());
980
+ }
981
+ levelLoadedListeners.get(level).add(cb);
982
+ return () => levelLoadedListeners.get(level)?.delete(cb);
927
983
  }
928
984
  };
929
985
  }