@route-forge/core 0.3.0 → 1.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/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # @route-forge/core
2
2
 
3
- 框架无关的命名路由客户端核心:分级懒加载、隔离缓存、并发去重、登录态感知、拦截器。
3
+ 框架无关的命名路由客户端核心:分级懒加载、隔离缓存、并发去重、拦截器。
4
4
 
5
5
  ## 安装
6
6
 
7
7
  ```bash
8
8
  pnpm add @route-forge/core
9
- # 可选:axios 适配器
9
+ # 可选:axios 适配器
10
10
  pnpm add axios
11
11
  ```
12
12
 
@@ -29,6 +29,13 @@ const url = forge.route('public', 'login.show')
29
29
  // url() 是 route() 的语义别名
30
30
  const url2 = forge.url('public', 'login.show')
31
31
 
32
+ // 检查路由是否存在
33
+ forge.hasRoute('admin', 'users.show') // true / false
34
+
35
+ // 获取路由元信息
36
+ const routes = forge.getRoutes('admin') // 指定层级
37
+ const allRoutes = forge.getRoutes() // 全部层级
38
+
32
39
  // 检查层级是否已加载
33
40
  if (!forge.isLoaded('admin')) {
34
41
  await forge.load('admin')
@@ -86,6 +93,88 @@ forge.api('admin', 'search.show', {
86
93
 
87
94
  规则:`params` 优先 > 平铺 `string|number` → 路径参数 > 对象类型按原定义(`query`/`body`/`headers`)。
88
95
 
96
+ ## 认证(Authentication)
97
+
98
+ Route Forge 不内置登录态管理,认证逻辑通过拦截器实现,灵活且完全可控。
99
+
100
+ ### Token 注入
101
+
102
+ ```ts
103
+ // 声明式(初始化时配置)
104
+ const forge = createRouteForge({
105
+ endpoint: '/_forge/routes',
106
+ interceptors: {
107
+ request: [
108
+ (config) => {
109
+ const token = authStore.getToken()
110
+ if (token) {
111
+ config.headers.Authorization = `Bearer ${token}`
112
+ }
113
+ return config
114
+ },
115
+ ],
116
+ },
117
+ })
118
+
119
+ // 或运行时动态注册
120
+ forge.interceptors.request.use((config) => {
121
+ const token = authStore.getToken()
122
+ if (token) {
123
+ config.headers.Authorization = `Bearer ${token}`
124
+ }
125
+ return config
126
+ })
127
+ ```
128
+
129
+ ### 401 响应处理
130
+
131
+ ```ts
132
+ forge.interceptors.response.use(
133
+ (res) => res, // 2xx 正常通过
134
+ (err) => {
135
+ if (err instanceof HTTPError && err.context?.status === 401) {
136
+ authStore.logout()
137
+ window.location.href = '/login'
138
+ }
139
+ return Promise.reject(err)
140
+ },
141
+ )
142
+ ```
143
+
144
+ ### 登出清理
145
+
146
+ ```ts
147
+ function logout() {
148
+ authStore.clearToken()
149
+ forge.invalidate() // 清空路由缓存
150
+ forge.interceptors.request.clear() // 清空拦截器
151
+ forge.interceptors.response.clear()
152
+ }
153
+ ```
154
+
155
+ > 拦截器 API 与 axios 完全一致(`use` / `eject` / `clear`),如果项目已使用 axios 拦截器,可跳过此节——
156
+ > `adapter: 'auto'` 模式下宿主 axios 的拦截器会自动生效。
157
+
158
+ ## 加载状态跟踪
159
+
160
+ 核心始终跟踪并发 API 请求的加载状态,无需配置。不需要使用时,不调用相关 API 即可。
161
+
162
+ ```ts
163
+ // 查询当前是否处于加载中
164
+ forge.isLoading() // boolean
165
+
166
+ // 订阅状态变更
167
+ const unsub = forge.onLoadingChange((event) => {
168
+ console.log(event.loading) // true / false
169
+ console.log(event.count) // 当前并发请求数
170
+ })
171
+
172
+ // 取消订阅
173
+ unsub()
174
+ ```
175
+
176
+ > 加载状态始终跟踪,用户不使用则不订阅即可。Vue/React 包可通过 `onLoadingChange` 订阅状态变更驱动组件显隐。
177
+
89
178
  ## 文档
90
179
 
91
180
  - 仓库主页: https://github.com/xyj2156/route-forge
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { R as RouteMeta } from './types-CBurRtGK.cjs';
2
+ import { R as RouteMeta } from './types-yzY9-FWd.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-CBurRtGK.js';
2
+ import { R as RouteMeta } from './types-yzY9-FWd.js';
3
3
 
4
4
  /**
5
5
  * @route-forge/core codegen CLI
package/dist/index.cjs CHANGED
@@ -50,7 +50,12 @@ var RouteCache = class {
50
50
  return entry;
51
51
  }
52
52
  set(resp) {
53
- const ttl = resp.cache !== void 0 && resp.cache !== null ? resp.cache : this.fallbackTtl;
53
+ let ttl;
54
+ if (resp.cache !== void 0 && resp.cache !== null) {
55
+ ttl = resp.cache > 0 ? Math.min(resp.cache, this.fallbackTtl) : resp.cache;
56
+ } else {
57
+ ttl = this.fallbackTtl;
58
+ }
54
59
  const entry = {
55
60
  level: resp.level,
56
61
  routes: resp.routes,
@@ -135,14 +140,6 @@ var MissingRouteParamError = class extends ForgeError {
135
140
  });
136
141
  }
137
142
  };
138
- var InsufficientAuthError = class extends ForgeError {
139
- constructor(level) {
140
- super(`Insufficient auth: level "${level}" requires login`, {
141
- code: "RF_FE_004",
142
- level
143
- });
144
- }
145
- };
146
143
  var AdapterNotFoundError = class extends ForgeError {
147
144
  constructor(adapter) {
148
145
  super(`Adapter "${adapter}" not available; install axios or use 'builtin'`, {
@@ -369,6 +366,65 @@ async function resolveAdapter(opts) {
369
366
  return createBuiltinHttp(opts.forgeInterceptors);
370
367
  }
371
368
 
369
+ // src/loading.ts
370
+ var LoadingTracker = class {
371
+ constructor() {
372
+ /** 当前并发请求计数 */
373
+ this.count = 0;
374
+ /** 订阅者集合 */
375
+ this.subscribers = /* @__PURE__ */ new Set();
376
+ }
377
+ /**
378
+ * 开始一次加载(计数器 +1)
379
+ */
380
+ start() {
381
+ this.count++;
382
+ this.notify();
383
+ }
384
+ /**
385
+ * 结束一次加载(计数器 -1)
386
+ */
387
+ stop() {
388
+ this.count = Math.max(0, this.count - 1);
389
+ this.notify();
390
+ }
391
+ /**
392
+ * 查询当前是否处于加载中
393
+ */
394
+ isLoading() {
395
+ return this.count > 0;
396
+ }
397
+ /**
398
+ * 获取当前并发计数
399
+ */
400
+ getCount() {
401
+ return this.count;
402
+ }
403
+ /**
404
+ * 订阅加载状态变更
405
+ * @returns 取消订阅函数
406
+ */
407
+ subscribe(cb) {
408
+ this.subscribers.add(cb);
409
+ return () => {
410
+ this.subscribers.delete(cb);
411
+ };
412
+ }
413
+ /** 通知所有订阅者 */
414
+ notify() {
415
+ const event = {
416
+ loading: this.count > 0,
417
+ count: this.count
418
+ };
419
+ for (const cb of this.subscribers) {
420
+ try {
421
+ cb(event);
422
+ } catch {
423
+ }
424
+ }
425
+ }
426
+ };
427
+
372
428
  // src/forge.ts
373
429
  var DEFAULT_TIMEOUT = 3e4;
374
430
  var DEFAULT_CACHE_TTL = 3600;
@@ -378,10 +434,10 @@ function createRouteForge(options) {
378
434
  adapter = "auto",
379
435
  timeout = DEFAULT_TIMEOUT,
380
436
  baseURL = "",
381
- auth,
382
437
  interceptors: declarativeInterceptors,
383
438
  cache: cacheOpts = {}
384
439
  } = options;
440
+ const loadingTracker = new LoadingTracker();
385
441
  const explicitLevels = options.levels;
386
442
  const explicitEager = options.eager;
387
443
  const explicitStrict = options.strict ?? false;
@@ -442,8 +498,12 @@ function createRouteForge(options) {
442
498
  } else {
443
499
  effectiveLevels = backendLevels;
444
500
  }
501
+ const backendEager = backendLevels.filter((lvl) => summary.levels[lvl]?.load === "eager");
445
502
  if (!explicitEager) {
446
- effectiveEager = backendLevels.filter((lvl) => summary.levels[lvl]?.load === "eager");
503
+ effectiveEager = backendEager;
504
+ } else {
505
+ const union = /* @__PURE__ */ new Set([...backendEager, ...explicitEager]);
506
+ effectiveEager = [...union];
447
507
  }
448
508
  });
449
509
  autoDiscoveryPromise.catch(() => {
@@ -454,15 +514,23 @@ function createRouteForge(options) {
454
514
  const requestInterceptors = new InterceptorManagerImpl();
455
515
  const responseInterceptors = new InterceptorManagerImpl();
456
516
  if (declarativeInterceptors?.request) {
457
- for (const pair of declarativeInterceptors.request) {
458
- const [onFulfilled, onRejected] = pair;
459
- requestInterceptors.use(onFulfilled, onRejected);
517
+ for (const entry of declarativeInterceptors.request) {
518
+ if (typeof entry === "function") {
519
+ requestInterceptors.use(entry);
520
+ } else {
521
+ const [onFulfilled, onRejected] = entry;
522
+ requestInterceptors.use(onFulfilled, onRejected);
523
+ }
460
524
  }
461
525
  }
462
526
  if (declarativeInterceptors?.response) {
463
- for (const pair of declarativeInterceptors.response) {
464
- const [onFulfilled, onRejected] = pair;
465
- responseInterceptors.use(onFulfilled, onRejected);
527
+ for (const entry of declarativeInterceptors.response) {
528
+ if (typeof entry === "function") {
529
+ responseInterceptors.use(entry);
530
+ } else {
531
+ const [onFulfilled, onRejected] = entry;
532
+ responseInterceptors.use(onFulfilled, onRejected);
533
+ }
466
534
  }
467
535
  }
468
536
  const adapterPromise = resolveAdapter({
@@ -482,14 +550,6 @@ function createRouteForge(options) {
482
550
  return adapterObj;
483
551
  }
484
552
  const inflight = /* @__PURE__ */ new Map();
485
- function isAuthRequired(level) {
486
- return Boolean(auth?.levels?.[level]);
487
- }
488
- function assertAuth(level) {
489
- if (isAuthRequired(level) && auth?.state && !auth.state()) {
490
- throw new InsufficientAuthError(level);
491
- }
492
- }
493
553
  function assertLevelDeclared(level) {
494
554
  if (!effectiveLevels.includes(level)) {
495
555
  throw new UnknownLevelError(level);
@@ -527,7 +587,6 @@ function createRouteForge(options) {
527
587
  }
528
588
  async function loadOne(level) {
529
589
  assertLevelDeclared(level);
530
- assertAuth(level);
531
590
  if (cache.get(level)) return;
532
591
  const existing = inflight.get(level);
533
592
  if (existing) return existing;
@@ -603,7 +662,6 @@ function createRouteForge(options) {
603
662
  return doApiCall(meta, params);
604
663
  }
605
664
  async function doApiCall(meta, params) {
606
- assertAuth(meta.level ?? "");
607
665
  const { pathParams, query, body, headers } = resolveApiParams(params);
608
666
  const method = pickMethod(meta);
609
667
  const urlWithQuery = appendQuery(buildRequestUrl(meta, pathParams), query);
@@ -620,34 +678,39 @@ function createRouteForge(options) {
620
678
  };
621
679
  const adp = await ensureAdapter();
622
680
  const finalConfig = adp.runsInterceptors ? config : await runRequestInterceptors(requestInterceptors, config);
623
- const source = adp.request(finalConfig).then(
624
- (resp) => {
625
- if (resp.status < 200 || resp.status >= 300) {
626
- throw new HTTPError(
627
- `HTTP ${resp.status} for route "${resp.route}" (${resp.method} ${resp.url})`,
628
- {
629
- route: resp.route,
630
- level: resp.level,
631
- status: resp.status,
632
- url: resp.url,
633
- method: resp.method
634
- }
681
+ loadingTracker.start();
682
+ try {
683
+ const source = adp.request(finalConfig).then(
684
+ (resp) => {
685
+ if (resp.status < 200 || resp.status >= 300) {
686
+ throw new HTTPError(
687
+ `HTTP ${resp.status} for route "${resp.route}" (${resp.method} ${resp.url})`,
688
+ {
689
+ route: resp.route,
690
+ level: resp.level,
691
+ status: resp.status,
692
+ url: resp.url,
693
+ method: resp.method
694
+ }
695
+ );
696
+ }
697
+ return resp;
698
+ },
699
+ (err) => {
700
+ if (err instanceof ForgeError) throw err;
701
+ throw new NetworkError(
702
+ err instanceof Error ? err.message : String(err),
703
+ meta.name,
704
+ meta.level,
705
+ err
635
706
  );
636
707
  }
637
- return resp;
638
- },
639
- (err) => {
640
- if (err instanceof ForgeError) throw err;
641
- throw new NetworkError(
642
- err instanceof Error ? err.message : String(err),
643
- meta.name,
644
- meta.level,
645
- err
646
- );
647
- }
648
- );
649
- if (adp.runsInterceptors) return source;
650
- return runResponseInterceptors(responseInterceptors, source);
708
+ );
709
+ const result = adp.runsInterceptors ? await source : await runResponseInterceptors(responseInterceptors, source);
710
+ return result;
711
+ } finally {
712
+ loadingTracker.stop();
713
+ }
651
714
  }
652
715
  function invalidate(level) {
653
716
  if (level) cache.del(level);
@@ -700,6 +763,8 @@ function createRouteForge(options) {
700
763
  isLoaded,
701
764
  hasRoute,
702
765
  getRoutes,
766
+ isLoading: () => loadingTracker.isLoading(),
767
+ onLoadingChange: (cb) => loadingTracker.subscribe(cb),
703
768
  interceptors: {
704
769
  request: requestInterceptors,
705
770
  response: responseInterceptors
@@ -765,9 +830,9 @@ function resolveApiParams(input) {
765
830
  exports.AdapterNotFoundError = AdapterNotFoundError;
766
831
  exports.ForgeError = ForgeError;
767
832
  exports.HTTPError = HTTPError;
768
- exports.InsufficientAuthError = InsufficientAuthError;
769
833
  exports.InterceptorManagerImpl = InterceptorManagerImpl;
770
834
  exports.InvalidInterceptorReturnError = InvalidInterceptorReturnError;
835
+ exports.LoadingTracker = LoadingTracker;
771
836
  exports.MissingRouteParamError = MissingRouteParamError;
772
837
  exports.NetworkError = NetworkError;
773
838
  exports.RouteCache = RouteCache;