@pithyjs/router 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +621 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/router/core/config.d.ts +22 -0
- package/dist/router/core/config.d.ts.map +1 -0
- package/dist/router/core/config.js +54 -0
- package/dist/router/core/config.js.map +1 -0
- package/dist/router/core/constants.d.ts +89 -0
- package/dist/router/core/constants.d.ts.map +1 -0
- package/dist/router/core/constants.js +95 -0
- package/dist/router/core/constants.js.map +1 -0
- package/dist/router/core/history.d.ts +54 -0
- package/dist/router/core/history.d.ts.map +1 -0
- package/dist/router/core/history.js +89 -0
- package/dist/router/core/history.js.map +1 -0
- package/dist/router/core/matcher.d.ts +43 -0
- package/dist/router/core/matcher.d.ts.map +1 -0
- package/dist/router/core/matcher.js +242 -0
- package/dist/router/core/matcher.js.map +1 -0
- package/dist/router/core/params.d.ts +88 -0
- package/dist/router/core/params.d.ts.map +1 -0
- package/dist/router/core/params.js +150 -0
- package/dist/router/core/params.js.map +1 -0
- package/dist/router/core/pipeline.d.ts +90 -0
- package/dist/router/core/pipeline.d.ts.map +1 -0
- package/dist/router/core/pipeline.js +323 -0
- package/dist/router/core/pipeline.js.map +1 -0
- package/dist/router/core/route-tree.d.ts +76 -0
- package/dist/router/core/route-tree.d.ts.map +1 -0
- package/dist/router/core/route-tree.js +143 -0
- package/dist/router/core/route-tree.js.map +1 -0
- package/dist/router/core/router.d.ts +229 -0
- package/dist/router/core/router.d.ts.map +1 -0
- package/dist/router/core/router.js +762 -0
- package/dist/router/core/router.js.map +1 -0
- package/dist/router/core/types.d.ts +387 -0
- package/dist/router/core/types.d.ts.map +1 -0
- package/dist/router/core/types.js +22 -0
- package/dist/router/core/types.js.map +1 -0
- package/dist/router/core/utils.d.ts +66 -0
- package/dist/router/core/utils.d.ts.map +1 -0
- package/dist/router/core/utils.js +152 -0
- package/dist/router/core/utils.js.map +1 -0
- package/dist/router/features/coordinator.d.ts +75 -0
- package/dist/router/features/coordinator.d.ts.map +1 -0
- package/dist/router/features/coordinator.js +203 -0
- package/dist/router/features/coordinator.js.map +1 -0
- package/dist/router/features/errors/error-codes.d.ts +107 -0
- package/dist/router/features/errors/error-codes.d.ts.map +1 -0
- package/dist/router/features/errors/error-codes.js +247 -0
- package/dist/router/features/errors/error-codes.js.map +1 -0
- package/dist/router/features/errors/error-resolution.d.ts +31 -0
- package/dist/router/features/errors/error-resolution.d.ts.map +1 -0
- package/dist/router/features/errors/error-resolution.js +47 -0
- package/dist/router/features/errors/error-resolution.js.map +1 -0
- package/dist/router/features/errors/error-types.d.ts +60 -0
- package/dist/router/features/errors/error-types.d.ts.map +1 -0
- package/dist/router/features/errors/error-types.js +57 -0
- package/dist/router/features/errors/error-types.js.map +1 -0
- package/dist/router/features/errors/global-error-fallback.d.ts +33 -0
- package/dist/router/features/errors/global-error-fallback.d.ts.map +1 -0
- package/dist/router/features/errors/global-error-fallback.js +182 -0
- package/dist/router/features/errors/global-error-fallback.js.map +1 -0
- package/dist/router/features/errors/index.d.ts +15 -0
- package/dist/router/features/errors/index.d.ts.map +1 -0
- package/dist/router/features/errors/index.js +18 -0
- package/dist/router/features/errors/index.js.map +1 -0
- package/dist/router/features/errors/router-errors.d.ts +44 -0
- package/dist/router/features/errors/router-errors.d.ts.map +1 -0
- package/dist/router/features/errors/router-errors.js +53 -0
- package/dist/router/features/errors/router-errors.js.map +1 -0
- package/dist/router/features/guards/engine.d.ts +40 -0
- package/dist/router/features/guards/engine.d.ts.map +1 -0
- package/dist/router/features/guards/engine.js +67 -0
- package/dist/router/features/guards/engine.js.map +1 -0
- package/dist/router/features/guards/index.d.ts +52 -0
- package/dist/router/features/guards/index.d.ts.map +1 -0
- package/dist/router/features/guards/index.js +95 -0
- package/dist/router/features/guards/index.js.map +1 -0
- package/dist/router/features/loaders/cache.d.ts +60 -0
- package/dist/router/features/loaders/cache.d.ts.map +1 -0
- package/dist/router/features/loaders/cache.js +80 -0
- package/dist/router/features/loaders/cache.js.map +1 -0
- package/dist/router/features/loaders/engine.d.ts +17 -0
- package/dist/router/features/loaders/engine.d.ts.map +1 -0
- package/dist/router/features/loaders/engine.js +27 -0
- package/dist/router/features/loaders/engine.js.map +1 -0
- package/dist/router/features/loaders/index.d.ts +49 -0
- package/dist/router/features/loaders/index.d.ts.map +1 -0
- package/dist/router/features/loaders/index.js +112 -0
- package/dist/router/features/loaders/index.js.map +1 -0
- package/dist/router/features/loading-ui-manager.d.ts +69 -0
- package/dist/router/features/loading-ui-manager.d.ts.map +1 -0
- package/dist/router/features/loading-ui-manager.js +209 -0
- package/dist/router/features/loading-ui-manager.js.map +1 -0
- package/dist/router/features/module-loader.d.ts +50 -0
- package/dist/router/features/module-loader.d.ts.map +1 -0
- package/dist/router/features/module-loader.js +121 -0
- package/dist/router/features/module-loader.js.map +1 -0
- package/dist/router/features/outlet/errors.d.ts +24 -0
- package/dist/router/features/outlet/errors.d.ts.map +1 -0
- package/dist/router/features/outlet/errors.js +96 -0
- package/dist/router/features/outlet/errors.js.map +1 -0
- package/dist/router/features/outlet/index.d.ts +45 -0
- package/dist/router/features/outlet/index.d.ts.map +1 -0
- package/dist/router/features/outlet/index.js +95 -0
- package/dist/router/features/outlet/index.js.map +1 -0
- package/dist/router/features/outlet/mount.d.ts +38 -0
- package/dist/router/features/outlet/mount.d.ts.map +1 -0
- package/dist/router/features/outlet/mount.js +301 -0
- package/dist/router/features/outlet/mount.js.map +1 -0
- package/dist/router/features/outlet/segment-chain.d.ts +16 -0
- package/dist/router/features/outlet/segment-chain.d.ts.map +1 -0
- package/dist/router/features/outlet/segment-chain.js +62 -0
- package/dist/router/features/outlet/segment-chain.js.map +1 -0
- package/dist/router/features/prefetch/index.d.ts +97 -0
- package/dist/router/features/prefetch/index.d.ts.map +1 -0
- package/dist/router/features/prefetch/index.js +345 -0
- package/dist/router/features/prefetch/index.js.map +1 -0
- package/dist/router/features/ux/focus.d.ts +62 -0
- package/dist/router/features/ux/focus.d.ts.map +1 -0
- package/dist/router/features/ux/focus.js +136 -0
- package/dist/router/features/ux/focus.js.map +1 -0
- package/dist/router/features/ux/live-region.d.ts +81 -0
- package/dist/router/features/ux/live-region.d.ts.map +1 -0
- package/dist/router/features/ux/live-region.js +166 -0
- package/dist/router/features/ux/live-region.js.map +1 -0
- package/dist/router/features/ux/scroll-focus.d.ts +105 -0
- package/dist/router/features/ux/scroll-focus.d.ts.map +1 -0
- package/dist/router/features/ux/scroll-focus.js +111 -0
- package/dist/router/features/ux/scroll-focus.js.map +1 -0
- package/dist/router/features/ux/scroll.d.ts +71 -0
- package/dist/router/features/ux/scroll.d.ts.map +1 -0
- package/dist/router/features/ux/scroll.js +137 -0
- package/dist/router/features/ux/scroll.js.map +1 -0
- package/dist/router/features/ux/transitions.d.ts +120 -0
- package/dist/router/features/ux/transitions.d.ts.map +1 -0
- package/dist/router/features/ux/transitions.js +235 -0
- package/dist/router/features/ux/transitions.js.map +1 -0
- package/dist/router/integration/directive-binder.d.ts +20 -0
- package/dist/router/integration/directive-binder.d.ts.map +1 -0
- package/dist/router/integration/directive-binder.js +139 -0
- package/dist/router/integration/directive-binder.js.map +1 -0
- package/dist/router/integration/directives.d.ts +42 -0
- package/dist/router/integration/directives.d.ts.map +1 -0
- package/dist/router/integration/directives.js +109 -0
- package/dist/router/integration/directives.js.map +1 -0
- package/dist/router/integration/history-adapter.d.ts +76 -0
- package/dist/router/integration/history-adapter.d.ts.map +1 -0
- package/dist/router/integration/history-adapter.js +157 -0
- package/dist/router/integration/history-adapter.js.map +1 -0
- package/dist/router/integration/hmr.d.ts +83 -0
- package/dist/router/integration/hmr.d.ts.map +1 -0
- package/dist/router/integration/hmr.js +160 -0
- package/dist/router/integration/hmr.js.map +1 -0
- package/dist/router/integration/link-interceptor.d.ts +49 -0
- package/dist/router/integration/link-interceptor.d.ts.map +1 -0
- package/dist/router/integration/link-interceptor.js +94 -0
- package/dist/router/integration/link-interceptor.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** @codex
|
|
2
|
+
{
|
|
3
|
+
"id": "pithy.router.directives",
|
|
4
|
+
"title": "Router Directives (@routerOutlet, @routerLink)",
|
|
5
|
+
"category": "directive"
|
|
6
|
+
}
|
|
7
|
+
*/
|
|
8
|
+
import { getPrefetchManager, } from '../features/prefetch';
|
|
9
|
+
import { isNavigationClick, shouldInterceptLink } from '../core/utils';
|
|
10
|
+
/** @codexApi {"parent":"pithy.router.directives","name":"routerOutlet","stability":"experimental","signature":"(element: HTMLElement, router: Router) => void"} */
|
|
11
|
+
/**
|
|
12
|
+
* Router outlet directive - marks where child routes should be rendered
|
|
13
|
+
*
|
|
14
|
+
* NOTE: This directive only marks the outlet element. The actual route component
|
|
15
|
+
* rendering is handled by OutletManager.mountSegments() when router.mount() is called.
|
|
16
|
+
*/
|
|
17
|
+
export function routerOutlet(element, _router) {
|
|
18
|
+
// Mark the element as a router outlet
|
|
19
|
+
element.setAttribute('data-router-outlet', 'true');
|
|
20
|
+
// Store cleanup function (currently no cleanup needed, but kept for future features)
|
|
21
|
+
element.__routerOutletCleanup = () => {
|
|
22
|
+
// Placeholder for future cleanup if needed
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** @codexApi {"parent":"pithy.router.directives","name":"routerLink","stability":"stable","signature":"(element: HTMLAnchorElement, router: Router, options?: RouterLinkOptions) => void"} */
|
|
26
|
+
/**
|
|
27
|
+
* Router link directive - enhances anchor tags with client-side navigation
|
|
28
|
+
* Fixed to prevent URL changes when guards deny navigation
|
|
29
|
+
*/
|
|
30
|
+
export function routerLink(element, router, options = {}) {
|
|
31
|
+
const { activeClass = 'router-link-active', exactMatch = false, prefetch = 'none', replace = false, } = options;
|
|
32
|
+
// Enhanced click handler that prevents URL changes when guards deny
|
|
33
|
+
const onClick = (event) => {
|
|
34
|
+
// Skip if event already handled
|
|
35
|
+
if (event.defaultPrevented)
|
|
36
|
+
return;
|
|
37
|
+
// Only handle navigation clicks (left click without modifiers)
|
|
38
|
+
if (!isNavigationClick(event))
|
|
39
|
+
return;
|
|
40
|
+
// Check if link should be intercepted
|
|
41
|
+
const href = shouldInterceptLink(element);
|
|
42
|
+
if (!href)
|
|
43
|
+
return;
|
|
44
|
+
// CRITICAL: Prevent browser from changing URL
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
event.stopPropagation();
|
|
47
|
+
// Delegate to router - only router should change URL, and only after guards pass
|
|
48
|
+
router.navigate(href, { replace }).catch(error => {
|
|
49
|
+
console.warn('[Router] Navigation failed:', error);
|
|
50
|
+
// Don't fallback to window.location - that would bypass guards
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
element.addEventListener('click', onClick);
|
|
54
|
+
// Subscribe to router state for active class management
|
|
55
|
+
const unsubscribe = router.state().subscribe((state) => {
|
|
56
|
+
const href = element.getAttribute('href');
|
|
57
|
+
if (!href)
|
|
58
|
+
return;
|
|
59
|
+
const isActive = exactMatch
|
|
60
|
+
? state.path === href
|
|
61
|
+
: state.path.startsWith(href) &&
|
|
62
|
+
(href === '/' ? state.path === '/' : true);
|
|
63
|
+
if (isActive) {
|
|
64
|
+
element.classList.add(activeClass);
|
|
65
|
+
element.setAttribute('aria-current', 'page');
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
element.classList.remove(activeClass);
|
|
69
|
+
element.removeAttribute('aria-current');
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// Integrate with PrefetchManager for intelligent prefetching
|
|
73
|
+
const prefetchManager = getPrefetchManager();
|
|
74
|
+
if (prefetch !== 'none') {
|
|
75
|
+
prefetchManager.registerLink(element, prefetch);
|
|
76
|
+
}
|
|
77
|
+
// Store cleanup functions for later use
|
|
78
|
+
element.__routerLinkCleanup = () => {
|
|
79
|
+
element.removeEventListener('click', onClick);
|
|
80
|
+
unsubscribe();
|
|
81
|
+
// Clean up prefetch registration
|
|
82
|
+
if (prefetch !== 'none') {
|
|
83
|
+
prefetchManager.unregisterLink(element);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** @codexApi {"parent":"pithy.router.directives","name":"cleanupRouterDirectives","stability":"experimental","signature":"(element: HTMLElement) => void"} */
|
|
88
|
+
/**
|
|
89
|
+
* Cleanup router directive subscriptions
|
|
90
|
+
*/
|
|
91
|
+
export function cleanupRouterDirectives(element) {
|
|
92
|
+
const target = element;
|
|
93
|
+
// Clean up router outlet
|
|
94
|
+
const outletCleanup = target.__routerOutletCleanup;
|
|
95
|
+
if (outletCleanup) {
|
|
96
|
+
outletCleanup();
|
|
97
|
+
delete target.__routerOutletCleanup;
|
|
98
|
+
}
|
|
99
|
+
// Clean up router links
|
|
100
|
+
const linkCleanup = target.__routerLinkCleanup;
|
|
101
|
+
if (linkCleanup) {
|
|
102
|
+
linkCleanup();
|
|
103
|
+
delete target.__routerLinkCleanup;
|
|
104
|
+
}
|
|
105
|
+
// Recursively clean up child elements
|
|
106
|
+
const children = element.querySelectorAll('[data-router-outlet], a[href]');
|
|
107
|
+
children.forEach(child => cleanupRouterDirectives(child));
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=directives.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directives.js","sourceRoot":"","sources":["../../../src/router/integration/directives.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** @codex
|
|
2
|
+
{
|
|
3
|
+
"id": "pithy.router.integration.history-adapter",
|
|
4
|
+
"title": "Browser History Adapter",
|
|
5
|
+
"category": "runtime"
|
|
6
|
+
}
|
|
7
|
+
*/
|
|
8
|
+
import type { HistoryAdapter, ScrollPositionData } from '../core/types';
|
|
9
|
+
/** @codexApi {"parent":"pithy.router.integration.history-adapter","name":"BrowserHistoryAdapter","stability":"stable","signature":"class BrowserHistoryAdapter implements HistoryAdapter","description":"Default history adapter using the window.history API"} */
|
|
10
|
+
/**
|
|
11
|
+
* Browser history adapter - default implementation using window.history API
|
|
12
|
+
* Provides clean abstraction over browser history for testing and alternative implementations
|
|
13
|
+
*/
|
|
14
|
+
export declare class BrowserHistoryAdapter implements HistoryAdapter {
|
|
15
|
+
/**
|
|
16
|
+
* Push a new URL to browser history
|
|
17
|
+
*/
|
|
18
|
+
push(url: string, state?: unknown): void;
|
|
19
|
+
/**
|
|
20
|
+
* Replace current history entry
|
|
21
|
+
*/
|
|
22
|
+
replace(url: string, state?: unknown): void;
|
|
23
|
+
/**
|
|
24
|
+
* Subscribe to popstate events (back/forward navigation)
|
|
25
|
+
* Returns unsubscribe function
|
|
26
|
+
*/
|
|
27
|
+
onPop(callback: (url: string, state?: unknown) => void): () => void;
|
|
28
|
+
/**
|
|
29
|
+
* Get current location from browser
|
|
30
|
+
*/
|
|
31
|
+
getLocation(): {
|
|
32
|
+
path: string;
|
|
33
|
+
hash: string;
|
|
34
|
+
search: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Save scroll position to current history state
|
|
38
|
+
* This captures window scroll and scrollable containers
|
|
39
|
+
*/
|
|
40
|
+
saveScrollPosition(data: ScrollPositionData): void;
|
|
41
|
+
/**
|
|
42
|
+
* Go back in browser history
|
|
43
|
+
*/
|
|
44
|
+
back(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Go forward in browser history
|
|
47
|
+
*/
|
|
48
|
+
forward(): void;
|
|
49
|
+
}
|
|
50
|
+
/** @codexApi {"parent":"pithy.router.integration.history-adapter","name":"MemoryHistoryAdapter","stability":"stable","signature":"class MemoryHistoryAdapter implements HistoryAdapter","description":"In-memory history adapter for testing without DOM"} */
|
|
51
|
+
/**
|
|
52
|
+
* Memory history adapter for testing (no DOM dependencies)
|
|
53
|
+
*/
|
|
54
|
+
export declare class MemoryHistoryAdapter implements HistoryAdapter {
|
|
55
|
+
private stack;
|
|
56
|
+
private currentIndex;
|
|
57
|
+
private listeners;
|
|
58
|
+
constructor(initialUrl?: string);
|
|
59
|
+
push(url: string, state?: unknown): void;
|
|
60
|
+
replace(url: string, state?: unknown): void;
|
|
61
|
+
onPop(callback: (url: string, state?: unknown) => void): () => void;
|
|
62
|
+
getLocation(): {
|
|
63
|
+
path: string;
|
|
64
|
+
hash: string;
|
|
65
|
+
search: string;
|
|
66
|
+
};
|
|
67
|
+
saveScrollPosition(data: ScrollPositionData): void;
|
|
68
|
+
back(): void;
|
|
69
|
+
forward(): void;
|
|
70
|
+
getStack(): Array<{
|
|
71
|
+
url: string;
|
|
72
|
+
state?: unknown;
|
|
73
|
+
}>;
|
|
74
|
+
getCurrentIndex(): number;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=history-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history-adapter.d.ts","sourceRoot":"","sources":["../../../src/router/integration/history-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;EAME;AAEF,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExE,mQAAmQ;AACnQ;;;GAGG;AACH,qBAAa,qBAAsB,YAAW,cAAc;IAC1D;;OAEG;IACI,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI;IAI/C;;OAEG;IACI,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI;IAIlD;;;OAGG;IACI,KAAK,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI;IAiB1E;;OAEG;IACI,WAAW,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IAQpE;;;OAGG;IACI,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI;IAWzD;;OAEG;IACI,IAAI,IAAI,IAAI;IAInB;;OAEG;IACI,OAAO,IAAI,IAAI;CAGvB;AAED,8PAA8P;AAC9P;;GAEG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IACzD,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,YAAY,CAAM;IAC1B,OAAO,CAAC,SAAS,CAAqD;gBAE1D,UAAU,SAAM;IAKrB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI;IAOxC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI;IAI3C,KAAK,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI;IAUnE,WAAW,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IAc7D,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI;IAelD,IAAI,IAAI,IAAI;IAUZ,OAAO,IAAI,IAAI;IAUf,QAAQ,IAAI,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAInD,eAAe,IAAI,MAAM;CAGjC"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/** @codex
|
|
2
|
+
{
|
|
3
|
+
"id": "pithy.router.integration.history-adapter",
|
|
4
|
+
"title": "Browser History Adapter",
|
|
5
|
+
"category": "runtime"
|
|
6
|
+
}
|
|
7
|
+
*/
|
|
8
|
+
/** @codexApi {"parent":"pithy.router.integration.history-adapter","name":"BrowserHistoryAdapter","stability":"stable","signature":"class BrowserHistoryAdapter implements HistoryAdapter","description":"Default history adapter using the window.history API"} */
|
|
9
|
+
/**
|
|
10
|
+
* Browser history adapter - default implementation using window.history API
|
|
11
|
+
* Provides clean abstraction over browser history for testing and alternative implementations
|
|
12
|
+
*/
|
|
13
|
+
export class BrowserHistoryAdapter {
|
|
14
|
+
/**
|
|
15
|
+
* Push a new URL to browser history
|
|
16
|
+
*/
|
|
17
|
+
push(url, state) {
|
|
18
|
+
window.history.pushState(state, '', url);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Replace current history entry
|
|
22
|
+
*/
|
|
23
|
+
replace(url, state) {
|
|
24
|
+
window.history.replaceState(state, '', url);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Subscribe to popstate events (back/forward navigation)
|
|
28
|
+
* Returns unsubscribe function
|
|
29
|
+
*/
|
|
30
|
+
onPop(callback) {
|
|
31
|
+
const handler = (event) => {
|
|
32
|
+
const url = window.location.pathname +
|
|
33
|
+
window.location.search +
|
|
34
|
+
window.location.hash;
|
|
35
|
+
callback(url, event.state);
|
|
36
|
+
};
|
|
37
|
+
window.addEventListener('popstate', handler);
|
|
38
|
+
// Return unsubscribe function
|
|
39
|
+
return () => {
|
|
40
|
+
window.removeEventListener('popstate', handler);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get current location from browser
|
|
45
|
+
*/
|
|
46
|
+
getLocation() {
|
|
47
|
+
return {
|
|
48
|
+
path: window.location.pathname,
|
|
49
|
+
hash: window.location.hash,
|
|
50
|
+
search: window.location.search,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Save scroll position to current history state
|
|
55
|
+
* This captures window scroll and scrollable containers
|
|
56
|
+
*/
|
|
57
|
+
saveScrollPosition(data) {
|
|
58
|
+
const currentState = window.history.state ?? {};
|
|
59
|
+
window.history.replaceState({
|
|
60
|
+
...currentState,
|
|
61
|
+
__scroll: data,
|
|
62
|
+
}, '');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Go back in browser history
|
|
66
|
+
*/
|
|
67
|
+
back() {
|
|
68
|
+
window.history.back();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Go forward in browser history
|
|
72
|
+
*/
|
|
73
|
+
forward() {
|
|
74
|
+
window.history.forward();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** @codexApi {"parent":"pithy.router.integration.history-adapter","name":"MemoryHistoryAdapter","stability":"stable","signature":"class MemoryHistoryAdapter implements HistoryAdapter","description":"In-memory history adapter for testing without DOM"} */
|
|
78
|
+
/**
|
|
79
|
+
* Memory history adapter for testing (no DOM dependencies)
|
|
80
|
+
*/
|
|
81
|
+
export class MemoryHistoryAdapter {
|
|
82
|
+
constructor(initialUrl = '/') {
|
|
83
|
+
this.stack = [];
|
|
84
|
+
this.currentIndex = -1;
|
|
85
|
+
this.listeners = [];
|
|
86
|
+
this.stack.push({ url: initialUrl });
|
|
87
|
+
this.currentIndex = 0;
|
|
88
|
+
}
|
|
89
|
+
push(url, state) {
|
|
90
|
+
// Remove any forward history
|
|
91
|
+
this.stack = this.stack.slice(0, this.currentIndex + 1);
|
|
92
|
+
this.stack.push({ url, state });
|
|
93
|
+
this.currentIndex++;
|
|
94
|
+
}
|
|
95
|
+
replace(url, state) {
|
|
96
|
+
this.stack[this.currentIndex] = { url, state };
|
|
97
|
+
}
|
|
98
|
+
onPop(callback) {
|
|
99
|
+
this.listeners.push(callback);
|
|
100
|
+
return () => {
|
|
101
|
+
const index = this.listeners.indexOf(callback);
|
|
102
|
+
if (index > -1) {
|
|
103
|
+
this.listeners.splice(index, 1);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
getLocation() {
|
|
108
|
+
const current = this.stack[this.currentIndex];
|
|
109
|
+
if (!current) {
|
|
110
|
+
return { path: '/', hash: '', search: '' };
|
|
111
|
+
}
|
|
112
|
+
const url = new URL(current.url, 'http://localhost');
|
|
113
|
+
return {
|
|
114
|
+
path: url.pathname,
|
|
115
|
+
hash: url.hash,
|
|
116
|
+
search: url.search,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
saveScrollPosition(data) {
|
|
120
|
+
const current = this.stack[this.currentIndex];
|
|
121
|
+
if (current) {
|
|
122
|
+
const prevState = typeof current.state === 'object' && current.state !== null
|
|
123
|
+
? current.state
|
|
124
|
+
: {};
|
|
125
|
+
current.state = {
|
|
126
|
+
...prevState,
|
|
127
|
+
__scroll: data,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Test utilities
|
|
132
|
+
back() {
|
|
133
|
+
if (this.currentIndex > 0) {
|
|
134
|
+
this.currentIndex--;
|
|
135
|
+
const current = this.stack[this.currentIndex];
|
|
136
|
+
if (current) {
|
|
137
|
+
this.listeners.forEach(cb => cb(current.url, current.state));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
forward() {
|
|
142
|
+
if (this.currentIndex < this.stack.length - 1) {
|
|
143
|
+
this.currentIndex++;
|
|
144
|
+
const current = this.stack[this.currentIndex];
|
|
145
|
+
if (current) {
|
|
146
|
+
this.listeners.forEach(cb => cb(current.url, current.state));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
getStack() {
|
|
151
|
+
return [...this.stack];
|
|
152
|
+
}
|
|
153
|
+
getCurrentIndex() {
|
|
154
|
+
return this.currentIndex;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=history-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history-adapter.js","sourceRoot":"","sources":["../../../src/router/integration/history-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** @codex
|
|
2
|
+
{
|
|
3
|
+
"id": "pithy.router.hmr",
|
|
4
|
+
"title": "Hot Module Replacement Isolation Layer",
|
|
5
|
+
"category": "runtime"
|
|
6
|
+
}
|
|
7
|
+
*/
|
|
8
|
+
import type { RouteNode, RouteMatch, NavigationOptions } from '../core/types';
|
|
9
|
+
/**
|
|
10
|
+
* HMR update callback function type
|
|
11
|
+
* Receives new routes and should re-mount with same router state
|
|
12
|
+
*/
|
|
13
|
+
export type HMRUpdateCallback = (newRoutes: RouteNode[]) => Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Scroll position snapshot for HMR restoration
|
|
16
|
+
*/
|
|
17
|
+
export interface ScrollSnapshot {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
path: string;
|
|
21
|
+
}
|
|
22
|
+
/** @codexApi {"parent":"pithy.router.hmr","name":"HMRManager","stability":"stable","signature":"class HMRManager"} */
|
|
23
|
+
/**
|
|
24
|
+
* Thin HMR isolation layer
|
|
25
|
+
*
|
|
26
|
+
* This module provides a minimal integration point between Vite's HMR system
|
|
27
|
+
* and the router. It subscribes to route module updates and triggers router
|
|
28
|
+
* re-mounting while preserving state (path, params, scroll position).
|
|
29
|
+
*
|
|
30
|
+
* **Design Principles:**
|
|
31
|
+
* - Thin layer: no business logic, just subscription and delegation
|
|
32
|
+
* - Preserves router state during updates
|
|
33
|
+
* - No full router re-creation, only outlet remounting
|
|
34
|
+
* - Debounces rapid updates to prevent loops
|
|
35
|
+
*/
|
|
36
|
+
export declare class HMRManager {
|
|
37
|
+
private updateCallback;
|
|
38
|
+
private pendingUpdate;
|
|
39
|
+
private currentScrollSnapshot;
|
|
40
|
+
/** @codexApi {"parent":"pithy.router.hmr","name":"enable","stability":"stable","signature":"(callback: HMRUpdateCallback) => () => void"} */
|
|
41
|
+
/**
|
|
42
|
+
* Enable HMR and subscribe to route module updates
|
|
43
|
+
*
|
|
44
|
+
* @param callback - Function to call when routes are updated (should re-mount outlets)
|
|
45
|
+
* @returns Cleanup function to unsubscribe
|
|
46
|
+
*/
|
|
47
|
+
enable(callback: HMRUpdateCallback): () => void;
|
|
48
|
+
/** @codexApi {"parent":"pithy.router.hmr","name":"captureScrollSnapshot","stability":"stable","signature":"() => ScrollSnapshot | null"} */
|
|
49
|
+
/**
|
|
50
|
+
* Capture current scroll position before HMR update
|
|
51
|
+
* This allows scroll restoration after route remounting
|
|
52
|
+
*/
|
|
53
|
+
captureScrollSnapshot(): ScrollSnapshot | null;
|
|
54
|
+
/** @codexApi {"parent":"pithy.router.hmr","name":"consumeScrollSnapshot","stability":"stable","signature":"() => ScrollSnapshot | null"} */
|
|
55
|
+
/**
|
|
56
|
+
* Get and clear the captured scroll snapshot
|
|
57
|
+
* Should be called after successful remount to restore scroll
|
|
58
|
+
*/
|
|
59
|
+
consumeScrollSnapshot(): ScrollSnapshot | null;
|
|
60
|
+
/** @codexApi {"parent":"pithy.router.hmr","name":"handleRouteUpdate","stability":"stable","signature":"async handleRouteUpdate(newRoutes: RouteNode[], currentMatch: RouteMatch | null, navigate: (path: string, options?: NavigationOptions) => Promise<boolean>, clearLoadingUI: () => void): Promise<string[]>","description":"Handle route updates from HMR with state preservation"} */
|
|
61
|
+
/**
|
|
62
|
+
* Handle route updates from HMR
|
|
63
|
+
* Updates route tree and navigates to refresh components
|
|
64
|
+
*
|
|
65
|
+
* @param newRoutes - New route tree from HMR
|
|
66
|
+
* @param currentMatch - Current route match before update
|
|
67
|
+
* @param navigate - Navigation function to trigger remount
|
|
68
|
+
* @param clearLoadingUI - Function to clear loading UI
|
|
69
|
+
* @returns Updated route patterns array
|
|
70
|
+
*/
|
|
71
|
+
handleRouteUpdate(newRoutes: RouteNode[], currentMatch: RouteMatch | null, navigate: (path: string, options?: NavigationOptions) => Promise<boolean>, clearLoadingUI: () => void): Promise<string[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Setup Vite HMR subscription
|
|
74
|
+
* Listens for virtual route module updates and triggers callback
|
|
75
|
+
*/
|
|
76
|
+
private setupViteHMR;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Global HMR manager instance
|
|
80
|
+
* Singleton to ensure single subscription point
|
|
81
|
+
*/
|
|
82
|
+
export declare const hmrManager: HMRManager;
|
|
83
|
+
//# sourceMappingURL=hmr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hmr.d.ts","sourceRoot":"","sources":["../../../src/router/integration/hmr.ts"],"names":[],"mappings":"AAAA;;;;;;EAME;AAEF,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAG9E;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1E;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,EAAE,MAAM,CAAC;CACd;AAED,sHAAsH;AACtH;;;;;;;;;;;;GAYG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,cAAc,CAAkC;IACxD,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,qBAAqB,CAA+B;IAE5D,6IAA6I;IAC7I;;;;;OAKG;IACH,MAAM,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,IAAI;IAqB/C,4IAA4I;IAC5I;;;OAGG;IACH,qBAAqB,IAAI,cAAc,GAAG,IAAI;IAe9C,4IAA4I;IAC5I;;;OAGG;IACH,qBAAqB,IAAI,cAAc,GAAG,IAAI;IAM9C,6XAA6X;IAC7X;;;;;;;;;OASG;IACG,iBAAiB,CACrB,SAAS,EAAE,SAAS,EAAE,EACtB,YAAY,EAAE,UAAU,GAAG,IAAI,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,EACzE,cAAc,EAAE,MAAM,IAAI,GACzB,OAAO,CAAC,MAAM,EAAE,CAAC;IA2CpB;;;OAGG;IACH,OAAO,CAAC,YAAY;CAuCrB;AAED;;;GAGG;AACH,eAAO,MAAM,UAAU,YAAmB,CAAC"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/** @codex
|
|
2
|
+
{
|
|
3
|
+
"id": "pithy.router.hmr",
|
|
4
|
+
"title": "Hot Module Replacement Isolation Layer",
|
|
5
|
+
"category": "runtime"
|
|
6
|
+
}
|
|
7
|
+
*/
|
|
8
|
+
import { flattenRouteTree } from '../core/route-tree';
|
|
9
|
+
/** @codexApi {"parent":"pithy.router.hmr","name":"HMRManager","stability":"stable","signature":"class HMRManager"} */
|
|
10
|
+
/**
|
|
11
|
+
* Thin HMR isolation layer
|
|
12
|
+
*
|
|
13
|
+
* This module provides a minimal integration point between Vite's HMR system
|
|
14
|
+
* and the router. It subscribes to route module updates and triggers router
|
|
15
|
+
* re-mounting while preserving state (path, params, scroll position).
|
|
16
|
+
*
|
|
17
|
+
* **Design Principles:**
|
|
18
|
+
* - Thin layer: no business logic, just subscription and delegation
|
|
19
|
+
* - Preserves router state during updates
|
|
20
|
+
* - No full router re-creation, only outlet remounting
|
|
21
|
+
* - Debounces rapid updates to prevent loops
|
|
22
|
+
*/
|
|
23
|
+
export class HMRManager {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.updateCallback = null;
|
|
26
|
+
this.currentScrollSnapshot = null;
|
|
27
|
+
}
|
|
28
|
+
/** @codexApi {"parent":"pithy.router.hmr","name":"enable","stability":"stable","signature":"(callback: HMRUpdateCallback) => () => void"} */
|
|
29
|
+
/**
|
|
30
|
+
* Enable HMR and subscribe to route module updates
|
|
31
|
+
*
|
|
32
|
+
* @param callback - Function to call when routes are updated (should re-mount outlets)
|
|
33
|
+
* @returns Cleanup function to unsubscribe
|
|
34
|
+
*/
|
|
35
|
+
enable(callback) {
|
|
36
|
+
this.updateCallback = callback;
|
|
37
|
+
// Only setup Vite HMR in browser with Vite
|
|
38
|
+
if (typeof window !== 'undefined' &&
|
|
39
|
+
import.meta.hot) {
|
|
40
|
+
this.setupViteHMR();
|
|
41
|
+
}
|
|
42
|
+
// Return cleanup function
|
|
43
|
+
return () => {
|
|
44
|
+
this.updateCallback = null;
|
|
45
|
+
if (this.pendingUpdate) {
|
|
46
|
+
cancelAnimationFrame(this.pendingUpdate);
|
|
47
|
+
this.pendingUpdate = undefined;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** @codexApi {"parent":"pithy.router.hmr","name":"captureScrollSnapshot","stability":"stable","signature":"() => ScrollSnapshot | null"} */
|
|
52
|
+
/**
|
|
53
|
+
* Capture current scroll position before HMR update
|
|
54
|
+
* This allows scroll restoration after route remounting
|
|
55
|
+
*/
|
|
56
|
+
captureScrollSnapshot() {
|
|
57
|
+
if (typeof window === 'undefined')
|
|
58
|
+
return null;
|
|
59
|
+
this.currentScrollSnapshot = {
|
|
60
|
+
x: window.scrollX,
|
|
61
|
+
y: window.scrollY,
|
|
62
|
+
path: window.location.pathname +
|
|
63
|
+
window.location.search +
|
|
64
|
+
window.location.hash,
|
|
65
|
+
};
|
|
66
|
+
return this.currentScrollSnapshot;
|
|
67
|
+
}
|
|
68
|
+
/** @codexApi {"parent":"pithy.router.hmr","name":"consumeScrollSnapshot","stability":"stable","signature":"() => ScrollSnapshot | null"} */
|
|
69
|
+
/**
|
|
70
|
+
* Get and clear the captured scroll snapshot
|
|
71
|
+
* Should be called after successful remount to restore scroll
|
|
72
|
+
*/
|
|
73
|
+
consumeScrollSnapshot() {
|
|
74
|
+
const snapshot = this.currentScrollSnapshot;
|
|
75
|
+
this.currentScrollSnapshot = null;
|
|
76
|
+
return snapshot;
|
|
77
|
+
}
|
|
78
|
+
/** @codexApi {"parent":"pithy.router.hmr","name":"handleRouteUpdate","stability":"stable","signature":"async handleRouteUpdate(newRoutes: RouteNode[], currentMatch: RouteMatch | null, navigate: (path: string, options?: NavigationOptions) => Promise<boolean>, clearLoadingUI: () => void): Promise<string[]>","description":"Handle route updates from HMR with state preservation"} */
|
|
79
|
+
/**
|
|
80
|
+
* Handle route updates from HMR
|
|
81
|
+
* Updates route tree and navigates to refresh components
|
|
82
|
+
*
|
|
83
|
+
* @param newRoutes - New route tree from HMR
|
|
84
|
+
* @param currentMatch - Current route match before update
|
|
85
|
+
* @param navigate - Navigation function to trigger remount
|
|
86
|
+
* @param clearLoadingUI - Function to clear loading UI
|
|
87
|
+
* @returns Updated route patterns array
|
|
88
|
+
*/
|
|
89
|
+
async handleRouteUpdate(newRoutes, currentMatch, navigate, clearLoadingUI) {
|
|
90
|
+
const currentPattern = currentMatch?.pattern;
|
|
91
|
+
// Capture scroll position (will be restored after remount)
|
|
92
|
+
this.captureScrollSnapshot();
|
|
93
|
+
// Flatten route tree to get patterns
|
|
94
|
+
const flatRoutes = flattenRouteTree(newRoutes);
|
|
95
|
+
const routePatterns = flatRoutes.map(entry => entry.node.pattern);
|
|
96
|
+
// Check if current route still exists in new route tree
|
|
97
|
+
const currentRouteStillExists = currentPattern
|
|
98
|
+
? newRoutes.some(r => r.pattern === currentPattern)
|
|
99
|
+
: false;
|
|
100
|
+
if (!currentRouteStillExists && currentMatch) {
|
|
101
|
+
// Clear any loading UI before HMR navigation
|
|
102
|
+
clearLoadingUI();
|
|
103
|
+
// Navigate to root (scroll will reset for new route)
|
|
104
|
+
await navigate('/', { replace: true, source: 'hmr' });
|
|
105
|
+
}
|
|
106
|
+
else if (currentMatch) {
|
|
107
|
+
// Current route still exists, re-navigate to refresh it with new code
|
|
108
|
+
// Clear any loading UI before HMR navigation
|
|
109
|
+
clearLoadingUI();
|
|
110
|
+
const currentPath = window.location.pathname +
|
|
111
|
+
window.location.search +
|
|
112
|
+
window.location.hash;
|
|
113
|
+
// Re-navigate to current path to load fresh components
|
|
114
|
+
// Navigation is marked as "hmr" to skip scroll/focus management
|
|
115
|
+
await navigate(currentPath, {
|
|
116
|
+
replace: true,
|
|
117
|
+
source: 'hmr',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return routePatterns;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Setup Vite HMR subscription
|
|
124
|
+
* Listens for virtual route module updates and triggers callback
|
|
125
|
+
*/
|
|
126
|
+
setupViteHMR() {
|
|
127
|
+
const hot = import.meta.hot;
|
|
128
|
+
if (!hot)
|
|
129
|
+
return;
|
|
130
|
+
// Accept updates to the virtual route manifest module
|
|
131
|
+
hot.accept('virtual:pithy/routes.gen', (newModule) => {
|
|
132
|
+
if (!newModule || !this.updateCallback)
|
|
133
|
+
return;
|
|
134
|
+
// Debounce rapid updates (prevents self-loops and multiple rapid changes)
|
|
135
|
+
if (this.pendingUpdate) {
|
|
136
|
+
cancelAnimationFrame(this.pendingUpdate);
|
|
137
|
+
}
|
|
138
|
+
this.pendingUpdate = requestAnimationFrame(async () => {
|
|
139
|
+
this.pendingUpdate = undefined;
|
|
140
|
+
try {
|
|
141
|
+
// Extract new routes from module
|
|
142
|
+
const mod = newModule;
|
|
143
|
+
const newRoutes = mod.routes || mod.default || [];
|
|
144
|
+
// Trigger router re-mount via callback
|
|
145
|
+
// Callback is responsible for preserving state
|
|
146
|
+
await this.updateCallback(newRoutes);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
console.error('[HMR] Failed to update routes:', error);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Global HMR manager instance
|
|
157
|
+
* Singleton to ensure single subscription point
|
|
158
|
+
*/
|
|
159
|
+
export const hmrManager = new HMRManager();
|
|
160
|
+
//# sourceMappingURL=hmr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hmr.js","sourceRoot":"","sources":["../../../src/router/integration/hmr.ts"],"names":[],"mappings":"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"}
|