ui5-lib-guard-router 1.0.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 +382 -0
- package/dist/index.d.ts +27 -0
- package/dist/resources/ui5/guard/router/.library +17 -0
- package/dist/resources/ui5/guard/router/Router-dbg.js +431 -0
- package/dist/resources/ui5/guard/router/Router-dbg.js.map +1 -0
- package/dist/resources/ui5/guard/router/Router.d.ts +28 -0
- package/dist/resources/ui5/guard/router/Router.d.ts.map +1 -0
- package/dist/resources/ui5/guard/router/Router.js +2 -0
- package/dist/resources/ui5/guard/router/Router.js.map +1 -0
- package/dist/resources/ui5/guard/router/library-dbg.js +17 -0
- package/dist/resources/ui5/guard/router/library-dbg.js.map +1 -0
- package/dist/resources/ui5/guard/router/library-preload.js +10 -0
- package/dist/resources/ui5/guard/router/library-preload.js.map +1 -0
- package/dist/resources/ui5/guard/router/library.d.ts +7 -0
- package/dist/resources/ui5/guard/router/library.d.ts.map +1 -0
- package/dist/resources/ui5/guard/router/library.js +2 -0
- package/dist/resources/ui5/guard/router/library.js.map +1 -0
- package/dist/resources/ui5/guard/router/manifest.json +33 -0
- package/dist/resources/ui5/guard/router/types-dbg.js +2 -0
- package/dist/resources/ui5/guard/router/types-dbg.js.map +1 -0
- package/dist/resources/ui5/guard/router/types.d.ts +118 -0
- package/dist/resources/ui5/guard/router/types.d.ts.map +1 -0
- package/dist/resources/ui5/guard/router/types.js +2 -0
- package/dist/resources/ui5/guard/router/types.js.map +1 -0
- package/package.json +52 -0
- package/src/.library +17 -0
- package/src/Router.ts +540 -0
- package/src/library.ts +17 -0
- package/src/manifest.json +33 -0
- package/src/types.ts +136 -0
- package/tsconfig.json +13 -0
- package/ui5.yaml +24 -0
package/src/Router.ts
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import MobileRouter from "sap/m/routing/Router";
|
|
2
|
+
import Log from "sap/base/Log";
|
|
3
|
+
import coreLibrary from "sap/ui/core/library";
|
|
4
|
+
import type {
|
|
5
|
+
GuardFn,
|
|
6
|
+
GuardContext,
|
|
7
|
+
GuardResult,
|
|
8
|
+
GuardRedirect,
|
|
9
|
+
GuardRouter,
|
|
10
|
+
LeaveGuardFn,
|
|
11
|
+
RouteGuardConfig,
|
|
12
|
+
RouterInternal,
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
const HistoryDirection = coreLibrary.routing.HistoryDirection;
|
|
16
|
+
|
|
17
|
+
const LOG_COMPONENT = "ui5.guard.router.Router";
|
|
18
|
+
|
|
19
|
+
function isGuardRedirect(value: GuardResult): value is GuardRedirect {
|
|
20
|
+
return typeof value === "object" && value !== null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isPromise<T>(value: T | Promise<T>): value is Promise<T> {
|
|
24
|
+
return value instanceof Promise;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isRouteGuardConfig(guard: GuardFn | RouteGuardConfig): guard is RouteGuardConfig {
|
|
28
|
+
return typeof guard === "object";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function addToGuardMap<T>(map: Map<string, T[]>, key: string, guard: T): void {
|
|
32
|
+
let guards = map.get(key);
|
|
33
|
+
if (!guards) {
|
|
34
|
+
guards = [];
|
|
35
|
+
map.set(key, guards);
|
|
36
|
+
}
|
|
37
|
+
guards.push(guard);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function removeFromGuardMap<T>(map: Map<string, T[]>, key: string, guard: T): void {
|
|
41
|
+
const guards = map.get(key);
|
|
42
|
+
if (!guards) return;
|
|
43
|
+
const index = guards.indexOf(guard);
|
|
44
|
+
if (index !== -1) guards.splice(index, 1);
|
|
45
|
+
if (guards.length === 0) map.delete(key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Router with navigation guard support.
|
|
50
|
+
*
|
|
51
|
+
* Extends `sap.m.routing.Router` by overriding `parse()` to run
|
|
52
|
+
* registered guard functions before any route matching, target loading,
|
|
53
|
+
* or event firing occurs.
|
|
54
|
+
*
|
|
55
|
+
* Key assumptions (see docs/architecture.md for full rationale):
|
|
56
|
+
* - `parse()` is intentionally NOT async. Sync guards execute in the
|
|
57
|
+
* same tick; async guards fall back to a deferred path.
|
|
58
|
+
* - `replaceHash` fires `hashChanged` synchronously (validated by test).
|
|
59
|
+
* - Redirect targets bypass guards to prevent infinite loops.
|
|
60
|
+
*
|
|
61
|
+
* @extends sap.m.routing.Router
|
|
62
|
+
*/
|
|
63
|
+
const Router = MobileRouter.extend("ui5.guard.router.Router", {
|
|
64
|
+
constructor: function (this: RouterInternal, ...args: unknown[]) {
|
|
65
|
+
MobileRouter.prototype.constructor.apply(this, args);
|
|
66
|
+
this._globalGuards = [];
|
|
67
|
+
this._enterGuards = new Map<string, GuardFn[]>();
|
|
68
|
+
this._leaveGuards = new Map<string, LeaveGuardFn[]>();
|
|
69
|
+
this._currentRoute = "";
|
|
70
|
+
this._currentHash = null; // null = no parse processed yet
|
|
71
|
+
this._pendingHash = null;
|
|
72
|
+
this._redirecting = false;
|
|
73
|
+
this._parseGeneration = 0;
|
|
74
|
+
this._suppressNextParse = false;
|
|
75
|
+
this._abortController = null;
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Register a global guard that runs for every navigation.
|
|
80
|
+
*/
|
|
81
|
+
addGuard(this: RouterInternal, guard: GuardFn): GuardRouter {
|
|
82
|
+
this._globalGuards.push(guard);
|
|
83
|
+
return this;
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Remove a previously registered global guard.
|
|
88
|
+
*/
|
|
89
|
+
removeGuard(this: RouterInternal, guard: GuardFn): GuardRouter {
|
|
90
|
+
const index = this._globalGuards.indexOf(guard);
|
|
91
|
+
if (index !== -1) {
|
|
92
|
+
this._globalGuards.splice(index, 1);
|
|
93
|
+
}
|
|
94
|
+
return this;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Register a guard for a specific route.
|
|
99
|
+
*
|
|
100
|
+
* Accepts either a guard function (registered as an enter guard) or a
|
|
101
|
+
* configuration object with `beforeEnter` and/or `beforeLeave` guards.
|
|
102
|
+
*/
|
|
103
|
+
addRouteGuard(this: RouterInternal, routeName: string, guard: GuardFn | RouteGuardConfig): GuardRouter {
|
|
104
|
+
if (isRouteGuardConfig(guard)) {
|
|
105
|
+
if (!guard.beforeEnter && !guard.beforeLeave) {
|
|
106
|
+
Log.info(
|
|
107
|
+
"addRouteGuard called with config missing both beforeEnter and beforeLeave",
|
|
108
|
+
routeName,
|
|
109
|
+
LOG_COMPONENT,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (guard.beforeEnter) {
|
|
113
|
+
this.addRouteGuard(routeName, guard.beforeEnter);
|
|
114
|
+
}
|
|
115
|
+
if (guard.beforeLeave) {
|
|
116
|
+
this.addLeaveGuard(routeName, guard.beforeLeave);
|
|
117
|
+
}
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
addToGuardMap(this._enterGuards, routeName, guard);
|
|
121
|
+
return this;
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Remove a guard from a specific route.
|
|
126
|
+
*
|
|
127
|
+
* Accepts the same forms as `addRouteGuard`: a guard function removes
|
|
128
|
+
* an enter guard; a configuration object removes `beforeEnter` and/or
|
|
129
|
+
* `beforeLeave` by reference.
|
|
130
|
+
*/
|
|
131
|
+
removeRouteGuard(this: RouterInternal, routeName: string, guard: GuardFn | RouteGuardConfig): GuardRouter {
|
|
132
|
+
if (isRouteGuardConfig(guard)) {
|
|
133
|
+
if (guard.beforeEnter) {
|
|
134
|
+
this.removeRouteGuard(routeName, guard.beforeEnter);
|
|
135
|
+
}
|
|
136
|
+
if (guard.beforeLeave) {
|
|
137
|
+
this.removeLeaveGuard(routeName, guard.beforeLeave);
|
|
138
|
+
}
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
removeFromGuardMap(this._enterGuards, routeName, guard);
|
|
142
|
+
return this;
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Register a leave guard for a specific route.
|
|
147
|
+
*
|
|
148
|
+
* Leave guards run when navigating **away from** the route, before any
|
|
149
|
+
* enter guards for the target route. They answer the binary question
|
|
150
|
+
* "can I leave?" and return only a boolean (no redirects).
|
|
151
|
+
*/
|
|
152
|
+
addLeaveGuard(this: RouterInternal, routeName: string, guard: LeaveGuardFn): GuardRouter {
|
|
153
|
+
addToGuardMap(this._leaveGuards, routeName, guard);
|
|
154
|
+
return this;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Remove a leave guard from a specific route.
|
|
159
|
+
*/
|
|
160
|
+
removeLeaveGuard(this: RouterInternal, routeName: string, guard: LeaveGuardFn): GuardRouter {
|
|
161
|
+
removeFromGuardMap(this._leaveGuards, routeName, guard);
|
|
162
|
+
return this;
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Intercept hash changes and run the guard pipeline before route matching.
|
|
167
|
+
*
|
|
168
|
+
* Called by the HashChanger on every `hashChanged` event. Runs leave guards
|
|
169
|
+
* (current route), then global + route-specific enter guards (target route).
|
|
170
|
+
* Stays synchronous when all guards return plain values; falls back to async
|
|
171
|
+
* when a guard returns a Promise. A generation counter discards stale results
|
|
172
|
+
* when navigations overlap.
|
|
173
|
+
*
|
|
174
|
+
* @override sap.ui.core.routing.Router#parse
|
|
175
|
+
*/
|
|
176
|
+
parse(this: RouterInternal, newHash: string): void {
|
|
177
|
+
if (this._suppressNextParse) {
|
|
178
|
+
this._suppressNextParse = false;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (this._redirecting) {
|
|
183
|
+
this._commitNavigation(newHash);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Same-hash dedup: also invalidates any pending async guard
|
|
188
|
+
if (this._currentHash !== null && newHash === this._currentHash) {
|
|
189
|
+
this._pendingHash = null;
|
|
190
|
+
++this._parseGeneration;
|
|
191
|
+
this._abortController?.abort();
|
|
192
|
+
this._abortController = null;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Dedup against in-flight pending navigation
|
|
197
|
+
if (this._pendingHash !== null && newHash === this._pendingHash) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const routeInfo = this.getRouteInfoByHash(newHash);
|
|
202
|
+
const toRoute = routeInfo?.name ?? "";
|
|
203
|
+
|
|
204
|
+
// Invalidate any pending async guards from a previous navigation
|
|
205
|
+
this._abortController?.abort();
|
|
206
|
+
this._abortController = null;
|
|
207
|
+
const generation = ++this._parseGeneration;
|
|
208
|
+
|
|
209
|
+
this._pendingHash = newHash;
|
|
210
|
+
|
|
211
|
+
// Check if any guards apply (leave OR enter)
|
|
212
|
+
const hasLeaveGuards = this._currentRoute !== "" && this._leaveGuards.has(this._currentRoute);
|
|
213
|
+
const hasEnterGuards = this._globalGuards.length > 0 || (toRoute !== "" && this._enterGuards.has(toRoute));
|
|
214
|
+
|
|
215
|
+
// No guards → fast path
|
|
216
|
+
if (!hasLeaveGuards && !hasEnterGuards) {
|
|
217
|
+
this._commitNavigation(newHash, toRoute);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Only create a controller when guards will actually run
|
|
222
|
+
this._abortController = new AbortController();
|
|
223
|
+
|
|
224
|
+
const context: GuardContext = {
|
|
225
|
+
toRoute,
|
|
226
|
+
toHash: newHash,
|
|
227
|
+
toArguments: routeInfo?.arguments ?? {},
|
|
228
|
+
fromRoute: this._currentRoute,
|
|
229
|
+
fromHash: this._currentHash ?? "",
|
|
230
|
+
signal: this._abortController.signal,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Run enter guards and apply result (reused after leave guards pass)
|
|
234
|
+
const runEnterGuards = (): void => {
|
|
235
|
+
const enterResult = this._runEnterGuards(this._globalGuards, toRoute, context);
|
|
236
|
+
|
|
237
|
+
if (isPromise(enterResult)) {
|
|
238
|
+
enterResult
|
|
239
|
+
.then((guardResult: GuardResult) => {
|
|
240
|
+
if (generation !== this._parseGeneration) {
|
|
241
|
+
Log.debug(
|
|
242
|
+
"Async enter guard result discarded (superseded by newer navigation)",
|
|
243
|
+
newHash,
|
|
244
|
+
LOG_COMPONENT,
|
|
245
|
+
);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// Apply result: true=commit, false=block, other=redirect
|
|
249
|
+
if (guardResult === true) {
|
|
250
|
+
this._commitNavigation(newHash, toRoute);
|
|
251
|
+
} else if (guardResult === false) {
|
|
252
|
+
this._blockNavigation();
|
|
253
|
+
} else {
|
|
254
|
+
this._redirect(guardResult);
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
.catch((error: unknown) => {
|
|
258
|
+
if (generation !== this._parseGeneration) return;
|
|
259
|
+
Log.error(
|
|
260
|
+
`Async enter guard for route "${toRoute}" failed, blocking navigation`,
|
|
261
|
+
String(error),
|
|
262
|
+
LOG_COMPONENT,
|
|
263
|
+
);
|
|
264
|
+
this._blockNavigation();
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// Apply result: true=commit, false=block, other=redirect
|
|
269
|
+
if (enterResult === true) {
|
|
270
|
+
this._commitNavigation(newHash, toRoute);
|
|
271
|
+
} else if (enterResult === false) {
|
|
272
|
+
this._blockNavigation();
|
|
273
|
+
} else {
|
|
274
|
+
this._redirect(enterResult);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// Run leave guards first, then enter guards
|
|
279
|
+
if (hasLeaveGuards) {
|
|
280
|
+
const leaveResult = this._runLeaveGuards(context);
|
|
281
|
+
|
|
282
|
+
if (isPromise(leaveResult)) {
|
|
283
|
+
leaveResult
|
|
284
|
+
.then((allowed: boolean) => {
|
|
285
|
+
if (generation !== this._parseGeneration) {
|
|
286
|
+
Log.debug(
|
|
287
|
+
"Async leave guard result discarded (superseded by newer navigation)",
|
|
288
|
+
newHash,
|
|
289
|
+
LOG_COMPONENT,
|
|
290
|
+
);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (allowed !== true) {
|
|
294
|
+
this._blockNavigation();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
runEnterGuards();
|
|
298
|
+
})
|
|
299
|
+
.catch((error: unknown) => {
|
|
300
|
+
if (generation !== this._parseGeneration) return;
|
|
301
|
+
Log.error(
|
|
302
|
+
`Async leave guard on route "${this._currentRoute}" failed, blocking navigation`,
|
|
303
|
+
String(error),
|
|
304
|
+
LOG_COMPONENT,
|
|
305
|
+
);
|
|
306
|
+
this._blockNavigation();
|
|
307
|
+
});
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (leaveResult !== true) {
|
|
311
|
+
this._blockNavigation();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Enter guards (leave guards passed or were absent)
|
|
317
|
+
runEnterGuards();
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Run leave guards for the current route. Returns boolean (no redirects).
|
|
322
|
+
*
|
|
323
|
+
* The guard array is snapshot-copied before iteration so that guards
|
|
324
|
+
* may safely add/remove themselves (e.g. one-shot guards) without
|
|
325
|
+
* affecting the current pipeline run.
|
|
326
|
+
*/
|
|
327
|
+
_runLeaveGuards(this: RouterInternal, context: GuardContext): boolean | Promise<boolean> {
|
|
328
|
+
const registered = this._leaveGuards.get(this._currentRoute);
|
|
329
|
+
if (!registered || registered.length === 0) return true;
|
|
330
|
+
|
|
331
|
+
const guards = registered.slice();
|
|
332
|
+
for (let i = 0; i < guards.length; i++) {
|
|
333
|
+
try {
|
|
334
|
+
const result = guards[i](context);
|
|
335
|
+
if (isPromise(result)) {
|
|
336
|
+
return this._continueGuardsAsync(
|
|
337
|
+
result,
|
|
338
|
+
guards,
|
|
339
|
+
i,
|
|
340
|
+
context,
|
|
341
|
+
() => false,
|
|
342
|
+
"Leave guard",
|
|
343
|
+
true,
|
|
344
|
+
) as Promise<boolean>;
|
|
345
|
+
}
|
|
346
|
+
if (result !== true) return false;
|
|
347
|
+
} catch (error) {
|
|
348
|
+
Log.error(
|
|
349
|
+
`Leave guard [${i}] on route "${this._currentRoute}" threw, blocking navigation`,
|
|
350
|
+
String(error),
|
|
351
|
+
LOG_COMPONENT,
|
|
352
|
+
);
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return true;
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Delegate to the parent router and update internal state.
|
|
361
|
+
*
|
|
362
|
+
* State is updated BEFORE calling parse to ensure that if event handlers
|
|
363
|
+
* (e.g., routeMatched) trigger nested navigation, the leave guards will
|
|
364
|
+
* run for the correct (new) route rather than the old one.
|
|
365
|
+
*/
|
|
366
|
+
_commitNavigation(this: RouterInternal, hash: string, route?: string): void {
|
|
367
|
+
this._pendingHash = null;
|
|
368
|
+
this._currentHash = hash;
|
|
369
|
+
this._currentRoute = route ?? this.getRouteInfoByHash(hash)?.name ?? "";
|
|
370
|
+
MobileRouter.prototype.parse.call(this, hash);
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
/** Run global guards, then route-specific guards. Stays sync when possible. */
|
|
374
|
+
_runEnterGuards(
|
|
375
|
+
this: RouterInternal,
|
|
376
|
+
globalGuards: GuardFn[],
|
|
377
|
+
toRoute: string,
|
|
378
|
+
context: GuardContext,
|
|
379
|
+
): GuardResult | Promise<GuardResult> {
|
|
380
|
+
const globalResult = this._runGuards(globalGuards, context);
|
|
381
|
+
|
|
382
|
+
if (isPromise(globalResult)) {
|
|
383
|
+
return globalResult.then((r: GuardResult) => {
|
|
384
|
+
if (r !== true) return r;
|
|
385
|
+
if (context.signal.aborted) return false;
|
|
386
|
+
return this._runRouteGuards(toRoute, context);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (globalResult !== true) return globalResult;
|
|
390
|
+
return this._runRouteGuards(toRoute, context);
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
/** Run route-specific guards if any are registered. */
|
|
394
|
+
_runRouteGuards(this: RouterInternal, toRoute: string, context: GuardContext): GuardResult | Promise<GuardResult> {
|
|
395
|
+
if (!toRoute || !this._enterGuards.has(toRoute)) return true;
|
|
396
|
+
return this._runGuards(this._enterGuards.get(toRoute)!, context);
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Run guards sync; switch to async path if a Promise is returned.
|
|
401
|
+
*
|
|
402
|
+
* The guard array is snapshot-copied before iteration so that guards
|
|
403
|
+
* may safely add/remove themselves (e.g. one-shot guards) without
|
|
404
|
+
* affecting the current pipeline run.
|
|
405
|
+
*/
|
|
406
|
+
_runGuards(this: RouterInternal, guards: GuardFn[], context: GuardContext): GuardResult | Promise<GuardResult> {
|
|
407
|
+
guards = guards.slice();
|
|
408
|
+
for (let i = 0; i < guards.length; i++) {
|
|
409
|
+
try {
|
|
410
|
+
const result = guards[i](context);
|
|
411
|
+
if (isPromise(result)) {
|
|
412
|
+
return this._continueGuardsAsync(
|
|
413
|
+
result,
|
|
414
|
+
guards,
|
|
415
|
+
i,
|
|
416
|
+
context,
|
|
417
|
+
(r) => this._validateGuardResult(r),
|
|
418
|
+
"Enter guard",
|
|
419
|
+
false,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
if (result !== true) return this._validateGuardResult(result);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
Log.error(
|
|
425
|
+
`Enter guard [${i}] for route "${context.toRoute}" threw, blocking navigation`,
|
|
426
|
+
String(error),
|
|
427
|
+
LOG_COMPONENT,
|
|
428
|
+
);
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return true;
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Continue guard array async from the first Promise onward.
|
|
437
|
+
*
|
|
438
|
+
* Shared by both enter and leave guard pipelines. The `onBlock` callback
|
|
439
|
+
* determines what to return for non-true results: leave guards always
|
|
440
|
+
* return `false`, enter guards validate and may return redirects.
|
|
441
|
+
*
|
|
442
|
+
* @param isLeaveGuard - When true, error logs reference `fromRoute`; otherwise `toRoute`.
|
|
443
|
+
*/
|
|
444
|
+
async _continueGuardsAsync(
|
|
445
|
+
this: RouterInternal,
|
|
446
|
+
pendingResult: Promise<GuardResult>,
|
|
447
|
+
guards: Array<(context: GuardContext) => GuardResult | Promise<GuardResult>>,
|
|
448
|
+
currentIndex: number,
|
|
449
|
+
context: GuardContext,
|
|
450
|
+
onBlock: (result: GuardResult) => GuardResult,
|
|
451
|
+
label: string,
|
|
452
|
+
isLeaveGuard: boolean,
|
|
453
|
+
): Promise<GuardResult> {
|
|
454
|
+
let guardIndex = currentIndex;
|
|
455
|
+
try {
|
|
456
|
+
const result = await pendingResult;
|
|
457
|
+
if (result !== true) return onBlock(result);
|
|
458
|
+
|
|
459
|
+
for (let i = currentIndex + 1; i < guards.length; i++) {
|
|
460
|
+
if (context.signal.aborted) return false;
|
|
461
|
+
guardIndex = i;
|
|
462
|
+
const r = await guards[i](context);
|
|
463
|
+
if (r !== true) return onBlock(r);
|
|
464
|
+
}
|
|
465
|
+
return true;
|
|
466
|
+
} catch (error) {
|
|
467
|
+
if (!context.signal.aborted) {
|
|
468
|
+
const route = isLeaveGuard ? context.fromRoute : context.toRoute;
|
|
469
|
+
Log.error(
|
|
470
|
+
`${label} [${guardIndex}] on route "${route}" threw, blocking navigation`,
|
|
471
|
+
String(error),
|
|
472
|
+
LOG_COMPONENT,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
/** Validate a non-true guard result; invalid values become false. */
|
|
480
|
+
_validateGuardResult(this: RouterInternal, result: GuardResult): GuardResult {
|
|
481
|
+
if (typeof result === "string" || typeof result === "boolean" || isGuardRedirect(result)) {
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
Log.warning("Guard returned invalid value, treating as block", String(result), LOG_COMPONENT);
|
|
485
|
+
return false;
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
/** Perform a guard redirect (string route name or GuardRedirect object). */
|
|
489
|
+
_redirect(this: RouterInternal, target: string | GuardRedirect): void {
|
|
490
|
+
this._pendingHash = null;
|
|
491
|
+
this._redirecting = true;
|
|
492
|
+
try {
|
|
493
|
+
if (typeof target === "string") {
|
|
494
|
+
this.navTo(target, {}, {}, true);
|
|
495
|
+
} else {
|
|
496
|
+
this.navTo(target.route, target.parameters ?? {}, target.componentTargetInfo, true);
|
|
497
|
+
}
|
|
498
|
+
} finally {
|
|
499
|
+
this._redirecting = false;
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
/** Clear pending state and restore the previous hash. */
|
|
504
|
+
_blockNavigation(this: RouterInternal): void {
|
|
505
|
+
this._pendingHash = null;
|
|
506
|
+
this._restoreHash();
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Restore the previous hash without creating a history entry.
|
|
511
|
+
* Assumes replaceHash fires hashChanged synchronously (validated by test).
|
|
512
|
+
* Note: _currentRoute intentionally stays unchanged — the blocked navigation
|
|
513
|
+
* never committed, so the user remains on the same logical route.
|
|
514
|
+
*/
|
|
515
|
+
_restoreHash(this: RouterInternal): void {
|
|
516
|
+
const hashChanger = this.getHashChanger();
|
|
517
|
+
if (hashChanger) {
|
|
518
|
+
this._suppressNextParse = true;
|
|
519
|
+
hashChanger.replaceHash(this._currentHash ?? "", HistoryDirection.Unknown);
|
|
520
|
+
if (this._suppressNextParse) {
|
|
521
|
+
// replaceHash was a no-op (same hash) - reset to prevent leak
|
|
522
|
+
this._suppressNextParse = false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
/** Clean up guards on destroy. Bumps generation to discard pending async results. */
|
|
528
|
+
destroy(this: RouterInternal) {
|
|
529
|
+
this._globalGuards = [];
|
|
530
|
+
this._enterGuards.clear();
|
|
531
|
+
this._leaveGuards.clear();
|
|
532
|
+
++this._parseGeneration;
|
|
533
|
+
this._pendingHash = null;
|
|
534
|
+
this._abortController?.abort();
|
|
535
|
+
this._abortController = null;
|
|
536
|
+
return MobileRouter.prototype.destroy.call(this);
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
export default Router;
|
package/src/library.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Lib from "sap/ui/core/Lib";
|
|
2
|
+
import "sap/ui/core/library";
|
|
3
|
+
import "sap/m/library";
|
|
4
|
+
|
|
5
|
+
const library = Lib.init({
|
|
6
|
+
apiVersion: 2,
|
|
7
|
+
name: "ui5.guard.router",
|
|
8
|
+
version: "${version}",
|
|
9
|
+
dependencies: ["sap.ui.core", "sap.m"],
|
|
10
|
+
types: [],
|
|
11
|
+
interfaces: [],
|
|
12
|
+
controls: [],
|
|
13
|
+
elements: [],
|
|
14
|
+
noLibraryCSS: true,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export default library;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_version": "2.0.0",
|
|
3
|
+
"sap.app": {
|
|
4
|
+
"id": "ui5.guard.router",
|
|
5
|
+
"type": "library",
|
|
6
|
+
"applicationVersion": {
|
|
7
|
+
"version": "1.0.1"
|
|
8
|
+
},
|
|
9
|
+
"title": "UI5 Router extension with async navigation guards",
|
|
10
|
+
"description": "Extends sap.m.routing.Router with async navigation guards, running before route matching begins."
|
|
11
|
+
},
|
|
12
|
+
"sap.ui": {
|
|
13
|
+
"technology": "UI5",
|
|
14
|
+
"deviceTypes": {
|
|
15
|
+
"desktop": true,
|
|
16
|
+
"tablet": true,
|
|
17
|
+
"phone": true
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"sap.ui5": {
|
|
21
|
+
"contentDensities": {
|
|
22
|
+
"compact": true,
|
|
23
|
+
"cozy": true
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"minUI5Version": "1.144.0",
|
|
27
|
+
"libs": {
|
|
28
|
+
"sap.ui.core": {},
|
|
29
|
+
"sap.m": {}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|