@route-forge/core 1.1.0 → 1.2.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
@@ -186,11 +186,63 @@ 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
- - 仓库主页: https://github.com/xyj2156/route-forge
192
- - 设计文档: https://github.com/xyj2156/route-forge/blob/main/.docs/DESIGN.md
193
- - 规范: https://github.com/xyj2156/route-forge/blob/main/.docs/SPEC.md
243
+ - 仓库主页: https://github.com/route-forge/route-forge
244
+ - 设计文档: https://github.com/route-forge/route-forge/blob/main/.docs/DESIGN.md
245
+ - 规范: https://github.com/route-forge/route-forge/blob/main/.docs/SPEC.md
194
246
 
195
247
  ## License
196
248
 
@@ -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-CMouq7zD.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-CMouq7zD.js';
3
3
 
4
4
  /**
5
5
  * @route-forge/core codegen CLI
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ // src/defineImmutableProps.ts
4
+ function defineImmutableProps(target, props) {
5
+ for (const key of Object.keys(props)) {
6
+ const val = props[key];
7
+ Object.defineProperty(target, key, {
8
+ value: val !== null && typeof val === "object" ? Object.freeze(val) : val,
9
+ writable: false,
10
+ enumerable: false,
11
+ configurable: false
12
+ });
13
+ }
14
+ return target;
15
+ }
16
+
17
+ exports.defineImmutableProps = defineImmutableProps;
18
+ //# sourceMappingURL=defineImmutableProps.cjs.map
19
+ //# sourceMappingURL=defineImmutableProps.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/defineImmutableProps.ts"],"names":[],"mappings":";;;AAOO,SAAS,oBAAA,CAAuC,QAAW,KAAA,EAAmC;AACnG,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,EAAG;AACpC,IAAA,MAAM,GAAA,GAAM,MAAM,GAAG,CAAA;AACrB,IAAA,MAAA,CAAO,cAAA,CAAe,QAAQ,GAAA,EAAK;AAAA,MACjC,KAAA,EAAO,QAAQ,IAAA,IAAQ,OAAO,QAAQ,QAAA,GAAW,MAAA,CAAO,MAAA,CAAO,GAAG,CAAA,GAAI,GAAA;AAAA,MACtE,QAAA,EAAU,KAAA;AAAA,MACV,UAAA,EAAY,KAAA;AAAA,MACZ,YAAA,EAAc;AAAA,KACf,CAAA;AAAA,EACH;AACA,EAAA,OAAO,MAAA;AACT","file":"defineImmutableProps.cjs","sourcesContent":["/**\n * 以不可变、不可枚举、不可重配置的方式将属性挂载到目标对象上。\n * 相比 Object.assign 更安全——外部无法通过遍历/删除/重写篡改这些方法。\n * 对象类型的值会浅冻结(Object.freeze),防止内部键被增删改。\n *\n * @internal 仅供框架适配层(vue/react)内部使用,不作为公共 API 导出\n */\nexport function defineImmutableProps<T extends object>(target: T, props: Record<string, unknown>): T {\n for (const key of Object.keys(props)) {\n const val = props[key];\n Object.defineProperty(target, key, {\n value: val !== null && typeof val === 'object' ? Object.freeze(val) : val,\n writable: false,\n enumerable: false,\n configurable: false,\n });\n }\n return target;\n}\n"]}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 以不可变、不可枚举、不可重配置的方式将属性挂载到目标对象上。
3
+ * 相比 Object.assign 更安全——外部无法通过遍历/删除/重写篡改这些方法。
4
+ * 对象类型的值会浅冻结(Object.freeze),防止内部键被增删改。
5
+ *
6
+ * @internal 仅供框架适配层(vue/react)内部使用,不作为公共 API 导出
7
+ */
8
+ declare function defineImmutableProps<T extends object>(target: T, props: Record<string, unknown>): T;
9
+
10
+ export { defineImmutableProps };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 以不可变、不可枚举、不可重配置的方式将属性挂载到目标对象上。
3
+ * 相比 Object.assign 更安全——外部无法通过遍历/删除/重写篡改这些方法。
4
+ * 对象类型的值会浅冻结(Object.freeze),防止内部键被增删改。
5
+ *
6
+ * @internal 仅供框架适配层(vue/react)内部使用,不作为公共 API 导出
7
+ */
8
+ declare function defineImmutableProps<T extends object>(target: T, props: Record<string, unknown>): T;
9
+
10
+ export { defineImmutableProps };
@@ -0,0 +1,17 @@
1
+ // src/defineImmutableProps.ts
2
+ function defineImmutableProps(target, props) {
3
+ for (const key of Object.keys(props)) {
4
+ const val = props[key];
5
+ Object.defineProperty(target, key, {
6
+ value: val !== null && typeof val === "object" ? Object.freeze(val) : val,
7
+ writable: false,
8
+ enumerable: false,
9
+ configurable: false
10
+ });
11
+ }
12
+ return target;
13
+ }
14
+
15
+ export { defineImmutableProps };
16
+ //# sourceMappingURL=defineImmutableProps.js.map
17
+ //# sourceMappingURL=defineImmutableProps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/defineImmutableProps.ts"],"names":[],"mappings":";AAOO,SAAS,oBAAA,CAAuC,QAAW,KAAA,EAAmC;AACnG,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,EAAG;AACpC,IAAA,MAAM,GAAA,GAAM,MAAM,GAAG,CAAA;AACrB,IAAA,MAAA,CAAO,cAAA,CAAe,QAAQ,GAAA,EAAK;AAAA,MACjC,KAAA,EAAO,QAAQ,IAAA,IAAQ,OAAO,QAAQ,QAAA,GAAW,MAAA,CAAO,MAAA,CAAO,GAAG,CAAA,GAAI,GAAA;AAAA,MACtE,QAAA,EAAU,KAAA;AAAA,MACV,UAAA,EAAY,KAAA;AAAA,MACZ,YAAA,EAAc;AAAA,KACf,CAAA;AAAA,EACH;AACA,EAAA,OAAO,MAAA;AACT","file":"defineImmutableProps.js","sourcesContent":["/**\n * 以不可变、不可枚举、不可重配置的方式将属性挂载到目标对象上。\n * 相比 Object.assign 更安全——外部无法通过遍历/删除/重写篡改这些方法。\n * 对象类型的值会浅冻结(Object.freeze),防止内部键被增删改。\n *\n * @internal 仅供框架适配层(vue/react)内部使用,不作为公共 API 导出\n */\nexport function defineImmutableProps<T extends object>(target: T, props: Record<string, unknown>): T {\n for (const key of Object.keys(props)) {\n const val = props[key];\n Object.defineProperty(target, key, {\n value: val !== null && typeof val === 'object' ? Object.freeze(val) : val,\n writable: false,\n enumerable: false,\n configurable: false,\n });\n }\n return target;\n}\n"]}
package/dist/index.cjs CHANGED
@@ -389,10 +389,11 @@ function toHeaders(raw) {
389
389
  }
390
390
  async function wrapAxiosAdapter() {
391
391
  let axios;
392
+ const axiosModule = "axios";
392
393
  try {
393
394
  const mod = await import(
394
395
  /* @vite-ignore */
395
- 'axios'
396
+ axiosModule
396
397
  );
397
398
  axios = mod.default ?? mod;
398
399
  } catch {
@@ -623,8 +624,16 @@ function createRouteForge(options) {
623
624
  effectiveEager = [...union];
624
625
  }
625
626
  });
626
- autoDiscoveryPromise.catch(() => {
627
+ let autoDiscoveryError = null;
628
+ autoDiscoveryPromise.catch((e) => {
629
+ autoDiscoveryError = e;
627
630
  });
631
+ let autoDiscoveryCompleted = false;
632
+ let resolveReady;
633
+ const readyPromise = new Promise((resolve) => {
634
+ resolveReady = resolve;
635
+ });
636
+ const levelLoadedListeners = /* @__PURE__ */ new Map();
628
637
  const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
629
638
  const cacheStorage = cacheOpts.storage ?? "memory";
630
639
  const cache = new RouteCache({ storage: cacheStorage, ttl: cacheTtl });
@@ -667,11 +676,20 @@ function createRouteForge(options) {
667
676
  return adapterObj;
668
677
  }
669
678
  const inflight = /* @__PURE__ */ new Map();
679
+ const invalidationGens = /* @__PURE__ */ new Map();
670
680
  function assertLevelDeclared(level) {
671
681
  if (!effectiveLevels.includes(level)) {
672
682
  throw new UnknownLevelError(level);
673
683
  }
674
684
  }
685
+ function assertDiscoveryReady() {
686
+ if (!autoDiscoveryCompleted && !explicitLevels?.length) {
687
+ 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.",
689
+ { code: "RF_FE_010" }
690
+ );
691
+ }
692
+ }
675
693
  function buildUrl(level) {
676
694
  const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
677
695
  const ep = effectiveEndpoint.startsWith("/") ? effectiveEndpoint : `/${effectiveEndpoint}`;
@@ -706,10 +724,12 @@ function createRouteForge(options) {
706
724
  return data;
707
725
  }
708
726
  async function loadOne(level) {
727
+ if (autoDiscoveryError) throw autoDiscoveryError;
709
728
  assertLevelDeclared(level);
710
729
  if (cache.get(level)) return;
711
730
  const existing = inflight.get(level);
712
731
  if (existing) return existing;
732
+ const gen = invalidationGens.get(level) ?? 0;
713
733
  const p = (async () => {
714
734
  try {
715
735
  if (level === UNASSIGNED_LEVEL && !backendHasUnassignedLevel && summaryUnassigned) {
@@ -718,10 +738,16 @@ function createRouteForge(options) {
718
738
  routes[r.name] = { ...r, level: UNASSIGNED_LEVEL };
719
739
  }
720
740
  cache.set({ level: UNASSIGNED_LEVEL, routes, cache: null });
741
+ const listeners = levelLoadedListeners.get(level);
742
+ if (listeners) listeners.forEach((cb) => cb());
721
743
  return;
722
744
  }
723
745
  const resp = await fetchLevel(level);
724
- cache.set(resp);
746
+ if ((invalidationGens.get(level) ?? 0) === gen) {
747
+ cache.set(resp);
748
+ const listeners = levelLoadedListeners.get(level);
749
+ if (listeners) listeners.forEach((cb) => cb());
750
+ }
725
751
  } finally {
726
752
  inflight.delete(level);
727
753
  }
@@ -735,6 +761,7 @@ function createRouteForge(options) {
735
761
  await Promise.all(list.map(loadOne));
736
762
  }
737
763
  function route(level, name, params) {
764
+ assertDiscoveryReady();
738
765
  const meta = findRouteMeta(level, name);
739
766
  if (!meta) {
740
767
  throw new UnknownRouteError(name, level);
@@ -765,12 +792,19 @@ function createRouteForge(options) {
765
792
  const optional = raw.endsWith("?");
766
793
  const name = optional ? raw.slice(0, -1) : raw;
767
794
  if (values[name] !== void 0) {
768
- return encodeURIComponent(String(values[name]));
795
+ const val = values[name];
796
+ if (typeof val === "object") {
797
+ throw new ForgeError(
798
+ `Path parameter "${name}" must be a primitive value (string, number, boolean), got ${typeof val}`,
799
+ { code: "RF_FE_003", route: meta.name, context: { param: name, value: val } }
800
+ );
801
+ }
802
+ return encodeURIComponent(String(val));
769
803
  }
770
804
  return optional ? "" : match;
771
805
  });
772
806
  uri = uri.replace(/\/+/g, "/").replace(/\/$/, "");
773
- if (effectiveUrlPrefix.includes("://")) {
807
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(effectiveUrlPrefix)) {
774
808
  const prefix2 = effectiveUrlPrefix.endsWith("/") ? effectiveUrlPrefix.slice(0, -1) : effectiveUrlPrefix;
775
809
  return uri.startsWith("/") ? `${prefix2}${uri}` : `${prefix2}/${uri}`;
776
810
  }
@@ -866,10 +900,20 @@ function createRouteForge(options) {
866
900
  function invalidate(level) {
867
901
  if (level === void 0) {
868
902
  cache.clear();
903
+ inflight.clear();
904
+ for (const lvl of effectiveLevels) {
905
+ invalidationGens.set(lvl, (invalidationGens.get(lvl) ?? 0) + 1);
906
+ }
869
907
  } else if (Array.isArray(level)) {
870
- for (const lvl of level) cache.del(lvl);
908
+ for (const lvl of level) {
909
+ cache.del(lvl);
910
+ inflight.delete(lvl);
911
+ invalidationGens.set(lvl, (invalidationGens.get(lvl) ?? 0) + 1);
912
+ }
871
913
  } else {
872
914
  cache.del(level);
915
+ inflight.delete(level);
916
+ invalidationGens.set(level, (invalidationGens.get(level) ?? 0) + 1);
873
917
  }
874
918
  }
875
919
  function isLoaded(level) {
@@ -877,6 +921,7 @@ function createRouteForge(options) {
877
921
  return effectiveLevels.length > 0 && effectiveLevels.every((lvl) => cache.get(lvl) !== void 0);
878
922
  }
879
923
  function hasRoute(level, name) {
924
+ assertDiscoveryReady();
880
925
  return findRouteMeta(level, name) !== void 0;
881
926
  }
882
927
  function getRoutes(level) {
@@ -885,7 +930,7 @@ function createRouteForge(options) {
885
930
  const routes = entry?.routes ?? {};
886
931
  const result2 = {};
887
932
  for (const [k, v] of Object.entries(routes)) {
888
- result2[k] = { ...v };
933
+ result2[k] = JSON.parse(JSON.stringify(v));
889
934
  }
890
935
  return result2;
891
936
  }
@@ -895,7 +940,7 @@ function createRouteForge(options) {
895
940
  if (entry) {
896
941
  const levelRoutes = {};
897
942
  for (const [k, v] of Object.entries(entry.routes)) {
898
- levelRoutes[k] = { ...v };
943
+ levelRoutes[k] = JSON.parse(JSON.stringify(v));
899
944
  }
900
945
  result[lvl] = levelRoutes;
901
946
  }
@@ -903,11 +948,15 @@ function createRouteForge(options) {
903
948
  return result;
904
949
  }
905
950
  void autoDiscoveryPromise.then(() => {
951
+ options.onSummaryReady?.();
906
952
  if (effectiveEager.length > 0) {
907
- void Promise.all(effectiveEager.map((lvl) => load(lvl))).catch((e) => {
953
+ return Promise.all(effectiveEager.map((lvl) => load(lvl))).catch((e) => {
908
954
  console.warn(`[route-forge] eager load failed: ${e.message}`);
909
955
  });
910
956
  }
957
+ }).then(() => {
958
+ autoDiscoveryCompleted = true;
959
+ resolveReady();
911
960
  }).catch(() => {
912
961
  });
913
962
  return {
@@ -924,6 +973,14 @@ function createRouteForge(options) {
924
973
  interceptors: {
925
974
  request: requestInterceptors,
926
975
  response: responseInterceptors
976
+ },
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);
927
984
  }
928
985
  };
929
986
  }
@@ -993,6 +1050,25 @@ function isAbortError2(err, signal) {
993
1050
  return false;
994
1051
  }
995
1052
 
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
+
996
1072
  exports.AdapterNotFoundError = AdapterNotFoundError;
997
1073
  exports.ForgeError = ForgeError;
998
1074
  exports.HTTPError = HTTPError;
@@ -1007,5 +1083,7 @@ exports.UnknownLevelError = UnknownLevelError;
1007
1083
  exports.UnknownRouteError = UnknownRouteError;
1008
1084
  exports.createInterceptorManager = createInterceptorManager;
1009
1085
  exports.createRouteForge = createRouteForge;
1086
+ exports.resolveRouteName = resolveRouteName;
1087
+ exports.resolveRouteNameSync = resolveRouteNameSync;
1010
1088
  //# sourceMappingURL=index.cjs.map
1011
1089
  //# sourceMappingURL=index.cjs.map