@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 +21 -0
- package/README.md +52 -0
- package/dist/codegen.d.cts +1 -1
- package/dist/codegen.d.ts +1 -1
- package/dist/codegen.js +0 -0
- package/dist/index.cjs +64 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +64 -8
- package/dist/index.js.map +1 -1
- package/dist/{types-pHQLqpYF.d.cts → types-CnZrdSEb.d.cts} +21 -0
- package/dist/{types-pHQLqpYF.d.ts → types-CnZrdSEb.d.ts} +21 -0
- package/package.json +11 -11
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
|
package/dist/codegen.d.cts
CHANGED
package/dist/codegen.d.ts
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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] =
|
|
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] =
|
|
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
|
-
|
|
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
|
}
|