ui5-lib-guard-router 1.5.3 → 1.6.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
@@ -28,7 +28,7 @@ npm install ui5-lib-guard-router
28
28
  ```
29
29
 
30
30
  > [!NOTE]
31
- > The npm package is ~150 KB compressed (~670 KB unpacked) because it ships both pre-built distributables (`dist/`) and TypeScript sources (`src/`) to support multiple [serving options](#serving-the-library). At runtime, the browser loads only the `library-preload.js` bundle (~25 KB).
31
+ > The npm package is ~150 KB compressed (~670 KB unpacked) because it ships both pre-built distributables (`dist/`) and TypeScript sources (`src/`) to support multiple [serving options](#serving-the-library). At runtime, the browser loads only the `library-preload.js` bundle (~29 KB).
32
32
 
33
33
  ### TypeScript
34
34
 
@@ -210,6 +210,13 @@ All guard registration and removal methods return `this` for chaining. `navigati
210
210
  | `removeRouteGuard(routeName, { beforeEnter?, beforeLeave? })` | Remove enter and/or leave guards via object form |
211
211
  | `removeLeaveGuard(routeName, fn)` | Remove a leave guard |
212
212
 
213
+ ### Route metadata
214
+
215
+ | Method | Description |
216
+ | ------------------------------- | --------------------------------------------------------------------- |
217
+ | `getRouteMeta(routeName)` | Get resolved metadata (manifest defaults merged with runtime) |
218
+ | `setRouteMeta(routeName, meta)` | Set runtime metadata for a route (replaces previous runtime metadata) |
219
+
213
220
  ### Unknown routes during registration
214
221
 
215
222
  `addRouteGuard()` and `addLeaveGuard()` warn when the route name is unknown at registration time, but they still register the guard. This is intentional so applications can attach guards before dynamic `addRoute()` calls or before route definitions are finalized.
@@ -227,10 +234,12 @@ Every guard receives a `GuardContext` object:
227
234
  | `fromHash` | `string` | Current hash |
228
235
  | `signal` | `AbortSignal` | Aborted when navigation is superseded, or on `stop()`/`destroy()` |
229
236
  | `bag` | `Map<string, unknown>` | Shared mutable store for inter-guard data passing within one navigation |
237
+ | `toMeta` | `Readonly<Record<string, unknown>>` | Resolved metadata for the target route (manifest + runtime, frozen) |
238
+ | `fromMeta` | `Readonly<Record<string, unknown>>` | Resolved metadata for the current route (manifest + runtime, frozen) |
230
239
 
231
240
  ### Return values (`GuardResult`)
232
241
 
233
- Enter guards return `GuardResult`, a union of four outcomes:
242
+ Enter guards return `GuardResult`, covering four behaviors:
234
243
 
235
244
  ```
236
245
  GuardResult = boolean | string | GuardRedirect
@@ -278,7 +287,7 @@ const result: NavigationResult = await router.navigationSettled();
278
287
 
279
288
  | `result.status` | Meaning |
280
289
  | ------------------------------ | ------------------------------------------------------------------------------------------------------- |
281
- | `NavigationOutcome.Committed` | Guards allowed the navigation; target route is now active |
290
+ | `NavigationOutcome.Committed` | Guards allowed the navigation; target route is active |
282
291
  | `NavigationOutcome.Bypassed` | Guards allowed the navigation, but no route matched; UI5 continued with `bypassed` / not-found handling |
283
292
  | `NavigationOutcome.Blocked` | A guard blocked navigation; previous route stays active |
284
293
  | `NavigationOutcome.Redirected` | A guard redirected navigation to a different route |
@@ -353,7 +362,7 @@ Use `detachNavigationSettled(fnFunction, oListener)` to remove the listener. The
353
362
 
354
363
  ### Error handling
355
364
 
356
- When a guard throws or its Promise rejects, the navigation settles as `Error` with `result.error` containing the thrown value. The previous route stays active. This is distinct from `Blocked` (intentional denial) `Error` indicates an unexpected failure.
365
+ When a guard throws or its Promise rejects, the navigation settles as `Error` with `result.error` containing the thrown value. The previous route stays active. `Error` indicates an unexpected failure, as opposed to `Blocked` which signals intentional denial.
357
366
 
358
367
  ### Execution order
359
368
 
@@ -375,15 +384,20 @@ Guards can be declared directly in `manifest.json` using the `guardRouter` block
375
384
  "config": {
376
385
  "routerClass": "ui5.guard.router.Router",
377
386
  "guardRouter": {
378
- "unknownRouteGuardRegistration": "warn",
387
+ "unknownRouteRegistration": "warn",
379
388
  "navToPreflight": "guard",
380
389
  "guardLoading": "lazy",
390
+ "inheritance": "none",
381
391
  "guards": {
382
392
  "*": ["guards.authGuard"],
383
393
  "admin": {
384
394
  "enter": ["guards.adminGuard"],
385
395
  "leave": ["guards.unsavedChangesGuard"]
386
396
  }
397
+ },
398
+ "routeMeta": {
399
+ "admin": { "requiresAuth": true, "roles": ["admin"] },
400
+ "profile": { "requiresAuth": true }
387
401
  }
388
402
  }
389
403
  }
@@ -394,11 +408,14 @@ Guards can be declared directly in `manifest.json` using the `guardRouter` block
394
408
 
395
409
  ### Router options
396
410
 
397
- | Option | Values | Default | Description |
398
- | ------------------------------- | ----------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
399
- | `unknownRouteGuardRegistration` | `"ignore"` \| `"warn"` \| `"throw"` | `"warn"` | What to do when a guard is declared for a route name that doesn't exist in the manifest |
400
- | `navToPreflight` | `"guard"` \| `"bypass"` \| `"off"` | `"guard"` | Whether `navTo()` calls run through the guard pipeline (`"guard"`), skip guards (`"bypass"`), or the preflight is disabled entirely (`"off"`) |
401
- | `guardLoading` | `"block"` \| `"lazy"` | `"lazy"` | `"lazy"`: registers lazy wrappers, loads modules on first navigation; a preload hint fires in the constructor to warm the cache; `initialize()` is always synchronous. `"block"`: loads all modules before `initialize()` completes; `initialize()` is async. |
411
+ | Option | Values | Default | Description |
412
+ | -------------------------- | ----------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
413
+ | `unknownRouteRegistration` | `"ignore"` \| `"warn"` \| `"throw"` | `"warn"` | Policy for guard and metadata registration against unknown route names |
414
+ | `navToPreflight` | `"guard"` \| `"bypass"` \| `"off"` | `"guard"` | Whether `navTo()` calls run through the guard pipeline (`"guard"`), skip guards (`"bypass"`), or the preflight is disabled entirely (`"off"`) |
415
+ | `guardLoading` | `"block"` \| `"lazy"` | `"lazy"` | `"lazy"`: registers lazy wrappers, loads modules on first navigation; a preload hint fires in the constructor to warm the cache; `initialize()` is always synchronous. `"block"`: loads all modules before `initialize()` completes; `initialize()` is async. |
416
+ | `inheritance` | `"none"` \| `"pattern-tree"` | `"none"` | `"none"`: guards and metadata apply only to their declared route. `"pattern-tree"`: guards propagate to all routes whose URL pattern extends the declared route's pattern; metadata propagates via shallow merge (child values override ancestor values on conflict). |
417
+
418
+ The `guardRouter` block also accepts `guards` (see [Declarative guards](#declarative-guards)) and `routeMeta` (see [Route metadata](#route-metadata)).
402
419
 
403
420
  ### Declarative guards
404
421
 
@@ -492,7 +509,7 @@ export default function authGuard(context: GuardContext): GuardResult {
492
509
  **Shape 2: Array (ordered guards)**
493
510
 
494
511
  ```typescript
495
- // guards/checks.ts registered as "checks#0", "checks#1"
512
+ // guards/checks.ts (registered as "checks#0", "checks#1")
496
513
  import type { GuardContext, GuardResult } from "ui5/guard/router/types";
497
514
 
498
515
  export default [
@@ -508,7 +525,7 @@ export default [
508
525
  **Shape 3: Plain Object (named guards)**
509
526
 
510
527
  ```typescript
511
- // guards/security.ts registered as "checkAuth", "checkRole"
528
+ // guards/security.ts (registered as "checkAuth", "checkRole")
512
529
  import type { GuardContext, GuardResult } from "ui5/guard/router/types";
513
530
 
514
531
  export default {
@@ -523,6 +540,9 @@ export default {
523
540
 
524
541
  Detection: function produces a single guard, `Array` produces ordered guards, and a plain object produces named guards in key order. Non-function entries in arrays and objects are warned and skipped. Empty arrays and objects are warned and produce no guards.
525
542
 
543
+ > [!NOTE]
544
+ > When a module path appears in a `"leave"` array, the exported function acts as a `LeaveGuardFn` and must return `boolean`. Returning a string or `GuardRedirect` from a leave guard is not an error, but any non-`true` value is treated as a block. Redirects from leave guards are not supported. Use enter guards for redirection.
545
+
526
546
  ### Cherry-pick syntax
527
547
 
528
548
  When a module exports multiple guards, you can register a subset using `#` to select by name or index:
@@ -568,6 +588,38 @@ The bag is typed as `Map<string, unknown>`, so consumers cast on `.get()`. This
568
588
 
569
589
  This is useful for avoiding repeated work (such as fetching the current user) when multiple guards need the same data in a single navigation.
570
590
 
591
+ ### Route metadata
592
+
593
+ Per-route metadata can be declared in the manifest under `guardRouter.routeMeta`. Keys are route names, values are arbitrary objects. The router stores but never interprets the metadata. Guards read it from `context.toMeta` and `context.fromMeta`.
594
+
595
+ ```json
596
+ "guardRouter": {
597
+ "routeMeta": {
598
+ "admin": { "requiresAuth": true, "roles": ["admin"] },
599
+ "profile": { "requiresAuth": true },
600
+ "home": { "public": true }
601
+ }
602
+ }
603
+ ```
604
+
605
+ A single global guard can then implement policy-driven access control:
606
+
607
+ ```typescript
608
+ router.addGuard((context) => {
609
+ if (context.toMeta.requiresAuth && !isLoggedIn()) return "login";
610
+ if (context.toMeta.roles && !hasAnyRole(context.toMeta.roles as string[])) return "forbidden";
611
+ return true;
612
+ });
613
+ ```
614
+
615
+ Runtime metadata can be set programmatically via `setRouteMeta()`. When read via `getRouteMeta()`, runtime values take precedence over manifest defaults:
616
+
617
+ ```typescript
618
+ router.setRouteMeta("betaFeature", { enabled: featureToggle.isActive("beta") });
619
+ ```
620
+
621
+ `getRouteMeta()` returns a frozen object with manifest defaults merged with runtime overrides. When `inheritance: "pattern-tree"` is enabled, the result also includes metadata inherited from ancestor routes (see [Guard and metadata inheritance](#guard-and-metadata-inheritance)). For unconfigured routes, it returns an empty frozen object.
622
+
571
623
  ### `skipGuards` option
572
624
 
573
625
  Pass `{ skipGuards: true }` as the fourth argument to `navTo()` to bypass all guards for a single call. Use this for internal redirects or navigations that should not be subject to guard logic:
@@ -576,11 +628,59 @@ Pass `{ skipGuards: true }` as the fourth argument to `navTo()` to bypass all gu
576
628
  router.navTo("settings", {}, false, { skipGuards: true });
577
629
  ```
578
630
 
631
+ ### Guard and metadata inheritance
632
+
633
+ When `inheritance` is set to `"pattern-tree"`, guards declared on a route automatically apply to all routes whose URL pattern extends that route's pattern:
634
+
635
+ ```json
636
+ "guardRouter": {
637
+ "inheritance": "pattern-tree",
638
+ "guards": {
639
+ "employees": ["guards.authGuard"]
640
+ }
641
+ }
642
+ ```
643
+
644
+ With routes `employees`, `employees/{id}`, and `employees/{id}/resume`, the auth guard runs for all three. Ancestor guards run before descendant guards.
645
+
646
+ With the same `inheritance: "pattern-tree"` setting, route metadata also propagates. Inheritance is determined by URL patterns. Assuming the route-to-pattern mapping `"employees"` -> `employees`, `"employee"` -> `employees/{id}`, `"employeeResume"` -> `employees/{id}/resume`:
647
+
648
+ ```json
649
+ "guardRouter": {
650
+ "inheritance": "pattern-tree",
651
+ "routeMeta": {
652
+ "employees": { "section": "hr", "requiresAuth": true },
653
+ "employee": { "clearance": "manager" }
654
+ }
655
+ }
656
+ ```
657
+
658
+ `getRouteMeta("employeeResume")` returns `{ section: "hr", requiresAuth: true, clearance: "manager" }` (inherited from both `employees` and `employee`). `getRouteMeta("employee")` returns `{ section: "hr", requiresAuth: true, clearance: "manager" }` (merged, own values win).
659
+
660
+ Defaults to `"none"` for backward compatibility.
661
+
662
+ **Root-pattern route (`""`) is a universal ancestor.** A route with an empty pattern (typically "home") is considered an ancestor of every other route in the router. With `pattern-tree` inheritance enabled, metadata or guards declared on the root-pattern route propagate to all routes:
663
+
664
+ ```json
665
+ "guardRouter": {
666
+ "inheritance": "pattern-tree",
667
+ "routeMeta": {
668
+ "home": { "requiresAuth": true }
669
+ }
670
+ }
671
+ ```
672
+
673
+ Every route in the app inherits `requiresAuth: true` from "home" unless it declares its own override. This is useful for app-wide defaults but requires care. Setting `{ "requiresAuth": false }` on the root route with `pattern-tree` inheritance would make every route public unless explicitly overridden.
674
+
675
+ Metadata is resolved lazily on first access via `getRouteMeta()` and cached until `setRouteMeta()` or `addRoute()` invalidates the cache. Guard inheritance is resolved at `initialize()` time; routes added dynamically via `addRoute()` are integrated into the pattern tree on the fly (inherited guards are registered and the metadata cache is cleared).
676
+
677
+ Runtime metadata set via `setRouteMeta()` participates in inheritance. Child routes see updated ancestor metadata after cache invalidation.
678
+
579
679
  ### Mixing declarative and programmatic guards
580
680
 
581
681
  Manifest guards and programmatic guards coexist on the same pipeline. Manifest guards are registered during `initialize()` (before the first navigation), and programmatic guards are added whenever `addGuard()` / `addRouteGuard()` / `addLeaveGuard()` is called.
582
682
 
583
- **Execution order:** manifest guards run first (in declaration order), then programmatic guards (in registration order). For the same route, both sets execute they are additive, not exclusive.
683
+ **Execution order:** manifest guards run first (in declaration order), then programmatic guards (in registration order). For the same route, both sets execute. They are additive, not exclusive.
584
684
 
585
685
  A common pattern is to declare static guards in the manifest and add context-dependent guards programmatically:
586
686
 
@@ -624,7 +724,7 @@ The demo app keeps `createRedirectWithParamsGuard()` as a reference implementati
624
724
 
625
725
  ### Guard factories
626
726
 
627
- The demo app keeps reusable guard factories in `packages/demo-app/webapp/guards.ts`. `createDirtyFormGuard()` is actively registered as a leave guard on the `"protected"` route; `createAuthGuard()` is a reference-only synchronous alternative to the async `createAsyncPermissionGuard()` used by the demo.
727
+ The demo app keeps reusable guard factories in `packages/demo-app/webapp/guards.ts`. `createDirtyFormGuard()` and `createAuthGuard()` are reference-only implementations showing the factory pattern; the runnable demo uses the manifest-declared `guards/dirtyFormGuard.ts` module for the `"protected"` leave guard and the async `createAsyncPermissionGuard()` for the `"protected"` enter guard.
628
728
 
629
729
  ```typescript
630
730
  // guards.ts
@@ -715,7 +815,10 @@ export default class HomeController extends BaseController {
715
815
  >
716
816
  > In FLP apps with `sap-keep-alive` enabled, the component persists when navigating to other apps. Guards remain registered since the same instance is reused.
717
817
 
718
- ### Metadata-driven guards via manifest
818
+ ### Metadata-driven guards via manifest (legacy alternative)
819
+
820
+ > [!NOTE]
821
+ > The native `guardRouter.routeMeta` configuration (see [Route metadata](#route-metadata)) is the recommended way to declare per-route metadata. The custom-namespace approach below predates native support and is shown for historical reference only.
719
822
 
720
823
  For common patterns like "this route requires authentication", you can store per-route metadata in a custom manifest section and use a single global guard instead of writing repetitive per-route guards:
721
824
 
@@ -732,7 +835,7 @@ For common patterns like "this route requires authentication", you can store per
732
835
  ```
733
836
 
734
837
  ```typescript
735
- // Component.ts read the custom section via getManifestEntry (typed path lookup)
838
+ // Component.ts: read the custom section via getManifestEntry (typed path lookup)
736
839
  type RouteMeta = Record<string, Record<string, unknown>>;
737
840
  const routeMeta = (this.getManifestEntry("/ui5.guard.router/routeMeta") ?? {}) as RouteMeta;
738
841
 
@@ -746,7 +849,7 @@ router.addGuard((context) => {
746
849
 
747
850
  `getManifestEntry()` accepts a path string (starting with `/`) to reach into nested manifest sections. The return type is `any`, so the local `RouteMeta` alias provides type safety at the consumption site.
748
851
 
749
- This keeps guard logic in one place and route annotations in the manifest where they're visible and auditable. The custom namespace `ui5.guard.router` is ignored by the UI5 framework -- it's a convention for your application data.
852
+ This keeps guard logic in one place and route annotations in the manifest where they're visible and auditable. The custom namespace `ui5.guard.router` is ignored by the UI5 framework. It is a convention for application data.
750
853
 
751
854
  ### Native alternative for leave guards: Fiori Launchpad data loss prevention
752
855
 
@@ -907,7 +1010,7 @@ Or set the global log level via URL parameter (per-component filtering is only a
907
1010
 
908
1011
  ### Common issues
909
1012
 
910
- **Guards not running**: Verify the route name passed to `addRouteGuard()` matches the route name in `manifest.json`, not the pattern or target name. Guards on redirect targets do run; if a redirect chain is blocked by loop detection, check the error log for details -- see [Redirect chains](#redirect-chains).
1013
+ **Guards not running**: Verify the route name passed to `addRouteGuard()` matches the route name in `manifest.json`, not the pattern or target name. Guards on redirect targets do run; if a redirect chain is blocked by loop detection, check the error log for details. See [Redirect chains](#redirect-chains).
911
1014
 
912
1015
  **Navigation blocked unexpectedly**: Only a strict `true` return value allows navigation. Returning `undefined`, `null`, or omitting a return statement blocks. Enable debug-level logging to identify which guard blocked.
913
1016
 
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "buildManifest": {
18
18
  "manifestVersion": "0.2",
19
- "timestamp": "2026-03-25T20:16:53.021Z",
19
+ "timestamp": "2026-03-30T16:01:41.057Z",
20
20
  "versions": {
21
21
  "builderVersion": "4.1.4",
22
22
  "projectVersion": "4.0.13",
@@ -31,7 +31,7 @@
31
31
  "includedTasks": [],
32
32
  "excludedTasks": []
33
33
  },
34
- "version": "1.5.3",
34
+ "version": "1.6.0",
35
35
  "namespace": "ui5/guard/router",
36
36
  "tags": {
37
37
  "/resources/ui5/guard/router/GuardPipeline-dbg.js": {
package/dist/index.d.ts CHANGED
@@ -1,39 +1,7 @@
1
- // Generated with TypeScript 5.7.3 / OpenUI5 1.144.0 using:
2
- // - yargs-parser@20.2.9
3
- // - typescript@5.7.3
4
- // - url@0.11.4
5
- // - string_decoder@1.3.0
6
- // - punycode@1.4.1
7
- // - events@3.3.0
8
- // - buffer@6.0.3
9
- // - undici-types@7.18.2
10
- // - @types/yauzl@2.10.3
11
- // - @types/yargs-parser@21.0.3
12
- // - @types/yargs@17.0.35
13
- // - @types/ws@8.18.1
14
- // - @types/which@2.0.2
15
- // - @types/unist@2.0.11
16
- // - @types/three@0.125.3
17
- // - @types/stack-utils@2.0.3
18
- // - @types/sizzle@2.3.10
19
- // - @types/sinonjs__fake-timers@8.1.5
20
- // - @types/sinon@21.0.0
21
- // - @types/shimmer@1.2.0
22
- // - @types/qunit@2.5.4
23
- // - @types/offscreencanvas@2019.6.4
24
- // - @types/npm-package-arg@6.1.4
25
- // - @types/normalize-package-data@2.4.4
26
- // - @types/node@25.5.0
27
- // - @types/mocha@10.0.10
28
- // - @types/minimist@1.2.5
29
- // - @types/minimatch@3.0.5
30
- // - @types/jquery@3.5.13
31
- // - @types/istanbul-reports@3.0.4
32
- // - @types/istanbul-lib-report@3.0.3
33
- // - @types/istanbul-lib-coverage@2.0.6
34
- // - @types/qunit@2.19.13
35
- /// <reference path="./resources/ui5/guard/router/library.d.ts"/>
1
+ // Generated with TypeScript 6.0.2 / OpenUI5 1.144.0 using:
2
+ // - typescript@6.0.2
36
3
  /// <reference path="./resources/ui5/guard/router/GuardPipeline.d.ts"/>
4
+ /// <reference path="./resources/ui5/guard/router/types.d.ts"/>
37
5
  /// <reference path="./resources/ui5/guard/router/Router.d.ts"/>
38
6
  /// <reference path="./resources/ui5/guard/router/NavigationOutcome.d.ts"/>
39
- /// <reference path="./resources/ui5/guard/router/types.d.ts"/>
7
+ /// <reference path="./resources/ui5/guard/router/library.d.ts"/>
@@ -2,7 +2,7 @@
2
2
  <library xmlns="http://www.sap.com/sap.ui.library.xsd">
3
3
  <name>ui5.guard.router</name>
4
4
  <vendor>Marco</vendor>
5
- <version>1.5.3</version>
5
+ <version>1.6.0</version>
6
6
  <copyright></copyright>
7
7
  <title>UI5 Router extension with async navigation guards</title>
8
8
  <documentation>Extends sap.m.routing.Router with async navigation guards, running before route matching begins.</documentation>