ui5-lib-guard-router 1.2.0 → 1.3.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 +168 -53
- package/dist/.ui5/build-manifest.json +2 -2
- package/dist/index.d.ts +7 -3
- package/dist/resources/ui5/guard/router/.library +1 -1
- package/dist/resources/ui5/guard/router/NavigationOutcome-dbg.js +2 -0
- package/dist/resources/ui5/guard/router/NavigationOutcome-dbg.js.map +1 -1
- package/dist/resources/ui5/guard/router/NavigationOutcome.d.ts +2 -0
- package/dist/resources/ui5/guard/router/NavigationOutcome.d.ts.map +1 -1
- package/dist/resources/ui5/guard/router/NavigationOutcome.js +1 -1
- package/dist/resources/ui5/guard/router/NavigationOutcome.js.map +1 -1
- package/dist/resources/ui5/guard/router/Router-dbg.js +349 -87
- package/dist/resources/ui5/guard/router/Router-dbg.js.map +1 -1
- package/dist/resources/ui5/guard/router/Router.d.ts +147 -10
- package/dist/resources/ui5/guard/router/Router.d.ts.map +1 -1
- package/dist/resources/ui5/guard/router/Router.js +1 -1
- package/dist/resources/ui5/guard/router/Router.js.map +1 -1
- package/dist/resources/ui5/guard/router/library-dbg.js +1 -1
- package/dist/resources/ui5/guard/router/library-dbg.js.map +1 -1
- package/dist/resources/ui5/guard/router/library-preload.js +4 -4
- package/dist/resources/ui5/guard/router/library-preload.js.map +1 -1
- package/dist/resources/ui5/guard/router/library.js +1 -1
- package/dist/resources/ui5/guard/router/library.js.map +1 -1
- package/dist/resources/ui5/guard/router/manifest.json +1 -1
- package/dist/resources/ui5/guard/router/types-dbg.js.map +1 -1
- package/dist/resources/ui5/guard/router/types.d.ts +135 -8
- package/dist/resources/ui5/guard/router/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NavigationOutcome.ts +6 -4
- package/src/Router.ts +457 -112
- package/src/manifest.json +1 -1
- package/src/types.ts +140 -8
- package/tsconfig.json +2 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Drop-in replacement for `sap.m.routing.Router` that intercepts navigation **before** route matching, target loading, or view creation, preventing flashes of unauthorized content and polluted browser history.
|
|
4
4
|
|
|
5
|
-
> Born from [SAP/openui5#3411](https://github.com/SAP/openui5/issues/3411), an open request
|
|
5
|
+
> Born from [SAP/openui5#3411](https://github.com/SAP/openui5/issues/3411), an open request for native navigation guard support in UI5.
|
|
6
6
|
>
|
|
7
7
|
> **Related resources**:
|
|
8
8
|
>
|
|
@@ -12,6 +12,9 @@ Drop-in replacement for `sap.m.routing.Router` that intercepts navigation **befo
|
|
|
12
12
|
> [!WARNING]
|
|
13
13
|
> This library is **experimental**. It is not battle-tested in production environments, and the API may change without notice. If you choose to consume it, you do so at your own risk. Make sure to pin your version and review changes before upgrading.
|
|
14
14
|
|
|
15
|
+
> [!CAUTION]
|
|
16
|
+
> Navigation guards are a UX layer, not a security boundary. They can prevent unauthorized content flashes and steer client-side navigation, but they do **not** replace server-side authorization, backend validation, or service-level access control.
|
|
17
|
+
|
|
15
18
|
## Why
|
|
16
19
|
|
|
17
20
|
UI5's router has no way to block or redirect navigation before views render. The usual workaround, scattering guard logic across `attachPatternMatched` callbacks, causes flashes of unauthorized content, polluted browser history, and scattered guard logic across controllers.
|
|
@@ -76,7 +79,7 @@ No transpile tooling, no middleware, no additional `ui5.yaml` changes.
|
|
|
76
79
|
|
|
77
80
|
#### Option B: Transpile from source
|
|
78
81
|
|
|
79
|
-
If you prefer to serve from TypeScript sources (e.g. for debugging with source maps), install [`ui5-tooling-transpile`](https://github.com/
|
|
82
|
+
If you prefer to serve from TypeScript sources (e.g. for debugging with source maps), install [`ui5-tooling-transpile`](https://github.com/ui5-community/ui5-ecosystem-showcase/tree/main/packages/ui5-tooling-transpile) and enable `transpileDependencies` in your app's `ui5.yaml`:
|
|
80
83
|
|
|
81
84
|
```bash
|
|
82
85
|
npm install -D ui5-tooling-transpile
|
|
@@ -96,7 +99,7 @@ This transpiles the library's `.ts` sources on the fly during `ui5 serve`.
|
|
|
96
99
|
|
|
97
100
|
#### Option C: Static serving (workaround)
|
|
98
101
|
|
|
99
|
-
If neither option works for your setup, you can mount the pre-built resources manually using [`ui5-middleware-servestatic`](https://github.com/
|
|
102
|
+
If neither option works for your setup, you can mount the pre-built resources manually using [`ui5-middleware-servestatic`](https://github.com/ui5-community/ui5-ecosystem-showcase/tree/main/packages/ui5-middleware-servestatic) (or a similar community middleware) and point it at the `dist/resources` folder in `node_modules`:
|
|
100
103
|
|
|
101
104
|
```bash
|
|
102
105
|
npm install -D ui5-middleware-servestatic
|
|
@@ -171,7 +174,12 @@ export default class Component extends UIComponent {
|
|
|
171
174
|
|
|
172
175
|
## How it works
|
|
173
176
|
|
|
174
|
-
The library extends [`sap.m.routing.Router`](https://sdk.openui5.org/api/sap.m.routing.Router) and
|
|
177
|
+
The library extends [`sap.m.routing.Router`](https://sdk.openui5.org/api/sap.m.routing.Router) and intercepts navigation through two entry points:
|
|
178
|
+
|
|
179
|
+
- **`navTo()` preflight**: For programmatic navigation (`router.navTo()`), guards run _before_ any hash change occurs. If a guard blocks or redirects, the hash never changes, so no history entry is created.
|
|
180
|
+
- **`parse()` fallback**: For browser-initiated navigation (back/forward buttons, URL bar entry, direct hash changes), guards run inside the `parse()` override after the browser has already changed the hash. If a guard blocks or redirects, the router restores the previous hash via `replaceHash()`.
|
|
181
|
+
|
|
182
|
+
Both entry points feed the same guard pipeline. There is no separate configuration. The same guard functions registered via `addGuard()`, `addRouteGuard()`, and `addLeaveGuard()` protect all navigation paths.
|
|
175
183
|
|
|
176
184
|
Because it extends the mobile router directly, all existing `sap.m.routing.Router` behavior (Targets, route events, `navTo`, back navigation) works unchanged.
|
|
177
185
|
|
|
@@ -179,7 +187,7 @@ The guard pipeline stays **synchronous when all guards return plain values** and
|
|
|
179
187
|
|
|
180
188
|
## API
|
|
181
189
|
|
|
182
|
-
All methods return `this` for chaining.
|
|
190
|
+
All guard registration and removal methods return `this` for chaining. `navigationSettled()` returns a `Promise<NavigationResult>`.
|
|
183
191
|
|
|
184
192
|
### Guard registration
|
|
185
193
|
|
|
@@ -199,6 +207,10 @@ All methods return `this` for chaining.
|
|
|
199
207
|
| `removeRouteGuard(routeName, { beforeEnter?, beforeLeave? })` | Remove enter and/or leave guards via object form |
|
|
200
208
|
| `removeLeaveGuard(routeName, fn)` | Remove a leave guard |
|
|
201
209
|
|
|
210
|
+
### Unknown routes during registration
|
|
211
|
+
|
|
212
|
+
`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.
|
|
213
|
+
|
|
202
214
|
### GuardContext
|
|
203
215
|
|
|
204
216
|
Every guard receives a `GuardContext` object:
|
|
@@ -220,12 +232,12 @@ Enter guards return `GuardResult`, a union of four outcomes:
|
|
|
220
232
|
GuardResult = boolean | string | GuardRedirect
|
|
221
233
|
```
|
|
222
234
|
|
|
223
|
-
| Return | Type | When to use | Effect
|
|
224
|
-
| ---------------------------------------------- | --------------- | ------------------------------------------------------- |
|
|
225
|
-
| `true` | `boolean` | Guard condition passes | Allow navigation
|
|
226
|
-
| `false` | `boolean` | Guard condition fails, no specific destination |
|
|
227
|
-
| `"routeName"` | `string` | Redirect to a fixed route (no parameters needed) |
|
|
228
|
-
| `{ route, parameters?, componentTargetInfo? }` | `GuardRedirect` | Redirect and pass route parameters or component targets |
|
|
235
|
+
| Return | Type | When to use | Effect |
|
|
236
|
+
| ---------------------------------------------- | --------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
237
|
+
| `true` | `boolean` | Guard condition passes | Allow navigation |
|
|
238
|
+
| `false` | `boolean` | Guard condition fails, no specific destination | Stay on current route. Programmatic `navTo()` creates no history entry. Browser-initiated navigation restores the previous hash. |
|
|
239
|
+
| `"routeName"` | `string` | Redirect to a fixed route (no parameters needed) | Navigate to target route. Programmatic `navTo()` goes directly to target with no intermediate entry. Browser-initiated navigation replaces the current hash. |
|
|
240
|
+
| `{ route, parameters?, componentTargetInfo? }` | `GuardRedirect` | Redirect and pass route parameters or component targets | Same as string redirect, with parameters |
|
|
229
241
|
|
|
230
242
|
`GuardRedirect` is the object form of a redirect. Use it when you need to pass route parameters (`parameters`) or nested component targets (`componentTargetInfo`). For simple redirects without parameters, the string shorthand (`return "home"`) is equivalent and shorter.
|
|
231
243
|
|
|
@@ -260,16 +272,19 @@ import NavigationOutcome from "ui5/guard/router/NavigationOutcome";
|
|
|
260
272
|
const result: NavigationResult = await router.navigationSettled();
|
|
261
273
|
```
|
|
262
274
|
|
|
263
|
-
| `result.status` | Meaning
|
|
264
|
-
| ------------------------------ |
|
|
265
|
-
| `NavigationOutcome.Committed` | Guards allowed the navigation; target route is now active
|
|
266
|
-
| `NavigationOutcome.
|
|
267
|
-
| `NavigationOutcome.
|
|
268
|
-
| `NavigationOutcome.
|
|
275
|
+
| `result.status` | Meaning |
|
|
276
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------------------- |
|
|
277
|
+
| `NavigationOutcome.Committed` | Guards allowed the navigation; target route is now active |
|
|
278
|
+
| `NavigationOutcome.Bypassed` | Guards allowed the navigation, but no route matched; UI5 continued with `bypassed` / not-found handling |
|
|
279
|
+
| `NavigationOutcome.Blocked` | A guard blocked navigation; previous route stays active |
|
|
280
|
+
| `NavigationOutcome.Redirected` | A guard redirected navigation to a different route |
|
|
281
|
+
| `NavigationOutcome.Cancelled` | Navigation was cancelled before settling (superseded, stopped, or destroyed) |
|
|
282
|
+
|
|
283
|
+
A guard redirect that fails to trigger a follow-up navigation settles as `Blocked` because no route change commits. A nonexistent route name is the most common cause, and the router logs the target name to help diagnose it.
|
|
269
284
|
|
|
270
|
-
|
|
285
|
+
An accepted unmatched hash settles as `Bypassed` with `route === ""` and the attempted hash preserved in `hash`. Idle `navigationSettled()` calls replay that `Bypassed` result until another navigation settles, matching the existing replay behavior for the other outcomes.
|
|
271
286
|
|
|
272
|
-
If no navigation is in flight, `navigationSettled()` resolves immediately with the most recent settlement result. That makes it safe to call right after `navTo()`, even when guards settle synchronously. On a fresh router
|
|
287
|
+
If no navigation is in flight, `navigationSettled()` resolves immediately with the most recent settlement result. That makes it safe to call right after `navTo()`, even when guards settle synchronously. On a fresh router, this defaults to `Committed` with the instance's current route/hash state. After `stop()`, those fields are reset, so idle calls resolve with empty strings until the next navigation settles. Multiple callers waiting on the same pending navigation all receive the same result.
|
|
273
288
|
|
|
274
289
|
**App code: busy indicator during async guards**
|
|
275
290
|
|
|
@@ -290,6 +305,9 @@ const result = await router.navigationSettled();
|
|
|
290
305
|
switch (result.status) {
|
|
291
306
|
case NavigationOutcome.Committed:
|
|
292
307
|
break; // navigation succeeded
|
|
308
|
+
case NavigationOutcome.Bypassed:
|
|
309
|
+
MessageToast.show("No route matched; showing not-found flow");
|
|
310
|
+
break;
|
|
293
311
|
case NavigationOutcome.Blocked:
|
|
294
312
|
MessageToast.show("Access denied");
|
|
295
313
|
break;
|
|
@@ -310,6 +328,21 @@ assert.strictEqual(result.status, NavigationOutcome.Blocked, "Navigation was blo
|
|
|
310
328
|
assert.strictEqual(result.route, "home", "User stays on home");
|
|
311
329
|
```
|
|
312
330
|
|
|
331
|
+
**Event-based: observe every navigation outcome**
|
|
332
|
+
|
|
333
|
+
`attachNavigationSettled` fires synchronously after every guard pipeline settlement. Unlike the one-shot `navigationSettled()` Promise, the event fires for every navigation without re-registration:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
router.attachNavigationSettled((event) => {
|
|
337
|
+
const status = event.getParameter("status"); // NavigationOutcome
|
|
338
|
+
const route = event.getParameter("route");
|
|
339
|
+
const hash = event.getParameter("hash");
|
|
340
|
+
console.log(`Navigation settled: ${status} on ${route}`);
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Use `detachNavigationSettled(fnFunction, oListener)` to remove the listener. The same function and listener references must match those passed to `attachNavigationSettled`. The event uses UI5's native `EventProvider` mechanism, so the standard `attachEvent` / `detachEvent` pattern also works.
|
|
345
|
+
|
|
313
346
|
### Execution order
|
|
314
347
|
|
|
315
348
|
1. **Leave guards** for the current route (registration order)
|
|
@@ -349,8 +382,12 @@ router.addGuard((context): GuardRedirect | true => {
|
|
|
349
382
|
});
|
|
350
383
|
```
|
|
351
384
|
|
|
385
|
+
The demo app keeps `createRedirectWithParamsGuard()` as a reference implementation in `packages/demo-app/webapp/guards.ts`; the runnable demo routes do not use it because they have no route parameters.
|
|
386
|
+
|
|
352
387
|
### Guard factories
|
|
353
388
|
|
|
389
|
+
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.
|
|
390
|
+
|
|
354
391
|
```typescript
|
|
355
392
|
// guards.ts
|
|
356
393
|
import JSONModel from "sap/ui/model/json/JSONModel";
|
|
@@ -371,16 +408,18 @@ export function createDirtyFormGuard(formModel: JSONModel): LeaveGuardFn {
|
|
|
371
408
|
|
|
372
409
|
### Object form with RouteGuardConfig
|
|
373
410
|
|
|
411
|
+
The runnable demo uses the same object form in `packages/demo-app/webapp/Component.ts`, pairing an async permission check with a leave guard on the `protected` route.
|
|
412
|
+
|
|
374
413
|
```typescript
|
|
375
414
|
import type { RouteGuardConfig } from "ui5/guard/router/types";
|
|
376
415
|
|
|
377
|
-
const
|
|
378
|
-
beforeEnter:
|
|
416
|
+
const protectedGuards: RouteGuardConfig = {
|
|
417
|
+
beforeEnter: createAsyncPermissionGuard(authModel),
|
|
379
418
|
beforeLeave: createDirtyFormGuard(formModel),
|
|
380
419
|
};
|
|
381
420
|
|
|
382
|
-
router.addRouteGuard("
|
|
383
|
-
// later: router.removeRouteGuard("
|
|
421
|
+
router.addRouteGuard("protected", protectedGuards);
|
|
422
|
+
// later: router.removeRouteGuard("protected", protectedGuards);
|
|
384
423
|
```
|
|
385
424
|
|
|
386
425
|
### Dynamic guard registration
|
|
@@ -400,25 +439,28 @@ router.removeGuard(logGuard);
|
|
|
400
439
|
|
|
401
440
|
### Leave guard with controller lifecycle
|
|
402
441
|
|
|
442
|
+
The demo app shows the same lifecycle pattern in `packages/demo-app/webapp/controller/Home.controller.ts`, registering `createHomeLeaveLogger()` on the `home` route and removing it again in `onExit()`.
|
|
443
|
+
|
|
403
444
|
```typescript
|
|
404
445
|
import type { GuardRouter, LeaveGuardFn } from "ui5/guard/router/types";
|
|
405
|
-
import
|
|
446
|
+
import BaseController from "./BaseController";
|
|
447
|
+
import { createHomeLeaveLogger } from "../guards";
|
|
406
448
|
|
|
407
|
-
export default class
|
|
408
|
-
private _leaveGuard: LeaveGuardFn;
|
|
449
|
+
export default class HomeController extends BaseController {
|
|
450
|
+
private _leaveGuard: LeaveGuardFn | null = null;
|
|
409
451
|
|
|
410
452
|
onInit(): void {
|
|
411
|
-
const
|
|
412
|
-
this.
|
|
413
|
-
|
|
414
|
-
const router = UIComponent.getRouterFor(this) as GuardRouter;
|
|
415
|
-
this._leaveGuard = createDirtyFormGuard(formModel);
|
|
416
|
-
router.addLeaveGuard("editOrder", this._leaveGuard);
|
|
453
|
+
const router = this.getRouter<GuardRouter>();
|
|
454
|
+
this._leaveGuard = createHomeLeaveLogger();
|
|
455
|
+
router.addLeaveGuard("home", this._leaveGuard);
|
|
417
456
|
}
|
|
418
457
|
|
|
419
458
|
onExit(): void {
|
|
420
|
-
|
|
421
|
-
|
|
459
|
+
if (this._leaveGuard) {
|
|
460
|
+
const router = this.getRouter<GuardRouter>();
|
|
461
|
+
router.removeLeaveGuard("home", this._leaveGuard);
|
|
462
|
+
this._leaveGuard = null;
|
|
463
|
+
}
|
|
422
464
|
}
|
|
423
465
|
}
|
|
424
466
|
```
|
|
@@ -523,31 +565,43 @@ router.addRouteGuard("dashboard", (context) => {
|
|
|
523
565
|
});
|
|
524
566
|
```
|
|
525
567
|
|
|
526
|
-
###
|
|
568
|
+
### History guarantees differ by navigation source
|
|
569
|
+
|
|
570
|
+
Programmatic `router.navTo()` calls get clean history: blocked or redirected navigations create no history entry. Browser back/forward and URL bar entry may leave an extra history entry because the browser changes the hash before guards can intercept. The guard still protects the route, but the browser history may contain a duplicate entry that the router repairs via `replaceHash()`.
|
|
527
571
|
|
|
528
|
-
|
|
572
|
+
### URL bar shows target hash during async guards (browser-initiated only)
|
|
529
573
|
|
|
530
|
-
|
|
574
|
+
For browser-initiated navigation (back/forward, URL bar entry, direct hash changes), the URL bar shows the target hash while an async guard resolves. If the guard blocks or redirects, the URL reverts via `replaceHash()`. There is a brief window where the displayed URL does not match the active route.
|
|
531
575
|
|
|
532
|
-
|
|
576
|
+
This does **not** apply to programmatic `navTo()` calls, where the hash does not change until guards approve. It also does not affect sync guards on the `parse()` path, which resolve in the same tick as the hash change.
|
|
577
|
+
|
|
578
|
+
**Why the parse() path cannot prevent this**: UI5's `HashChanger` updates the URL and fires `hashChanged` before `parse()` runs. The router cannot prevent the URL change; it can only react to it. Frameworks like Vue Router and Angular Router avoid this by controlling the URL update themselves (calling `history.pushState` only after guards resolve), but UI5's architecture does not allow this without intercepting at the HashChanger level, which is globally scoped and fragile.
|
|
533
579
|
|
|
534
580
|
```
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
581
|
+
Browser-initiated navigation (back/forward, URL bar, setHash):
|
|
582
|
+
HashChanger updates browser URL ← URL changes HERE
|
|
583
|
+
↓
|
|
584
|
+
HashChanger fires hashChanged
|
|
585
|
+
↓
|
|
586
|
+
Router.parse() called ← guards run HERE
|
|
587
|
+
↓
|
|
588
|
+
┌────┴────┐
|
|
589
|
+
allowed blocked
|
|
590
|
+
↓ ↓
|
|
591
|
+
views _restoreHash()
|
|
592
|
+
load reverts URL
|
|
593
|
+
|
|
594
|
+
Programmatic navigation (navTo):
|
|
595
|
+
navTo() called ← guards run HERE
|
|
596
|
+
↓
|
|
597
|
+
┌────┴────┐
|
|
598
|
+
allowed blocked
|
|
599
|
+
↓ ↓
|
|
600
|
+
super.navTo() return
|
|
601
|
+
hash changes (no hash change)
|
|
548
602
|
```
|
|
549
603
|
|
|
550
|
-
|
|
604
|
+
For the `parse()` path, show a busy indicator while async guards resolve. This communicates that navigation is in progress, making the URL bar state a non-issue:
|
|
551
605
|
|
|
552
606
|
```typescript
|
|
553
607
|
router.addRouteGuard("dashboard", async (context) => {
|
|
@@ -567,12 +621,67 @@ router.addRouteGuard("dashboard", async (context) => {
|
|
|
567
621
|
|
|
568
622
|
This follows the same pattern as [TanStack Router's `pendingComponent`](https://tanstack.com/router/latest/docs/framework/react/guide/navigation-blocking#handling-blocked-navigations): the URL reflects the intent while a loading state signals that the navigation hasn't committed yet.
|
|
569
623
|
|
|
624
|
+
## Debugging and Troubleshooting
|
|
625
|
+
|
|
626
|
+
### Enabling guard logs
|
|
627
|
+
|
|
628
|
+
The router logs guard registration errors, pipeline decisions, and async discard events through UI5's `Log` API under the component name `ui5.guard.router.Router`.
|
|
629
|
+
|
|
630
|
+
Enable debug-level output programmatically:
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
import Log from "sap/base/Log";
|
|
634
|
+
Log.setLevel(Log.Level.DEBUG, "ui5.guard.router.Router");
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
Or set the global log level via URL parameter (per-component filtering is only available through the programmatic API above):
|
|
638
|
+
|
|
639
|
+
```
|
|
640
|
+
?sap-ui-log-level=DEBUG
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
> **Note**: UI5 1.120+ uses kebab-case URL parameters (`sap-ui-log-level`). Older versions use camelCase (`sap-ui-logLevel`).
|
|
644
|
+
|
|
645
|
+
### Log reference
|
|
646
|
+
|
|
647
|
+
| Level | Message | Trigger |
|
|
648
|
+
| ------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
649
|
+
| warning | `addGuard called with invalid guard, ignoring` | Non-function passed to `addGuard()` |
|
|
650
|
+
| warning | `addRouteGuard called with invalid guard, ignoring` | Non-function `beforeEnter`, `beforeLeave`, or direct guard |
|
|
651
|
+
| info | `addRouteGuard called with config missing both beforeEnter and beforeLeave` | Empty `RouteGuardConfig` object (no handlers) |
|
|
652
|
+
| warning | `addLeaveGuard called with invalid guard, ignoring` | Non-function passed to `addLeaveGuard()` |
|
|
653
|
+
| warning | `removeGuard called with invalid guard, ignoring` | Non-function passed to `removeGuard()` |
|
|
654
|
+
| warning | `removeRouteGuard called with invalid guard, ignoring` | Non-function passed to `removeRouteGuard()` |
|
|
655
|
+
| warning | `removeLeaveGuard called with invalid guard, ignoring` | Non-function passed to `removeLeaveGuard()` |
|
|
656
|
+
| warning | `{method} called for unknown route; guard will still register...` | Route name not found at registration time |
|
|
657
|
+
| warning | `Guard returned invalid value, treating as block` | Enter guard returned something other than `true`, `false`, a non-empty string, or a `GuardRedirect` |
|
|
658
|
+
| warning | `Leave guard returned non-boolean value, treating as block` | Leave guard returned something other than `true` or `false` |
|
|
659
|
+
| warning | `Guard redirect target "{route}" did not produce a navigation, treating as blocked` | Redirect target did not trigger a follow-up navigation (most commonly an unknown route name) |
|
|
660
|
+
| error | `Guard pipeline failed for "{hash}", blocking navigation` | Async guard pipeline rejected in `parse()` fallback path |
|
|
661
|
+
| error | `Async preflight guard failed for route "{route}", blocking navigation` | Async guard pipeline rejected in `navTo()` preflight path |
|
|
662
|
+
| error | `Enter guard [{n}] for/on route "{route}" threw, blocking navigation` | Sync or async enter guard threw an exception |
|
|
663
|
+
| error | `Leave guard [{n}] on route "{route}" threw, blocking navigation` | Sync or async leave guard threw an exception |
|
|
664
|
+
| debug | `Async guard result discarded (superseded by newer navigation)` | A newer navigation invalidated the pending async result (`parse()` path) |
|
|
665
|
+
| debug | `Async preflight result discarded (superseded by newer navigation)` | A newer navigation invalidated the pending async result (`navTo()` path) |
|
|
666
|
+
|
|
667
|
+
### Common issues
|
|
668
|
+
|
|
669
|
+
**Guards not running**: Verify the route name passed to `addRouteGuard()` matches the route name in `manifest.json`, not the pattern or target name. If the guard is on a redirect target, it does not run -- see [Redirect targets bypass guards](#redirect-targets-bypass-guards).
|
|
670
|
+
|
|
671
|
+
**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.
|
|
672
|
+
|
|
673
|
+
**Redirect treated as blocked**: The redirect did not trigger a follow-up navigation. Most often the target route name is wrong, but a same-hash no-op can look similar. The router logs the target name so you can verify the route and parameters.
|
|
674
|
+
|
|
675
|
+
**Async guard result discarded**: A newer navigation started before the async guard resolved. The router uses a generation counter to discard stale results. This is expected behavior during rapid sequential navigations. The debug log confirms when this occurs.
|
|
676
|
+
|
|
677
|
+
**URL bar shows target hash, then reverts**: This is expected for async guards. The `HashChanger` updates the URL before `parse()` runs. See [URL bar shows target hash during async guards](#url-bar-shows-target-hash-during-async-guards) for the architectural explanation and the busy-indicator pattern.
|
|
678
|
+
|
|
570
679
|
## Compatibility
|
|
571
680
|
|
|
572
681
|
> [!IMPORTANT]
|
|
573
682
|
> **Shipped UI5 baseline: 1.144.0**
|
|
574
683
|
>
|
|
575
|
-
> The published package declares `minUI5Version: 1.144.0`, and the full CI suite runs on that shipped baseline. In addition, CI runs the library QUnit suite against OpenUI5 `1.120.0` as a compatibility lane for the core router implementation. The compatibility baseline is 1.120 because `DataType.registerEnum` (used for the `NavigationOutcome` enum) requires that version.
|
|
684
|
+
> The published package declares `minUI5Version: 1.144.0`, and the full CI suite runs on that shipped baseline. In addition, CI runs the library QUnit suite against OpenUI5 `1.120.0` as a compatibility lane for the core router implementation. The compatibility baseline is 1.120 because `DataType.registerEnum` (used for the `NavigationOutcome` enum) requires that version. The shipped baseline also carries a vendored OpenUI5 router parity lane for inherited `sap.m.routing.Router` behavior when no guards are active.
|
|
576
685
|
|
|
577
686
|
If you maintain an app on an older UI5 stack and want to validate locally, run the dedicated compatibility check from the monorepo root:
|
|
578
687
|
|
|
@@ -580,6 +689,12 @@ If you maintain an app on an older UI5 stack and want to validate locally, run t
|
|
|
580
689
|
npm run test:qunit:compat:120
|
|
581
690
|
```
|
|
582
691
|
|
|
692
|
+
The vendored parity tests run as part of the main QUnit suite:
|
|
693
|
+
|
|
694
|
+
```bash
|
|
695
|
+
npm run test:qunit
|
|
696
|
+
```
|
|
697
|
+
|
|
583
698
|
## License
|
|
584
699
|
|
|
585
700
|
[MIT](LICENSE)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"buildManifest": {
|
|
18
18
|
"manifestVersion": "0.2",
|
|
19
|
-
"timestamp": "2026-03-
|
|
19
|
+
"timestamp": "2026-03-20T13:27:11.510Z",
|
|
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.
|
|
34
|
+
"version": "1.3.0",
|
|
35
35
|
"namespace": "ui5/guard/router",
|
|
36
36
|
"tags": {
|
|
37
37
|
"/resources/ui5/guard/router/library-dbg.js": {
|
package/dist/index.d.ts
CHANGED
|
@@ -12,23 +12,27 @@
|
|
|
12
12
|
// - @types/yargs@17.0.35
|
|
13
13
|
// - @types/ws@8.18.1
|
|
14
14
|
// - @types/which@2.0.2
|
|
15
|
+
// - @types/unist@2.0.11
|
|
15
16
|
// - @types/three@0.125.3
|
|
16
17
|
// - @types/stack-utils@2.0.3
|
|
17
18
|
// - @types/sizzle@2.3.10
|
|
18
19
|
// - @types/sinonjs__fake-timers@8.1.5
|
|
20
|
+
// - @types/sinon@21.0.0
|
|
19
21
|
// - @types/shimmer@1.2.0
|
|
20
22
|
// - @types/qunit@2.5.4
|
|
21
23
|
// - @types/offscreencanvas@2019.6.4
|
|
24
|
+
// - @types/npm-package-arg@6.1.4
|
|
22
25
|
// - @types/normalize-package-data@2.4.4
|
|
23
26
|
// - @types/node@25.5.0
|
|
24
27
|
// - @types/mocha@10.0.10
|
|
28
|
+
// - @types/minimist@1.2.5
|
|
25
29
|
// - @types/minimatch@3.0.5
|
|
26
30
|
// - @types/jquery@3.5.13
|
|
27
31
|
// - @types/istanbul-reports@3.0.4
|
|
28
32
|
// - @types/istanbul-lib-report@3.0.3
|
|
29
33
|
// - @types/istanbul-lib-coverage@2.0.6
|
|
30
34
|
// - @types/qunit@2.19.13
|
|
31
|
-
/// <reference path="./resources/ui5/guard/router/Router.d.ts"/>
|
|
32
|
-
/// <reference path="./resources/ui5/guard/router/library.d.ts"/>
|
|
33
35
|
/// <reference path="./resources/ui5/guard/router/NavigationOutcome.d.ts"/>
|
|
34
|
-
/// <reference path="./resources/ui5/guard/router/
|
|
36
|
+
/// <reference path="./resources/ui5/guard/router/library.d.ts"/>
|
|
37
|
+
/// <reference path="./resources/ui5/guard/router/types.d.ts"/>
|
|
38
|
+
/// <reference path="./resources/ui5/guard/router/Router.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
|
+
<version>1.3.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>
|
|
@@ -14,6 +14,8 @@ sap.ui.define([], function () {
|
|
|
14
14
|
const NavigationOutcome = Object.freeze({
|
|
15
15
|
/** Navigation was allowed and the target route activated. */
|
|
16
16
|
Committed: "committed",
|
|
17
|
+
/** Navigation was allowed, but no route matched; UI5 continued with bypassed handling. */
|
|
18
|
+
Bypassed: "bypassed",
|
|
17
19
|
/** A guard blocked navigation; the previous route remains active. */
|
|
18
20
|
Blocked: "blocked",
|
|
19
21
|
/** A guard redirected navigation to a different route. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavigationOutcome-dbg.js","names":["NavigationOutcome","Object","freeze","Committed","Blocked","Redirected","Cancelled"],"sources":["NavigationOutcome.ts"],"sourcesContent":["/**\n * Outcome of a navigation after the guard pipeline settles.\n *\n * Registered as a UI5 enum via `library.ts` so that it is discoverable\n * through `sap.ui.base.DataType.getType()`. Application code can import\n * the value object directly.\n *\n * @enum {string}\n * @namespace ui5.guard.router\n */\nconst NavigationOutcome = Object.freeze({\n\t/** Navigation was allowed and the target route activated. */\n\tCommitted: \"committed\"
|
|
1
|
+
{"version":3,"file":"NavigationOutcome-dbg.js","names":["NavigationOutcome","Object","freeze","Committed","Bypassed","Blocked","Redirected","Cancelled"],"sources":["NavigationOutcome.ts"],"sourcesContent":["/**\n * Outcome of a navigation after the guard pipeline settles.\n *\n * Registered as a UI5 enum via `library.ts` so that it is discoverable\n * through `sap.ui.base.DataType.getType()`. Application code can import\n * the value object directly.\n *\n * @enum {string}\n * @namespace ui5.guard.router\n */\nconst NavigationOutcome = Object.freeze({\n\t/** Navigation was allowed and the target route activated. */\n\tCommitted: \"committed\",\n\t/** Navigation was allowed, but no route matched; UI5 continued with bypassed handling. */\n\tBypassed: \"bypassed\",\n\t/** A guard blocked navigation; the previous route remains active. */\n\tBlocked: \"blocked\",\n\t/** A guard redirected navigation to a different route. */\n\tRedirected: \"redirected\",\n\t/** Navigation was cancelled before settling (superseded, stopped, or destroyed). */\n\tCancelled: \"cancelled\",\n});\n\ntype NavigationOutcome = (typeof NavigationOutcome)[keyof typeof NavigationOutcome];\n\nexport default NavigationOutcome;\n"],"mappings":";;;EAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;EACA,MAAMA,iBAAiB,GAAGC,MAAM,CAACC,MAAM,CAAC;IACvC;IACAC,SAAS,EAAE,WAAW;IACtB;IACAC,QAAQ,EAAE,UAAU;IACpB;IACAC,OAAO,EAAE,SAAS;IAClB;IACAC,UAAU,EAAE,YAAY;IACxB;IACAC,SAAS,EAAE;EACZ,CAAC,CAAC;EAAC,OAIYP,iBAAiB;AAAA","ignoreList":[]}
|
|
@@ -12,6 +12,8 @@ declare module "ui5/guard/router/NavigationOutcome" {
|
|
|
12
12
|
const NavigationOutcome: Readonly<{
|
|
13
13
|
/** Navigation was allowed and the target route activated. */
|
|
14
14
|
Committed: "committed";
|
|
15
|
+
/** Navigation was allowed, but no route matched; UI5 continued with bypassed handling. */
|
|
16
|
+
Bypassed: "bypassed";
|
|
15
17
|
/** A guard blocked navigation; the previous route remains active. */
|
|
16
18
|
Blocked: "blocked";
|
|
17
19
|
/** A guard redirected navigation to a different route. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavigationOutcome.d.ts","sourceRoot":"../../../../..","sources":["src/NavigationOutcome.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,oCAAoC,CAAC;IACpD;;;;;;;;;OASG;IACH,MAAM,iBAAiB;QACtB,6DAA6D;;QAE7D,qEAAqE;;QAErE,0DAA0D;;QAE1D,oFAAoF;;MAEnF,CAAC;IAEH,KAAK,iBAAiB,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,OAAO,iBAAiB,CAAC,CAAC;IAEpF,eAAe,iBAAiB,CAAC;CAEhC"}
|
|
1
|
+
{"version":3,"file":"NavigationOutcome.d.ts","sourceRoot":"../../../../..","sources":["src/NavigationOutcome.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,oCAAoC,CAAC;IACpD;;;;;;;;;OASG;IACH,MAAM,iBAAiB;QACtB,6DAA6D;;QAE7D,0FAA0F;;QAE1F,qEAAqE;;QAErE,0DAA0D;;QAE1D,oFAAoF;;MAEnF,CAAC;IAEH,KAAK,iBAAiB,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,OAAO,iBAAiB,CAAC,CAAC;IAEpF,eAAe,iBAAiB,CAAC;CAEhC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
sap.ui.define([],function(){"use strict";const e=Object.freeze({Committed:"committed",Blocked:"blocked",Redirected:"redirected",Cancelled:"cancelled"});return e});
|
|
1
|
+
sap.ui.define([],function(){"use strict";const e=Object.freeze({Committed:"committed",Bypassed:"bypassed",Blocked:"blocked",Redirected:"redirected",Cancelled:"cancelled"});return e});
|
|
2
2
|
//# sourceMappingURL=NavigationOutcome.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavigationOutcome.js","names":["NavigationOutcome","Object","freeze","Committed","Blocked","Redirected","Cancelled"],"sources":["NavigationOutcome.ts"],"sourcesContent":["/**\n * Outcome of a navigation after the guard pipeline settles.\n *\n * Registered as a UI5 enum via `library.ts` so that it is discoverable\n * through `sap.ui.base.DataType.getType()`. Application code can import\n * the value object directly.\n *\n * @enum {string}\n * @namespace ui5.guard.router\n */\nconst NavigationOutcome = Object.freeze({\n\t/** Navigation was allowed and the target route activated. */\n\tCommitted: \"committed\"
|
|
1
|
+
{"version":3,"file":"NavigationOutcome.js","names":["NavigationOutcome","Object","freeze","Committed","Bypassed","Blocked","Redirected","Cancelled"],"sources":["NavigationOutcome.ts"],"sourcesContent":["/**\n * Outcome of a navigation after the guard pipeline settles.\n *\n * Registered as a UI5 enum via `library.ts` so that it is discoverable\n * through `sap.ui.base.DataType.getType()`. Application code can import\n * the value object directly.\n *\n * @enum {string}\n * @namespace ui5.guard.router\n */\nconst NavigationOutcome = Object.freeze({\n\t/** Navigation was allowed and the target route activated. */\n\tCommitted: \"committed\",\n\t/** Navigation was allowed, but no route matched; UI5 continued with bypassed handling. */\n\tBypassed: \"bypassed\",\n\t/** A guard blocked navigation; the previous route remains active. */\n\tBlocked: \"blocked\",\n\t/** A guard redirected navigation to a different route. */\n\tRedirected: \"redirected\",\n\t/** Navigation was cancelled before settling (superseded, stopped, or destroyed). */\n\tCancelled: \"cancelled\",\n});\n\ntype NavigationOutcome = (typeof NavigationOutcome)[keyof typeof NavigationOutcome];\n\nexport default NavigationOutcome;\n"],"mappings":"yCAUA,MAAMA,EAAoBC,OAAOC,OAAO,CAEvCC,UAAW,YAEXC,SAAU,WAEVC,QAAS,UAETC,WAAY,aAEZC,UAAW,cACT,OAIYP,CAAiB","ignoreList":[]}
|