@openwebf/react-router 0.23.7 → 0.24.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/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
@@ -420,17 +431,21 @@ interface HybridRouterChangeEvent extends SyntheticEvent {
420
431
  readonly path: string;
421
432
  }
422
433
  type HybridRouterChangeEventHandler = EventHandler<HybridRouterChangeEvent>;
434
+ interface HybridRouterPrerenderingEvent extends SyntheticEvent {
435
+ }
436
+ type HybridRouterPrerenderingEventHandler = EventHandler<HybridRouterPrerenderingEvent>;
423
437
  interface WebFHybridRouterProps {
424
438
  path: string;
425
439
  title?: string;
426
440
  theme?: 'material' | 'cupertino';
427
441
  onScreen?: HybridRouterChangeEventHandler;
428
442
  offScreen?: HybridRouterChangeEventHandler;
443
+ onPrerendering?: HybridRouterPrerenderingEventHandler;
429
444
  children?: ReactNode;
430
445
  }
431
446
  interface WebFRouterLinkElement extends WebFElementWithMethods<{}> {
432
447
  }
433
448
  declare const WebFRouterLink: FC<WebFHybridRouterProps>;
434
449
 
435
- export { Route, Routes, WebFRouter, WebFRouterLink, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
436
- export type { HybridRouteStackEntry, HybridRouterChangeEvent, HybridRouterChangeEventHandler, Location, NavigateFunction, NavigateOptions, NavigationMethods, RouteMatch, RouteObject, RouteParams, RouteProps, RoutesProps, WebFHybridRouterProps, WebFRouterLinkElement };
450
+ export { Route, Routes, WebFRouter, WebFRouterLink, __unstable_setEnsureRouteMountedCallback, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
451
+ export type { HybridRouteStackEntry, HybridRouterChangeEvent, HybridRouterChangeEventHandler, HybridRouterPrerenderingEvent, HybridRouterPrerenderingEventHandler, 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 {
@@ -262,6 +341,13 @@ const RawWebFRouterLink = createWebFComponent({
262
341
  callback(event);
263
342
  },
264
343
  },
344
+ {
345
+ propName: 'onPrerendering',
346
+ eventName: 'prerendering',
347
+ handler: (callback) => (event) => {
348
+ callback(event);
349
+ },
350
+ },
265
351
  ],
266
352
  });
267
353
  const WebFRouterLink = function (props) {
@@ -272,7 +358,12 @@ const WebFRouterLink = function (props) {
272
358
  props.onScreen(event);
273
359
  }
274
360
  };
275
- return (React.createElement(RawWebFRouterLink, { title: props.title, path: props.path, theme: props.theme, onScreen: handleOnScreen, offScreen: props.offScreen }, isRender ? props.children : null));
361
+ const handlePrerendering = (event) => {
362
+ var _a;
363
+ enableRender(true);
364
+ (_a = props.onPrerendering) === null || _a === void 0 ? void 0 : _a.call(props, event);
365
+ };
366
+ return (React.createElement(RawWebFRouterLink, { title: props.title, path: props.path, theme: props.theme, onScreen: handleOnScreen, offScreen: props.offScreen, onPrerendering: handlePrerendering }, isRender ? props.children : null));
276
367
  };
277
368
 
278
369
  /**
@@ -289,7 +380,7 @@ const WebFRouterLink = function (props) {
289
380
  *
290
381
  * Responsible for managing page rendering, lifecycle and navigation bar
291
382
  */
292
- function Route({ path, prerender = false, element, title, theme }) {
383
+ function Route({ path, mountedPath, prerender = false, element, title, theme }) {
293
384
  // Mark whether the page has been rendered
294
385
  const [hasRendered, updateRender] = useState(false);
295
386
  /**
@@ -308,7 +399,7 @@ function Route({ path, prerender = false, element, title, theme }) {
308
399
  */
309
400
  const handleOffScreen = useMemoizedFn(() => {
310
401
  });
311
- return (React.createElement(WebFRouterLink, { path: path, title: title, theme: theme, onScreen: handleOnScreen, offScreen: handleOffScreen }, shouldRenderChildren ? element : null));
402
+ return (React.createElement(WebFRouterLink, { path: mountedPath !== null && mountedPath !== void 0 ? mountedPath : path, title: title, theme: theme, onScreen: handleOnScreen, offScreen: handleOffScreen }, shouldRenderChildren ? element : null));
312
403
  }
313
404
 
314
405
  /**
@@ -316,6 +407,7 @@ function Route({ path, prerender = false, element, title, theme }) {
316
407
  */
317
408
  const RouteContext = createContext({
318
409
  path: undefined,
410
+ mountedPath: undefined,
319
411
  params: undefined,
320
412
  routeParams: undefined,
321
413
  activePath: undefined,
@@ -336,9 +428,9 @@ const RouteContext = createContext({
336
428
  */
337
429
  function useRouteContext() {
338
430
  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;
431
+ const isActive = context.activePath !== undefined
432
+ && context.mountedPath !== undefined
433
+ && context.activePath === context.mountedPath;
342
434
  return Object.assign(Object.assign({}, context), { isActive });
343
435
  }
344
436
  /**
@@ -364,12 +456,9 @@ function useLocation() {
364
456
  const context = useRouteContext();
365
457
  // Create location object from context
366
458
  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;
459
+ const pathname = context.activePath || WebFRouter.path;
371
460
  // Get state - prioritize context params, fallback to WebFRouter.state
372
- const state = (context.isActive || isCurrentRoute)
461
+ const state = context.isActive
373
462
  ? (context.params || WebFRouter.state)
374
463
  : WebFRouter.state;
375
464
  return {
@@ -378,7 +467,7 @@ function useLocation() {
378
467
  isActive: context.isActive,
379
468
  key: `${pathname}-${Date.now()}`
380
469
  };
381
- }, [context.isActive, context.path, context.activePath, context.params]);
470
+ }, [context.isActive, context.activePath, context.params]);
382
471
  return location;
383
472
  }
384
473
  /**
@@ -419,75 +508,110 @@ function useParams() {
419
508
  /**
420
509
  * Route-specific context provider that only updates when the route is active
421
510
  */
422
- function RouteContextProvider({ path, children }) {
511
+ function RouteContextProvider({ patternPath, mountedPath, children, }) {
423
512
  const globalContext = useContext(RouteContext);
424
513
  // Create a route-specific context that only updates when this route is active
425
514
  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
515
+ const isActive = globalContext.activePath !== undefined && globalContext.activePath === mountedPath;
516
+ const match = isActive ? matchPath(patternPath, mountedPath) : null;
517
+ if (isActive && match) {
432
518
  const effectiveParams = globalContext.params !== undefined ? globalContext.params : WebFRouter.state;
433
519
  return {
434
- path,
520
+ path: patternPath,
521
+ mountedPath,
435
522
  params: effectiveParams,
436
- routeParams: effectiveRouteParams,
523
+ routeParams: match.params,
437
524
  activePath: globalContext.activePath,
438
525
  routeEventKind: globalContext.routeEventKind
439
526
  };
440
527
  }
441
- // Return previous values if not active
442
528
  return {
443
- path,
529
+ path: patternPath,
530
+ mountedPath,
444
531
  params: undefined,
445
532
  routeParams: undefined,
446
533
  activePath: globalContext.activePath,
447
534
  routeEventKind: undefined
448
535
  };
449
- }, [path, globalContext.activePath, globalContext.params, globalContext.routeParams, globalContext.routeEventKind]);
536
+ }, [patternPath, mountedPath, globalContext.activePath, globalContext.params, globalContext.routeEventKind]);
450
537
  return (React.createElement(RouteContext.Provider, { value: routeSpecificContext }, children));
451
538
  }
539
+ function patternScore(pattern) {
540
+ if (pattern === '*')
541
+ return 0;
542
+ const segments = pattern.split('/').filter(Boolean);
543
+ let score = 0;
544
+ for (const segment of segments) {
545
+ if (segment === '*')
546
+ score += 1;
547
+ else if (segment.startsWith(':'))
548
+ score += 2;
549
+ else
550
+ score += 3;
551
+ }
552
+ return score * 100 + segments.length;
553
+ }
554
+ function findBestMatch(patterns, pathname) {
555
+ var _a;
556
+ let best = null;
557
+ for (const pattern of patterns) {
558
+ const match = matchPath(pattern, pathname);
559
+ if (!match)
560
+ continue;
561
+ const score = patternScore(pattern);
562
+ if (!best || score > best.score)
563
+ best = { match, score };
564
+ }
565
+ return (_a = best === null || best === void 0 ? void 0 : best.match) !== null && _a !== void 0 ? _a : null;
566
+ }
567
+ function escapeAttributeValue(value) {
568
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
569
+ }
452
570
  function Routes({ children }) {
453
571
  // State to track current route information
454
572
  const [routeState, setRouteState] = useState({
455
573
  path: undefined,
574
+ mountedPath: undefined,
456
575
  activePath: WebFRouter.path, // Initialize with current path
457
576
  params: undefined,
458
577
  routeParams: undefined,
459
578
  routeEventKind: undefined
460
579
  });
580
+ const [stack, setStack] = useState(() => WebFRouter.stack);
581
+ const [preMountedPaths, setPreMountedPaths] = useState([]);
582
+ const routePatternsRef = useRef([]);
583
+ const pendingEnsureResolversRef = useRef(new Map());
584
+ // Keep a stable view of declared route patterns for event handlers.
585
+ useEffect(() => {
586
+ const patterns = [];
587
+ Children.forEach(children, (child) => {
588
+ if (!isValidElement(child))
589
+ return;
590
+ if (child.type !== Route)
591
+ return;
592
+ patterns.push(child.props.path);
593
+ });
594
+ routePatternsRef.current = patterns;
595
+ }, [children]);
461
596
  // Listen to hybridrouterchange event
462
597
  useEffect(() => {
463
598
  const handleRouteChange = (event) => {
599
+ var _a, _b, _c;
464
600
  const routeEvent = event;
465
601
  // Check for new event detail structure with params
466
602
  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;
603
+ const newActivePath = WebFRouter.path;
604
+ const newStack = WebFRouter.stack;
605
+ setStack(newStack);
606
+ setPreMountedPaths((prev) => prev.filter((p) => newStack.some((entry) => entry.path === p)));
607
+ const bestMatch = newActivePath ? findBestMatch(routePatternsRef.current, newActivePath) : null;
608
+ const routeParams = (eventDetail === null || eventDetail === void 0 ? void 0 : eventDetail.params) || (bestMatch === null || bestMatch === void 0 ? void 0 : bestMatch.params) || undefined;
609
+ const activeEntry = [...newStack].reverse().find((entry) => entry.path === newActivePath);
610
+ 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
611
  // Update state based on event kind
489
612
  setRouteState({
490
613
  path: routeEvent.path,
614
+ mountedPath: routeEvent.path,
491
615
  activePath: newActivePath,
492
616
  params: eventState,
493
617
  routeParams: routeParams, // Use params from Flutter if available
@@ -500,31 +624,108 @@ function Routes({ children }) {
500
624
  return () => {
501
625
  document.removeEventListener('hybridrouterchange', handleRouteChange);
502
626
  };
503
- }, [routeState.activePath]);
627
+ }, []);
628
+ useEffect(() => {
629
+ __unstable_setEnsureRouteMountedCallback((pathname) => {
630
+ var _a;
631
+ if (!pathname)
632
+ return;
633
+ const bestMatch = findBestMatch(routePatternsRef.current, pathname);
634
+ if (!bestMatch)
635
+ return;
636
+ const selector = `webf-router-link[path="${escapeAttributeValue(pathname)}"]`;
637
+ if (document.querySelector(selector))
638
+ return;
639
+ let resolveFn;
640
+ const promise = new Promise((resolve) => {
641
+ resolveFn = resolve;
642
+ });
643
+ pendingEnsureResolversRef.current.set(pathname, [
644
+ ...((_a = pendingEnsureResolversRef.current.get(pathname)) !== null && _a !== void 0 ? _a : []),
645
+ resolveFn,
646
+ ]);
647
+ setPreMountedPaths((prev) => (prev.includes(pathname) ? prev : [...prev, pathname]));
648
+ return promise;
649
+ });
650
+ return () => {
651
+ __unstable_setEnsureRouteMountedCallback(null);
652
+ };
653
+ }, []);
654
+ useEffect(() => {
655
+ const pending = pendingEnsureResolversRef.current;
656
+ for (const [pathname, resolvers] of pending.entries()) {
657
+ const selector = `webf-router-link[path="${escapeAttributeValue(pathname)}"]`;
658
+ if (!document.querySelector(selector))
659
+ continue;
660
+ for (const resolve of resolvers)
661
+ resolve();
662
+ pending.delete(pathname);
663
+ }
664
+ }, [children, stack, preMountedPaths, routeState.activePath]);
504
665
  // Global context value
505
666
  const globalContextValue = useMemo(() => ({
506
667
  path: undefined,
668
+ mountedPath: undefined,
507
669
  params: routeState.params,
508
670
  routeParams: routeState.routeParams, // Pass through route params from Flutter
509
671
  activePath: routeState.activePath,
510
672
  routeEventKind: routeState.routeEventKind
511
673
  }), [routeState.activePath, routeState.params, routeState.routeParams, routeState.routeEventKind]);
512
- // Wrap each Route component with its own context provider
513
674
  const wrappedChildren = useMemo(() => {
514
- return Children.map(children, (child) => {
675
+ const declaredRoutes = [];
676
+ const patterns = [];
677
+ const declaredPaths = new Set();
678
+ Children.forEach(children, (child) => {
679
+ var _a;
515
680
  if (!isValidElement(child)) {
516
- return child;
681
+ declaredRoutes.push(child);
682
+ return;
517
683
  }
518
- // Ensure only Route components are direct children
519
684
  if (child.type !== Route) {
520
685
  console.warn('Routes component should only contain Route components as direct children');
521
- return child;
686
+ declaredRoutes.push(child);
687
+ return;
522
688
  }
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));
689
+ const patternPath = child.props.path;
690
+ patterns.push(patternPath);
691
+ declaredPaths.add(patternPath);
692
+ const mountedPath = (_a = child.props.mountedPath) !== null && _a !== void 0 ? _a : patternPath;
693
+ declaredRoutes.push(React.createElement(RouteContextProvider, { key: `declared:${patternPath}`, patternPath: patternPath, mountedPath: mountedPath }, child));
526
694
  });
527
- }, [children]);
695
+ const mountedPaths = [];
696
+ for (const entry of stack)
697
+ mountedPaths.push(entry.path);
698
+ for (const path of preMountedPaths)
699
+ mountedPaths.push(path);
700
+ if (routeState.activePath && !mountedPaths.includes(routeState.activePath))
701
+ mountedPaths.push(routeState.activePath);
702
+ const dynamicRoutes = [];
703
+ const seenMountedPaths = new Set();
704
+ for (const mountedPath of mountedPaths) {
705
+ if (seenMountedPaths.has(mountedPath))
706
+ continue;
707
+ seenMountedPaths.add(mountedPath);
708
+ if (declaredPaths.has(mountedPath))
709
+ continue;
710
+ const bestMatch = findBestMatch(patterns, mountedPath);
711
+ if (!bestMatch)
712
+ continue;
713
+ const matchingRouteElement = Children.toArray(children).find((node) => {
714
+ if (!isValidElement(node))
715
+ return false;
716
+ if (node.type !== Route)
717
+ return false;
718
+ return node.props.path === bestMatch.path;
719
+ });
720
+ if (!matchingRouteElement)
721
+ continue;
722
+ const routeInstance = React.cloneElement(matchingRouteElement, {
723
+ mountedPath,
724
+ });
725
+ dynamicRoutes.push(React.createElement(RouteContextProvider, { key: `dynamic:${mountedPath}`, patternPath: bestMatch.path, mountedPath: mountedPath }, routeInstance));
726
+ }
727
+ return [...declaredRoutes, ...dynamicRoutes];
728
+ }, [children, stack, preMountedPaths, routeState.activePath]);
528
729
  return (React.createElement(RouteContext.Provider, { value: globalContextValue }, wrappedChildren));
529
730
  }
530
731
  /**
@@ -624,5 +825,5 @@ function useNavigate() {
624
825
  }, []);
625
826
  }
626
827
 
627
- export { Route, Routes, WebFRouter, WebFRouterLink, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
828
+ export { Route, Routes, WebFRouter, WebFRouterLink, __unstable_setEnsureRouteMountedCallback, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
628
829
  //# sourceMappingURL=index.esm.js.map