@openwebf/react-router 0.23.7 → 0.24.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/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ import React, { SyntheticEvent, EventHandler, ReactNode, FC } from 'react';
2
2
  import { WebFElementWithMethods } from '@openwebf/react-core-ui';
3
3
 
4
4
  type RoutePath = string;
5
+ type EnsureRouteMountedCallback = (pathname: string) => Promise<void> | void;
6
+ declare function __unstable_setEnsureRouteMountedCallback(callback: EnsureRouteMountedCallback | null): void;
5
7
  /**
6
8
  * Single entry in the hybrid router stack.
7
9
  * Mirrors the data returned from webf.hybridHistory.buildContextStack.
@@ -27,7 +29,7 @@ declare const WebFRouter: {
27
29
  /**
28
30
  * Get the current route path
29
31
  */
30
- readonly path: RoutePath;
32
+ readonly path: string;
31
33
  /**
32
34
  * Navigate to a specified route
33
35
  * Applies route guards for permission checks before navigation
@@ -153,6 +155,11 @@ interface RouteProps {
153
155
  * Must be a member of the RoutePath enum
154
156
  */
155
157
  path: string;
158
+ /**
159
+ * The concrete path to mount for this route instance.
160
+ * Used internally to support dynamic routes like `/users/:id` mounting at `/users/123`.
161
+ */
162
+ mountedPath?: string;
156
163
  /**
157
164
  * Whether to pre-render
158
165
  * If true, the page will be rendered when the app starts, rather than waiting for route navigation
@@ -179,7 +186,7 @@ interface RouteProps {
179
186
  *
180
187
  * Responsible for managing page rendering, lifecycle and navigation bar
181
188
  */
182
- declare function Route({ path, prerender, element, title, theme }: RouteProps): React.JSX.Element;
189
+ declare function Route({ path, mountedPath, prerender, element, title, theme }: RouteProps): React.JSX.Element;
183
190
 
184
191
  /**
185
192
  * Hook to get route context
@@ -201,6 +208,10 @@ declare function useRouteContext(): {
201
208
  * Current route path, corresponds to RoutePath enum
202
209
  */
203
210
  path: string | undefined;
211
+ /**
212
+ * The concrete mounted path for this route instance (e.g. `/users/123`).
213
+ */
214
+ mountedPath: string | undefined;
204
215
  /**
205
216
  * Page state
206
217
  * State data passed during route navigation
@@ -432,5 +443,5 @@ interface WebFRouterLinkElement extends WebFElementWithMethods<{}> {
432
443
  }
433
444
  declare const WebFRouterLink: FC<WebFHybridRouterProps>;
434
445
 
435
- export { Route, Routes, WebFRouter, WebFRouterLink, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
446
+ export { Route, Routes, WebFRouter, WebFRouterLink, __unstable_setEnsureRouteMountedCallback, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
436
447
  export type { HybridRouteStackEntry, HybridRouterChangeEvent, HybridRouterChangeEventHandler, Location, NavigateFunction, NavigateOptions, NavigationMethods, RouteMatch, RouteObject, RouteParams, RouteProps, RoutesProps, WebFHybridRouterProps, WebFRouterLinkElement };
package/dist/index.esm.js CHANGED
@@ -1,4 +1,3 @@
1
- import { webf } from '@openwebf/webf-enterprise-typings';
2
1
  import React, { useRef, useMemo, useState, createContext, useContext, useEffect, Children, isValidElement } from 'react';
3
2
  import { createWebFComponent } from '@openwebf/react-core-ui';
4
3
 
@@ -34,6 +33,26 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
34
33
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
35
34
  };
36
35
 
36
+ /**
37
+ * Router management module
38
+ *
39
+ * Encapsulates routing navigation functionality with route guard mechanism for permission checks
40
+ */
41
+ function getHybridHistory() {
42
+ var _a;
43
+ return (_a = globalThis === null || globalThis === void 0 ? void 0 : globalThis.webf) === null || _a === void 0 ? void 0 : _a.hybridHistory;
44
+ }
45
+ let ensureRouteMountedCallback = null;
46
+ function __unstable_setEnsureRouteMountedCallback(callback) {
47
+ ensureRouteMountedCallback = callback;
48
+ }
49
+ function ensureRouteMounted(pathname) {
50
+ return __awaiter(this, void 0, void 0, function* () {
51
+ if (!ensureRouteMountedCallback)
52
+ return;
53
+ yield ensureRouteMountedCallback(pathname);
54
+ });
55
+ }
37
56
  /**
38
57
  * WebF Router object - provides comprehensive navigation APIs
39
58
  * Combines web-like history management with Flutter-like navigation patterns
@@ -43,110 +62,161 @@ const WebFRouter = {
43
62
  * Get the current state object associated with the history entry
44
63
  */
45
64
  get state() {
46
- return webf.hybridHistory.state;
65
+ var _a;
66
+ return (_a = getHybridHistory()) === null || _a === void 0 ? void 0 : _a.state;
47
67
  },
48
68
  /**
49
69
  * Get the full hybrid router build context stack.
50
70
  * The stack is ordered from root route (index 0) to the current top route (last element).
51
71
  */
52
72
  get stack() {
53
- return webf.hybridHistory.buildContextStack;
73
+ var _a, _b;
74
+ return (_b = (_a = getHybridHistory()) === null || _a === void 0 ? void 0 : _a.buildContextStack) !== null && _b !== void 0 ? _b : [];
54
75
  },
55
76
  /**
56
77
  * Get the current route path
57
78
  */
58
79
  get path() {
59
- return webf.hybridHistory.path;
80
+ var _a, _b;
81
+ return (_b = (_a = getHybridHistory()) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : '/';
60
82
  },
61
83
  /**
62
84
  * Navigate to a specified route
63
85
  * Applies route guards for permission checks before navigation
64
86
  */
65
87
  push: (path, state) => __awaiter(void 0, void 0, void 0, function* () {
66
- webf.hybridHistory.pushNamed(path, { arguments: state });
88
+ const hybridHistory = getHybridHistory();
89
+ if (!hybridHistory)
90
+ throw new Error('WebF hybridHistory is not available');
91
+ yield ensureRouteMounted(path);
92
+ hybridHistory.pushNamed(path, { arguments: state });
67
93
  }),
68
94
  /**
69
95
  * Replace the current route without adding to history
70
96
  * Applies route guards for permission checks before navigation
71
97
  */
72
98
  replace: (path, state) => __awaiter(void 0, void 0, void 0, function* () {
73
- webf.hybridHistory.pushReplacementNamed(path, { arguments: state });
99
+ const hybridHistory = getHybridHistory();
100
+ if (!hybridHistory)
101
+ throw new Error('WebF hybridHistory is not available');
102
+ yield ensureRouteMounted(path);
103
+ hybridHistory.pushReplacementNamed(path, { arguments: state });
74
104
  }),
75
105
  /**
76
106
  * Navigate back to the previous route
77
107
  */
78
108
  back: () => {
79
- webf.hybridHistory.back();
109
+ const hybridHistory = getHybridHistory();
110
+ if (!hybridHistory)
111
+ throw new Error('WebF hybridHistory is not available');
112
+ hybridHistory.back();
80
113
  },
81
114
  /**
82
115
  * Close the current screen and return to the previous one
83
116
  * Flutter-style navigation method
84
117
  */
85
118
  pop: (result) => {
86
- webf.hybridHistory.pop(result);
119
+ const hybridHistory = getHybridHistory();
120
+ if (!hybridHistory)
121
+ throw new Error('WebF hybridHistory is not available');
122
+ hybridHistory.pop(result);
87
123
  },
88
124
  /**
89
125
  * Pop routes until reaching a specific route
90
126
  */
91
127
  popUntil: (path) => {
92
- webf.hybridHistory.popUntil(path);
128
+ const hybridHistory = getHybridHistory();
129
+ if (!hybridHistory)
130
+ throw new Error('WebF hybridHistory is not available');
131
+ hybridHistory.popUntil(path);
93
132
  },
94
133
  /**
95
134
  * Pop the current route and push a new named route
96
135
  */
97
136
  popAndPushNamed: (path, state) => __awaiter(void 0, void 0, void 0, function* () {
98
- webf.hybridHistory.popAndPushNamed(path, { arguments: state });
137
+ const hybridHistory = getHybridHistory();
138
+ if (!hybridHistory)
139
+ throw new Error('WebF hybridHistory is not available');
140
+ yield ensureRouteMounted(path);
141
+ hybridHistory.popAndPushNamed(path, { arguments: state });
99
142
  }),
100
143
  /**
101
144
  * Push a new route and remove routes until reaching a specific route
102
145
  */
103
146
  pushNamedAndRemoveUntil: (path, state, untilPath) => __awaiter(void 0, void 0, void 0, function* () {
104
- webf.hybridHistory.pushNamedAndRemoveUntil(state, path, untilPath);
147
+ const hybridHistory = getHybridHistory();
148
+ if (!hybridHistory)
149
+ throw new Error('WebF hybridHistory is not available');
150
+ yield ensureRouteMounted(path);
151
+ hybridHistory.pushNamedAndRemoveUntil(state, path, untilPath);
105
152
  }),
106
153
  /**
107
154
  * Push a new route and remove all routes until a specific route (Flutter-style)
108
155
  */
109
156
  pushNamedAndRemoveUntilRoute: (newPath, untilPath, state) => __awaiter(void 0, void 0, void 0, function* () {
110
- webf.hybridHistory.pushNamedAndRemoveUntilRoute(newPath, untilPath, { arguments: state });
157
+ const hybridHistory = getHybridHistory();
158
+ if (!hybridHistory)
159
+ throw new Error('WebF hybridHistory is not available');
160
+ yield ensureRouteMounted(newPath);
161
+ hybridHistory.pushNamedAndRemoveUntilRoute(newPath, untilPath, { arguments: state });
111
162
  }),
112
163
  /**
113
164
  * Check if the navigator can go back
114
165
  */
115
166
  canPop: () => {
116
- return webf.hybridHistory.canPop();
167
+ const hybridHistory = getHybridHistory();
168
+ if (!hybridHistory)
169
+ return false;
170
+ return hybridHistory.canPop();
117
171
  },
118
172
  /**
119
173
  * Pop the current route if possible
120
174
  * Returns true if the pop was successful, false otherwise
121
175
  */
122
176
  maybePop: (result) => {
123
- return webf.hybridHistory.maybePop(result);
177
+ const hybridHistory = getHybridHistory();
178
+ if (!hybridHistory)
179
+ return false;
180
+ return hybridHistory.maybePop(result);
124
181
  },
125
182
  /**
126
183
  * Push a new state to the history stack (web-style navigation)
127
184
  */
128
185
  pushState: (state, name) => {
129
- webf.hybridHistory.pushState(state, name);
186
+ const hybridHistory = getHybridHistory();
187
+ if (!hybridHistory)
188
+ throw new Error('WebF hybridHistory is not available');
189
+ hybridHistory.pushState(state, name);
130
190
  },
131
191
  /**
132
192
  * Replace the current history entry with a new one (web-style navigation)
133
193
  */
134
194
  replaceState: (state, name) => {
135
- webf.hybridHistory.replaceState(state, name);
195
+ const hybridHistory = getHybridHistory();
196
+ if (!hybridHistory)
197
+ throw new Error('WebF hybridHistory is not available');
198
+ hybridHistory.replaceState(state, name);
136
199
  },
137
200
  /**
138
201
  * Pop and push with restoration capability
139
202
  * Returns a restoration ID string
140
203
  */
141
204
  restorablePopAndPushState: (state, name) => {
142
- return webf.hybridHistory.restorablePopAndPushState(state, name);
205
+ const hybridHistory = getHybridHistory();
206
+ if (!hybridHistory)
207
+ throw new Error('WebF hybridHistory is not available');
208
+ return hybridHistory.restorablePopAndPushState(state, name);
143
209
  },
144
210
  /**
145
211
  * Pop and push named route with restoration capability
146
212
  * Returns a restoration ID string
147
213
  */
148
214
  restorablePopAndPushNamed: (path, state) => __awaiter(void 0, void 0, void 0, function* () {
149
- return webf.hybridHistory.restorablePopAndPushNamed(path, { arguments: state });
215
+ const hybridHistory = getHybridHistory();
216
+ if (!hybridHistory)
217
+ throw new Error('WebF hybridHistory is not available');
218
+ yield ensureRouteMounted(path);
219
+ return hybridHistory.restorablePopAndPushNamed(path, { arguments: state });
150
220
  })
151
221
  };
152
222
  /**
@@ -156,6 +226,10 @@ const WebFRouter = {
156
226
  */
157
227
  function pathToRegex(pattern) {
158
228
  const paramNames = [];
229
+ if (pattern === '*') {
230
+ paramNames.push('*');
231
+ return { regex: /^(.*)$/, paramNames };
232
+ }
159
233
  // Escape special regex characters except : and *
160
234
  let regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
161
235
  // Replace :param with named capture groups
@@ -163,6 +237,11 @@ function pathToRegex(pattern) {
163
237
  paramNames.push(paramName);
164
238
  return '([^/]+)';
165
239
  });
240
+ // Replace * with a splat capture group (matches across segments)
241
+ regexPattern = regexPattern.replace(/\*/g, () => {
242
+ paramNames.push('*');
243
+ return '(.*)';
244
+ });
166
245
  // Add anchors for exact matching
167
246
  regexPattern = `^${regexPattern}$`;
168
247
  return {
@@ -289,7 +368,7 @@ const WebFRouterLink = function (props) {
289
368
  *
290
369
  * Responsible for managing page rendering, lifecycle and navigation bar
291
370
  */
292
- function Route({ path, prerender = false, element, title, theme }) {
371
+ function Route({ path, mountedPath, prerender = false, element, title, theme }) {
293
372
  // Mark whether the page has been rendered
294
373
  const [hasRendered, updateRender] = useState(false);
295
374
  /**
@@ -308,7 +387,7 @@ function Route({ path, prerender = false, element, title, theme }) {
308
387
  */
309
388
  const handleOffScreen = useMemoizedFn(() => {
310
389
  });
311
- return (React.createElement(WebFRouterLink, { path: path, title: title, theme: theme, onScreen: handleOnScreen, offScreen: handleOffScreen }, shouldRenderChildren ? element : null));
390
+ return (React.createElement(WebFRouterLink, { path: mountedPath !== null && mountedPath !== void 0 ? mountedPath : path, title: title, theme: theme, onScreen: handleOnScreen, offScreen: handleOffScreen }, shouldRenderChildren ? element : null));
312
391
  }
313
392
 
314
393
  /**
@@ -316,6 +395,7 @@ function Route({ path, prerender = false, element, title, theme }) {
316
395
  */
317
396
  const RouteContext = createContext({
318
397
  path: undefined,
398
+ mountedPath: undefined,
319
399
  params: undefined,
320
400
  routeParams: undefined,
321
401
  activePath: undefined,
@@ -336,9 +416,9 @@ const RouteContext = createContext({
336
416
  */
337
417
  function useRouteContext() {
338
418
  const context = useContext(RouteContext);
339
- // isActive is true only for push events with matching path
340
- const isActive = (context.routeEventKind === 'didPush' || context.routeEventKind === 'didPushNext')
341
- && context.path === context.activePath;
419
+ const isActive = context.activePath !== undefined
420
+ && context.mountedPath !== undefined
421
+ && context.activePath === context.mountedPath;
342
422
  return Object.assign(Object.assign({}, context), { isActive });
343
423
  }
344
424
  /**
@@ -364,12 +444,9 @@ function useLocation() {
364
444
  const context = useRouteContext();
365
445
  // Create location object from context
366
446
  const location = useMemo(() => {
367
- const currentPath = context.path || context.activePath || WebFRouter.path;
368
- let pathname = currentPath;
369
- // Check if the current component's route matches the active path
370
- const isCurrentRoute = context.path === context.activePath;
447
+ const pathname = context.activePath || WebFRouter.path;
371
448
  // Get state - prioritize context params, fallback to WebFRouter.state
372
- const state = (context.isActive || isCurrentRoute)
449
+ const state = context.isActive
373
450
  ? (context.params || WebFRouter.state)
374
451
  : WebFRouter.state;
375
452
  return {
@@ -378,7 +455,7 @@ function useLocation() {
378
455
  isActive: context.isActive,
379
456
  key: `${pathname}-${Date.now()}`
380
457
  };
381
- }, [context.isActive, context.path, context.activePath, context.params]);
458
+ }, [context.isActive, context.activePath, context.params]);
382
459
  return location;
383
460
  }
384
461
  /**
@@ -419,75 +496,110 @@ function useParams() {
419
496
  /**
420
497
  * Route-specific context provider that only updates when the route is active
421
498
  */
422
- function RouteContextProvider({ path, children }) {
499
+ function RouteContextProvider({ patternPath, mountedPath, children, }) {
423
500
  const globalContext = useContext(RouteContext);
424
501
  // Create a route-specific context that only updates when this route is active
425
502
  const routeSpecificContext = useMemo(() => {
426
- // Check if this route pattern matches the active path
427
- const match = globalContext.activePath ? matchPath(path, globalContext.activePath) : null;
428
- if (match) {
429
- // Use route params from Flutter event if available, otherwise from local matching
430
- const effectiveRouteParams = globalContext.routeParams || match.params;
431
- // For matching routes, always try to get state from WebFRouter if params is undefined
503
+ const isActive = globalContext.activePath !== undefined && globalContext.activePath === mountedPath;
504
+ const match = isActive ? matchPath(patternPath, mountedPath) : null;
505
+ if (isActive && match) {
432
506
  const effectiveParams = globalContext.params !== undefined ? globalContext.params : WebFRouter.state;
433
507
  return {
434
- path,
508
+ path: patternPath,
509
+ mountedPath,
435
510
  params: effectiveParams,
436
- routeParams: effectiveRouteParams,
511
+ routeParams: match.params,
437
512
  activePath: globalContext.activePath,
438
513
  routeEventKind: globalContext.routeEventKind
439
514
  };
440
515
  }
441
- // Return previous values if not active
442
516
  return {
443
- path,
517
+ path: patternPath,
518
+ mountedPath,
444
519
  params: undefined,
445
520
  routeParams: undefined,
446
521
  activePath: globalContext.activePath,
447
522
  routeEventKind: undefined
448
523
  };
449
- }, [path, globalContext.activePath, globalContext.params, globalContext.routeParams, globalContext.routeEventKind]);
524
+ }, [patternPath, mountedPath, globalContext.activePath, globalContext.params, globalContext.routeEventKind]);
450
525
  return (React.createElement(RouteContext.Provider, { value: routeSpecificContext }, children));
451
526
  }
527
+ function patternScore(pattern) {
528
+ if (pattern === '*')
529
+ return 0;
530
+ const segments = pattern.split('/').filter(Boolean);
531
+ let score = 0;
532
+ for (const segment of segments) {
533
+ if (segment === '*')
534
+ score += 1;
535
+ else if (segment.startsWith(':'))
536
+ score += 2;
537
+ else
538
+ score += 3;
539
+ }
540
+ return score * 100 + segments.length;
541
+ }
542
+ function findBestMatch(patterns, pathname) {
543
+ var _a;
544
+ let best = null;
545
+ for (const pattern of patterns) {
546
+ const match = matchPath(pattern, pathname);
547
+ if (!match)
548
+ continue;
549
+ const score = patternScore(pattern);
550
+ if (!best || score > best.score)
551
+ best = { match, score };
552
+ }
553
+ return (_a = best === null || best === void 0 ? void 0 : best.match) !== null && _a !== void 0 ? _a : null;
554
+ }
555
+ function escapeAttributeValue(value) {
556
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
557
+ }
452
558
  function Routes({ children }) {
453
559
  // State to track current route information
454
560
  const [routeState, setRouteState] = useState({
455
561
  path: undefined,
562
+ mountedPath: undefined,
456
563
  activePath: WebFRouter.path, // Initialize with current path
457
564
  params: undefined,
458
565
  routeParams: undefined,
459
566
  routeEventKind: undefined
460
567
  });
568
+ const [stack, setStack] = useState(() => WebFRouter.stack);
569
+ const [preMountedPaths, setPreMountedPaths] = useState([]);
570
+ const routePatternsRef = useRef([]);
571
+ const pendingEnsureResolversRef = useRef(new Map());
572
+ // Keep a stable view of declared route patterns for event handlers.
573
+ useEffect(() => {
574
+ const patterns = [];
575
+ Children.forEach(children, (child) => {
576
+ if (!isValidElement(child))
577
+ return;
578
+ if (child.type !== Route)
579
+ return;
580
+ patterns.push(child.props.path);
581
+ });
582
+ routePatternsRef.current = patterns;
583
+ }, [children]);
461
584
  // Listen to hybridrouterchange event
462
585
  useEffect(() => {
463
586
  const handleRouteChange = (event) => {
587
+ var _a, _b, _c;
464
588
  const routeEvent = event;
465
589
  // Check for new event detail structure with params
466
590
  const eventDetail = event.detail;
467
- // Only update activePath for push events
468
- const newActivePath = (routeEvent.kind === 'didPushNext' || routeEvent.kind === 'didPush')
469
- ? routeEvent.path
470
- : routeState.activePath;
471
- // For dynamic routes, extract parameters from the path using registered route patterns
472
- let routeParams = (eventDetail === null || eventDetail === void 0 ? void 0 : eventDetail.params) || undefined;
473
- if (!routeParams && newActivePath) {
474
- // Try to extract parameters from registered route patterns
475
- const registeredRoutes = Array.from(document.querySelectorAll('webf-router-link'));
476
- for (const routeElement of registeredRoutes) {
477
- const routePath = routeElement.getAttribute('path');
478
- if (routePath && routePath.includes(':')) {
479
- const match = matchPath(routePath, newActivePath);
480
- if (match) {
481
- routeParams = match.params;
482
- break;
483
- }
484
- }
485
- }
486
- }
487
- const eventState = (eventDetail === null || eventDetail === void 0 ? void 0 : eventDetail.state) || routeEvent.state;
591
+ const newActivePath = WebFRouter.path;
592
+ const newStack = WebFRouter.stack;
593
+ setStack(newStack);
594
+ setPreMountedPaths((prev) => prev.filter((p) => newStack.some((entry) => entry.path === p)));
595
+ const bestMatch = newActivePath ? findBestMatch(routePatternsRef.current, newActivePath) : null;
596
+ const routeParams = (eventDetail === null || eventDetail === void 0 ? void 0 : eventDetail.params) || (bestMatch === null || bestMatch === void 0 ? void 0 : bestMatch.params) || undefined;
597
+ const activeEntry = [...newStack].reverse().find((entry) => entry.path === newActivePath);
598
+ const eventState = (_c = (_b = (_a = activeEntry === null || activeEntry === void 0 ? void 0 : activeEntry.state) !== null && _a !== void 0 ? _a : eventDetail === null || eventDetail === void 0 ? void 0 : eventDetail.state) !== null && _b !== void 0 ? _b : routeEvent.state) !== null && _c !== void 0 ? _c : WebFRouter.state;
488
599
  // Update state based on event kind
489
600
  setRouteState({
490
601
  path: routeEvent.path,
602
+ mountedPath: routeEvent.path,
491
603
  activePath: newActivePath,
492
604
  params: eventState,
493
605
  routeParams: routeParams, // Use params from Flutter if available
@@ -500,31 +612,108 @@ function Routes({ children }) {
500
612
  return () => {
501
613
  document.removeEventListener('hybridrouterchange', handleRouteChange);
502
614
  };
503
- }, [routeState.activePath]);
615
+ }, []);
616
+ useEffect(() => {
617
+ __unstable_setEnsureRouteMountedCallback((pathname) => {
618
+ var _a;
619
+ if (!pathname)
620
+ return;
621
+ const bestMatch = findBestMatch(routePatternsRef.current, pathname);
622
+ if (!bestMatch)
623
+ return;
624
+ const selector = `webf-router-link[path="${escapeAttributeValue(pathname)}"]`;
625
+ if (document.querySelector(selector))
626
+ return;
627
+ let resolveFn;
628
+ const promise = new Promise((resolve) => {
629
+ resolveFn = resolve;
630
+ });
631
+ pendingEnsureResolversRef.current.set(pathname, [
632
+ ...((_a = pendingEnsureResolversRef.current.get(pathname)) !== null && _a !== void 0 ? _a : []),
633
+ resolveFn,
634
+ ]);
635
+ setPreMountedPaths((prev) => (prev.includes(pathname) ? prev : [...prev, pathname]));
636
+ return promise;
637
+ });
638
+ return () => {
639
+ __unstable_setEnsureRouteMountedCallback(null);
640
+ };
641
+ }, []);
642
+ useEffect(() => {
643
+ const pending = pendingEnsureResolversRef.current;
644
+ for (const [pathname, resolvers] of pending.entries()) {
645
+ const selector = `webf-router-link[path="${escapeAttributeValue(pathname)}"]`;
646
+ if (!document.querySelector(selector))
647
+ continue;
648
+ for (const resolve of resolvers)
649
+ resolve();
650
+ pending.delete(pathname);
651
+ }
652
+ }, [children, stack, preMountedPaths, routeState.activePath]);
504
653
  // Global context value
505
654
  const globalContextValue = useMemo(() => ({
506
655
  path: undefined,
656
+ mountedPath: undefined,
507
657
  params: routeState.params,
508
658
  routeParams: routeState.routeParams, // Pass through route params from Flutter
509
659
  activePath: routeState.activePath,
510
660
  routeEventKind: routeState.routeEventKind
511
661
  }), [routeState.activePath, routeState.params, routeState.routeParams, routeState.routeEventKind]);
512
- // Wrap each Route component with its own context provider
513
662
  const wrappedChildren = useMemo(() => {
514
- return Children.map(children, (child) => {
663
+ const declaredRoutes = [];
664
+ const patterns = [];
665
+ const declaredPaths = new Set();
666
+ Children.forEach(children, (child) => {
667
+ var _a;
515
668
  if (!isValidElement(child)) {
516
- return child;
669
+ declaredRoutes.push(child);
670
+ return;
517
671
  }
518
- // Ensure only Route components are direct children
519
672
  if (child.type !== Route) {
520
673
  console.warn('Routes component should only contain Route components as direct children');
521
- return child;
674
+ declaredRoutes.push(child);
675
+ return;
522
676
  }
523
- // Wrap each Route with its own context provider
524
- const routePath = child.props.path;
525
- return (React.createElement(RouteContextProvider, { key: routePath, path: routePath }, child));
677
+ const patternPath = child.props.path;
678
+ patterns.push(patternPath);
679
+ declaredPaths.add(patternPath);
680
+ const mountedPath = (_a = child.props.mountedPath) !== null && _a !== void 0 ? _a : patternPath;
681
+ declaredRoutes.push(React.createElement(RouteContextProvider, { key: `declared:${patternPath}`, patternPath: patternPath, mountedPath: mountedPath }, child));
526
682
  });
527
- }, [children]);
683
+ const mountedPaths = [];
684
+ for (const entry of stack)
685
+ mountedPaths.push(entry.path);
686
+ for (const path of preMountedPaths)
687
+ mountedPaths.push(path);
688
+ if (routeState.activePath && !mountedPaths.includes(routeState.activePath))
689
+ mountedPaths.push(routeState.activePath);
690
+ const dynamicRoutes = [];
691
+ const seenMountedPaths = new Set();
692
+ for (const mountedPath of mountedPaths) {
693
+ if (seenMountedPaths.has(mountedPath))
694
+ continue;
695
+ seenMountedPaths.add(mountedPath);
696
+ if (declaredPaths.has(mountedPath))
697
+ continue;
698
+ const bestMatch = findBestMatch(patterns, mountedPath);
699
+ if (!bestMatch)
700
+ continue;
701
+ const matchingRouteElement = Children.toArray(children).find((node) => {
702
+ if (!isValidElement(node))
703
+ return false;
704
+ if (node.type !== Route)
705
+ return false;
706
+ return node.props.path === bestMatch.path;
707
+ });
708
+ if (!matchingRouteElement)
709
+ continue;
710
+ const routeInstance = React.cloneElement(matchingRouteElement, {
711
+ mountedPath,
712
+ });
713
+ dynamicRoutes.push(React.createElement(RouteContextProvider, { key: `dynamic:${mountedPath}`, patternPath: bestMatch.path, mountedPath: mountedPath }, routeInstance));
714
+ }
715
+ return [...declaredRoutes, ...dynamicRoutes];
716
+ }, [children, stack, preMountedPaths, routeState.activePath]);
528
717
  return (React.createElement(RouteContext.Provider, { value: globalContextValue }, wrappedChildren));
529
718
  }
530
719
  /**
@@ -624,5 +813,5 @@ function useNavigate() {
624
813
  }, []);
625
814
  }
626
815
 
627
- export { Route, Routes, WebFRouter, WebFRouterLink, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
816
+ export { Route, Routes, WebFRouter, WebFRouterLink, __unstable_setEnsureRouteMountedCallback, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
628
817
  //# sourceMappingURL=index.esm.js.map