@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.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +621 -0
  3. package/dist/index.d.ts +29 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +49 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/router/core/config.d.ts +22 -0
  8. package/dist/router/core/config.d.ts.map +1 -0
  9. package/dist/router/core/config.js +54 -0
  10. package/dist/router/core/config.js.map +1 -0
  11. package/dist/router/core/constants.d.ts +89 -0
  12. package/dist/router/core/constants.d.ts.map +1 -0
  13. package/dist/router/core/constants.js +95 -0
  14. package/dist/router/core/constants.js.map +1 -0
  15. package/dist/router/core/history.d.ts +54 -0
  16. package/dist/router/core/history.d.ts.map +1 -0
  17. package/dist/router/core/history.js +89 -0
  18. package/dist/router/core/history.js.map +1 -0
  19. package/dist/router/core/matcher.d.ts +43 -0
  20. package/dist/router/core/matcher.d.ts.map +1 -0
  21. package/dist/router/core/matcher.js +242 -0
  22. package/dist/router/core/matcher.js.map +1 -0
  23. package/dist/router/core/params.d.ts +88 -0
  24. package/dist/router/core/params.d.ts.map +1 -0
  25. package/dist/router/core/params.js +150 -0
  26. package/dist/router/core/params.js.map +1 -0
  27. package/dist/router/core/pipeline.d.ts +90 -0
  28. package/dist/router/core/pipeline.d.ts.map +1 -0
  29. package/dist/router/core/pipeline.js +323 -0
  30. package/dist/router/core/pipeline.js.map +1 -0
  31. package/dist/router/core/route-tree.d.ts +76 -0
  32. package/dist/router/core/route-tree.d.ts.map +1 -0
  33. package/dist/router/core/route-tree.js +143 -0
  34. package/dist/router/core/route-tree.js.map +1 -0
  35. package/dist/router/core/router.d.ts +229 -0
  36. package/dist/router/core/router.d.ts.map +1 -0
  37. package/dist/router/core/router.js +762 -0
  38. package/dist/router/core/router.js.map +1 -0
  39. package/dist/router/core/types.d.ts +387 -0
  40. package/dist/router/core/types.d.ts.map +1 -0
  41. package/dist/router/core/types.js +22 -0
  42. package/dist/router/core/types.js.map +1 -0
  43. package/dist/router/core/utils.d.ts +66 -0
  44. package/dist/router/core/utils.d.ts.map +1 -0
  45. package/dist/router/core/utils.js +152 -0
  46. package/dist/router/core/utils.js.map +1 -0
  47. package/dist/router/features/coordinator.d.ts +75 -0
  48. package/dist/router/features/coordinator.d.ts.map +1 -0
  49. package/dist/router/features/coordinator.js +203 -0
  50. package/dist/router/features/coordinator.js.map +1 -0
  51. package/dist/router/features/errors/error-codes.d.ts +107 -0
  52. package/dist/router/features/errors/error-codes.d.ts.map +1 -0
  53. package/dist/router/features/errors/error-codes.js +247 -0
  54. package/dist/router/features/errors/error-codes.js.map +1 -0
  55. package/dist/router/features/errors/error-resolution.d.ts +31 -0
  56. package/dist/router/features/errors/error-resolution.d.ts.map +1 -0
  57. package/dist/router/features/errors/error-resolution.js +47 -0
  58. package/dist/router/features/errors/error-resolution.js.map +1 -0
  59. package/dist/router/features/errors/error-types.d.ts +60 -0
  60. package/dist/router/features/errors/error-types.d.ts.map +1 -0
  61. package/dist/router/features/errors/error-types.js +57 -0
  62. package/dist/router/features/errors/error-types.js.map +1 -0
  63. package/dist/router/features/errors/global-error-fallback.d.ts +33 -0
  64. package/dist/router/features/errors/global-error-fallback.d.ts.map +1 -0
  65. package/dist/router/features/errors/global-error-fallback.js +182 -0
  66. package/dist/router/features/errors/global-error-fallback.js.map +1 -0
  67. package/dist/router/features/errors/index.d.ts +15 -0
  68. package/dist/router/features/errors/index.d.ts.map +1 -0
  69. package/dist/router/features/errors/index.js +18 -0
  70. package/dist/router/features/errors/index.js.map +1 -0
  71. package/dist/router/features/errors/router-errors.d.ts +44 -0
  72. package/dist/router/features/errors/router-errors.d.ts.map +1 -0
  73. package/dist/router/features/errors/router-errors.js +53 -0
  74. package/dist/router/features/errors/router-errors.js.map +1 -0
  75. package/dist/router/features/guards/engine.d.ts +40 -0
  76. package/dist/router/features/guards/engine.d.ts.map +1 -0
  77. package/dist/router/features/guards/engine.js +67 -0
  78. package/dist/router/features/guards/engine.js.map +1 -0
  79. package/dist/router/features/guards/index.d.ts +52 -0
  80. package/dist/router/features/guards/index.d.ts.map +1 -0
  81. package/dist/router/features/guards/index.js +95 -0
  82. package/dist/router/features/guards/index.js.map +1 -0
  83. package/dist/router/features/loaders/cache.d.ts +60 -0
  84. package/dist/router/features/loaders/cache.d.ts.map +1 -0
  85. package/dist/router/features/loaders/cache.js +80 -0
  86. package/dist/router/features/loaders/cache.js.map +1 -0
  87. package/dist/router/features/loaders/engine.d.ts +17 -0
  88. package/dist/router/features/loaders/engine.d.ts.map +1 -0
  89. package/dist/router/features/loaders/engine.js +27 -0
  90. package/dist/router/features/loaders/engine.js.map +1 -0
  91. package/dist/router/features/loaders/index.d.ts +49 -0
  92. package/dist/router/features/loaders/index.d.ts.map +1 -0
  93. package/dist/router/features/loaders/index.js +112 -0
  94. package/dist/router/features/loaders/index.js.map +1 -0
  95. package/dist/router/features/loading-ui-manager.d.ts +69 -0
  96. package/dist/router/features/loading-ui-manager.d.ts.map +1 -0
  97. package/dist/router/features/loading-ui-manager.js +209 -0
  98. package/dist/router/features/loading-ui-manager.js.map +1 -0
  99. package/dist/router/features/module-loader.d.ts +50 -0
  100. package/dist/router/features/module-loader.d.ts.map +1 -0
  101. package/dist/router/features/module-loader.js +121 -0
  102. package/dist/router/features/module-loader.js.map +1 -0
  103. package/dist/router/features/outlet/errors.d.ts +24 -0
  104. package/dist/router/features/outlet/errors.d.ts.map +1 -0
  105. package/dist/router/features/outlet/errors.js +96 -0
  106. package/dist/router/features/outlet/errors.js.map +1 -0
  107. package/dist/router/features/outlet/index.d.ts +45 -0
  108. package/dist/router/features/outlet/index.d.ts.map +1 -0
  109. package/dist/router/features/outlet/index.js +95 -0
  110. package/dist/router/features/outlet/index.js.map +1 -0
  111. package/dist/router/features/outlet/mount.d.ts +38 -0
  112. package/dist/router/features/outlet/mount.d.ts.map +1 -0
  113. package/dist/router/features/outlet/mount.js +301 -0
  114. package/dist/router/features/outlet/mount.js.map +1 -0
  115. package/dist/router/features/outlet/segment-chain.d.ts +16 -0
  116. package/dist/router/features/outlet/segment-chain.d.ts.map +1 -0
  117. package/dist/router/features/outlet/segment-chain.js +62 -0
  118. package/dist/router/features/outlet/segment-chain.js.map +1 -0
  119. package/dist/router/features/prefetch/index.d.ts +97 -0
  120. package/dist/router/features/prefetch/index.d.ts.map +1 -0
  121. package/dist/router/features/prefetch/index.js +345 -0
  122. package/dist/router/features/prefetch/index.js.map +1 -0
  123. package/dist/router/features/ux/focus.d.ts +62 -0
  124. package/dist/router/features/ux/focus.d.ts.map +1 -0
  125. package/dist/router/features/ux/focus.js +136 -0
  126. package/dist/router/features/ux/focus.js.map +1 -0
  127. package/dist/router/features/ux/live-region.d.ts +81 -0
  128. package/dist/router/features/ux/live-region.d.ts.map +1 -0
  129. package/dist/router/features/ux/live-region.js +166 -0
  130. package/dist/router/features/ux/live-region.js.map +1 -0
  131. package/dist/router/features/ux/scroll-focus.d.ts +105 -0
  132. package/dist/router/features/ux/scroll-focus.d.ts.map +1 -0
  133. package/dist/router/features/ux/scroll-focus.js +111 -0
  134. package/dist/router/features/ux/scroll-focus.js.map +1 -0
  135. package/dist/router/features/ux/scroll.d.ts +71 -0
  136. package/dist/router/features/ux/scroll.d.ts.map +1 -0
  137. package/dist/router/features/ux/scroll.js +137 -0
  138. package/dist/router/features/ux/scroll.js.map +1 -0
  139. package/dist/router/features/ux/transitions.d.ts +120 -0
  140. package/dist/router/features/ux/transitions.d.ts.map +1 -0
  141. package/dist/router/features/ux/transitions.js +235 -0
  142. package/dist/router/features/ux/transitions.js.map +1 -0
  143. package/dist/router/integration/directive-binder.d.ts +20 -0
  144. package/dist/router/integration/directive-binder.d.ts.map +1 -0
  145. package/dist/router/integration/directive-binder.js +139 -0
  146. package/dist/router/integration/directive-binder.js.map +1 -0
  147. package/dist/router/integration/directives.d.ts +42 -0
  148. package/dist/router/integration/directives.d.ts.map +1 -0
  149. package/dist/router/integration/directives.js +109 -0
  150. package/dist/router/integration/directives.js.map +1 -0
  151. package/dist/router/integration/history-adapter.d.ts +76 -0
  152. package/dist/router/integration/history-adapter.d.ts.map +1 -0
  153. package/dist/router/integration/history-adapter.js +157 -0
  154. package/dist/router/integration/history-adapter.js.map +1 -0
  155. package/dist/router/integration/hmr.d.ts +83 -0
  156. package/dist/router/integration/hmr.d.ts.map +1 -0
  157. package/dist/router/integration/hmr.js +160 -0
  158. package/dist/router/integration/hmr.js.map +1 -0
  159. package/dist/router/integration/link-interceptor.d.ts +49 -0
  160. package/dist/router/integration/link-interceptor.d.ts.map +1 -0
  161. package/dist/router/integration/link-interceptor.js +94 -0
  162. package/dist/router/integration/link-interceptor.js.map +1 -0
  163. 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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"}