@absolutejs/absolute 0.20.0-beta.169 → 0.20.0-beta.170

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 (238) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +232 -0
  3. package/changelog.json +228 -0
  4. package/dist/angular/browser.js +7 -8
  5. package/dist/angular/browser.js.map +6 -5
  6. package/dist/angular/components/core/streamingSlotRegistrar.js +6 -2
  7. package/dist/angular/components/core/streamingSlotRegistry.js +7 -3
  8. package/dist/angular/index.js +555 -156
  9. package/dist/angular/index.js.map +29 -23
  10. package/dist/angular/server.js +555 -156
  11. package/dist/angular/server.js.map +29 -23
  12. package/dist/build/buildWorker.js +14 -8
  13. package/dist/build.js +4949 -1941
  14. package/dist/build.js.map +72 -42
  15. package/dist/cli/{add-7azr0c83.js → add-6a7pjkn9.js} +11 -7
  16. package/dist/cli/{analyze-3cyek6v3.js → analyze-606fshwa.js} +1 -1
  17. package/dist/cli/{build-25rz8v25.js → build-g81xqbb9.js} +1 -1
  18. package/dist/cli/{compile-4jnfxts9.js → compile-9y1w9zpj.js} +3 -3
  19. package/dist/cli/config/server.js +10699 -1538
  20. package/dist/cli/{dev-9tk5hn2e.js → dev-hyeb89hp.js} +5 -1
  21. package/dist/cli/{doctor-4nv2c2j3.js → doctor-h55y9hj1.js} +1 -1
  22. package/dist/cli/{generate-v75p2fct.js → generate-20x1pkf7.js} +4 -4
  23. package/dist/cli/{htmx-427swtdz.js → htmx-6mz2h7k1.js} +2 -2
  24. package/dist/cli/{index-bhbs6d5z.js → index-32ge2w1f.js} +12 -4
  25. package/dist/cli/{index-a4ws2jwd.js → index-3wz3mktm.js} +1 -1
  26. package/dist/cli/{index-b7hjgknf.js → index-9sksd8c8.js} +13 -4
  27. package/dist/cli/{index-eyzwc7gz.js → index-qbrzvmh9.js} +4 -2
  28. package/dist/cli/index-qw98g1p6.js +269 -0
  29. package/dist/cli/{index-434b7j4a.js → index-vtp20gwf.js} +69 -1
  30. package/dist/cli/{index-h7qhz02e.js → index-wk0pyxrg.js} +3 -0
  31. package/dist/cli/{index-k3y8cb6c.js → index-z6f43b20.js} +1 -1
  32. package/dist/cli/index.js +16 -16
  33. package/dist/cli/{islands-8sgky9gh.js → islands-dc54e5y1.js} +1 -1
  34. package/dist/cli/{ls-4m4yqwww.js → ls-b6zz24nt.js} +1 -1
  35. package/dist/cli/{mobile-hcxjr2gf.js → mobile-ajnnvqtv.js} +3 -3
  36. package/dist/cli/prerender-fqpp0m1j.js +10 -0
  37. package/dist/cli/{remove-1ycjx85k.js → remove-t8x1mrxg.js} +4 -4
  38. package/dist/cli/{start-mbaejxhm.js → start-d6p831ny.js} +3 -3
  39. package/dist/cli/{typecheck-dr068pmh.js → typecheck-stbt0qt5.js} +6 -1
  40. package/dist/cli/{workspace-1sycp9g1.js → workspace-61hh6jq1.js} +1 -1
  41. package/dist/client/chunks/index-6b66nzyf.js +381 -0
  42. package/dist/client/chunks/index-6b66nzyf.js.map +11 -0
  43. package/dist/client/chunks/{islands-rc28k5sx.js → islands-s2ta5ajb.js} +5 -2
  44. package/dist/client/chunks/{islands-rc28k5sx.js.map → islands-s2ta5ajb.js.map} +7 -6
  45. package/dist/client/index.js +160 -3
  46. package/dist/client/index.js.map +6 -4
  47. package/dist/client/navigation.js +764 -0
  48. package/dist/client/navigation.js.map +10 -0
  49. package/dist/client/prefetch.js +33 -375
  50. package/dist/client/prefetch.js.map +3 -4
  51. package/dist/core/streamingSlotRegistrar.js +5 -2
  52. package/dist/core/streamingSlotRegistrar.js.map +5 -4
  53. package/dist/core/streamingSlotRegistry.js +5 -2
  54. package/dist/core/streamingSlotRegistry.js.map +5 -4
  55. package/dist/dev/client/cspNonce.ts +28 -0
  56. package/dist/dev/client/handlers/html.ts +4 -3
  57. package/dist/dev/client/handlers/htmx.ts +3 -2
  58. package/dist/dev/client/handlers/svelte.ts +15 -90
  59. package/dist/dev/client/syncDevtools.ts +2 -1
  60. package/dist/dev/serverBootstrap.js +11 -4
  61. package/dist/i18n/index.js +543 -0
  62. package/dist/i18n/index.js.map +15 -0
  63. package/dist/index.js +6339 -2233
  64. package/dist/index.js.map +89 -54
  65. package/dist/islands/index.js +282 -40
  66. package/dist/islands/index.js.map +14 -10
  67. package/dist/mobile/browser.js +7 -2
  68. package/dist/mobile/browser.js.map +5 -4
  69. package/dist/mobile/index.js +7 -2
  70. package/dist/mobile/index.js.map +5 -4
  71. package/dist/mobile/remoteMacAgentEntry.js +1 -1
  72. package/dist/react/components/browser/index.js +94 -4
  73. package/dist/react/components/index.js +150 -10
  74. package/dist/react/components/index.js.map +15 -10
  75. package/dist/react/hooks/index.js +479 -13
  76. package/dist/react/hooks/index.js.map +12 -4
  77. package/dist/react/index.js +769 -166
  78. package/dist/react/index.js.map +32 -21
  79. package/dist/react/router/browser.js +21 -4
  80. package/dist/react/router/browser.js.map +5 -4
  81. package/dist/react/router/index.js +21 -4
  82. package/dist/react/router/index.js.map +5 -4
  83. package/dist/react/server.js +597 -203
  84. package/dist/react/server.js.map +25 -15
  85. package/dist/src/angular/zoneRuntime.d.ts +5 -0
  86. package/dist/src/build/buildReactServerComponents.d.ts +22 -0
  87. package/dist/src/build/downlevelAsyncForZone.d.ts +1 -0
  88. package/dist/src/build/fileRoutes.d.ts +65 -0
  89. package/dist/src/build/fonts/buildFonts.d.ts +25 -0
  90. package/dist/src/build/fonts/fallbackMetrics.d.ts +13 -0
  91. package/dist/src/build/fonts/googleFonts.d.ts +18 -0
  92. package/dist/src/build/fonts/injectFontTags.d.ts +7 -0
  93. package/dist/src/build/fonts/parseFontMetrics.d.ts +4 -0
  94. package/dist/src/build/generateReactIndexes.d.ts +7 -1
  95. package/dist/src/build/htmlIncludes.d.ts +16 -0
  96. package/dist/src/build/inlineSourceMapComment.d.ts +1 -0
  97. package/dist/src/build/scanConventions.d.ts +3 -0
  98. package/dist/src/build/vitals.d.ts +21 -0
  99. package/dist/src/client/formActions.d.ts +29 -0
  100. package/dist/src/client/index.d.ts +2 -0
  101. package/dist/src/client/navigation.d.ts +16 -0
  102. package/dist/src/client/prefetch.d.ts +2 -30
  103. package/dist/src/core/build.d.ts +2 -2
  104. package/dist/src/core/cspNonce.d.ts +12 -0
  105. package/dist/src/core/fileRouteAdapter.d.ts +15 -0
  106. package/dist/src/core/fileRoutes.d.ts +5 -0
  107. package/dist/src/core/formActionFlash.d.ts +25 -0
  108. package/dist/src/core/formActions.d.ts +249 -0
  109. package/dist/src/core/index.d.ts +5 -0
  110. package/dist/src/core/partialPrerender.d.ts +28 -0
  111. package/dist/src/core/partialPrerenderServe.d.ts +24 -0
  112. package/dist/src/core/precompressed.d.ts +11 -1
  113. package/dist/src/core/prerenderNonce.d.ts +9 -0
  114. package/dist/src/core/productionRuntime.d.ts +4 -2
  115. package/dist/src/core/serverOnlyStaticFiles.d.ts +8 -0
  116. package/dist/src/core/staticHtmlNonce.d.ts +2 -0
  117. package/dist/src/core/vitals.d.ts +108 -0
  118. package/dist/src/dev/angular/fastHmrCompiler.d.ts +1 -1
  119. package/dist/src/dev/client/cspNonce.d.ts +2 -0
  120. package/dist/src/dev/fileRouteRefresher.d.ts +5 -0
  121. package/dist/src/dev/importCost/instrument.d.ts +1 -1
  122. package/dist/src/dev/reactServerComponentsRebuild.d.ts +3 -0
  123. package/dist/src/i18n/defineMessages.d.ts +5 -0
  124. package/dist/src/i18n/formatMessage.d.ts +26 -0
  125. package/dist/src/i18n/index.d.ts +6 -0
  126. package/dist/src/i18n/intlCache.d.ts +5 -0
  127. package/dist/src/i18n/routing.d.ts +28 -0
  128. package/dist/src/i18n/translator.d.ts +11 -0
  129. package/dist/src/mobile/androidRelease.d.ts +2 -2
  130. package/dist/src/mobile/iosRelease.d.ts +4 -4
  131. package/dist/src/mobile/materializedBundle.d.ts +1 -1
  132. package/dist/src/mobile/mobileInspect.d.ts +1 -1
  133. package/dist/src/mobile/releaseArtifact.d.ts +2 -2
  134. package/dist/src/mobile/remoteMacProtocol.d.ts +2 -2
  135. package/dist/src/plugins/i18n.d.ts +30 -0
  136. package/dist/src/plugins/index.d.ts +2 -1
  137. package/dist/src/plugins/secureHeaders.d.ts +48 -0
  138. package/dist/src/react/components/Head.d.ts +1 -1
  139. package/dist/src/react/components/StreamSlot.browser.d.ts +1 -0
  140. package/dist/src/react/components/StreamSlot.d.ts +5 -1
  141. package/dist/src/react/components/SuspenseSlot.browser.d.ts +2 -0
  142. package/dist/src/react/components/SuspenseSlot.d.ts +12 -0
  143. package/dist/src/react/fileRoute.d.ts +18 -0
  144. package/dist/src/react/hooks/index.d.ts +4 -0
  145. package/dist/src/react/hooks/useFormAction.d.ts +42 -0
  146. package/dist/src/react/hooks/useI18n.d.ts +15 -0
  147. package/dist/src/react/index.d.ts +2 -0
  148. package/dist/src/react/server.d.ts +2 -0
  149. package/dist/src/react/serverComponents/constants.d.ts +11 -0
  150. package/dist/src/react/serverComponents/flightPayload.d.ts +2 -0
  151. package/dist/src/react/serverComponents/pageHandler.d.ts +14 -0
  152. package/dist/src/svelte/browser.d.ts +1 -0
  153. package/dist/src/svelte/fileRoute.d.ts +15 -0
  154. package/dist/src/svelte/i18n.d.ts +8 -0
  155. package/dist/src/svelte/index.d.ts +2 -0
  156. package/dist/src/svelte/lowerAwaitSlotSyntax.d.ts +5 -1
  157. package/dist/src/svelte/renderToReadableStream.d.ts +10 -2
  158. package/dist/src/svelte/router/browser.d.ts +3 -1
  159. package/dist/src/svelte/router/goto.d.ts +13 -10
  160. package/dist/src/svelte/router/guards.d.ts +43 -0
  161. package/dist/src/svelte/router/index.d.ts +3 -1
  162. package/dist/src/svelte/router/lazy.d.ts +17 -0
  163. package/dist/src/svelte/router/lazyModule.d.ts +29 -0
  164. package/dist/src/svelte/router/navigation.d.ts +55 -0
  165. package/dist/src/svelte/router/pushState.d.ts +4 -2
  166. package/dist/src/svelte/router/scroll.d.ts +34 -0
  167. package/dist/src/svelte/routerSsr.d.ts +9 -0
  168. package/dist/src/svelte/server.d.ts +1 -0
  169. package/dist/src/utils/cspNonceRef.d.ts +8 -0
  170. package/dist/src/utils/fontAssets.d.ts +17 -0
  171. package/dist/src/utils/formActionProtocol.d.ts +10 -0
  172. package/dist/src/utils/generateHeadElement.d.ts +1 -1
  173. package/dist/src/utils/index.d.ts +1 -0
  174. package/dist/src/utils/inlinePageCss.d.ts +18 -0
  175. package/dist/src/utils/loadConfig.d.ts +5 -0
  176. package/dist/src/utils/localeRef.d.ts +10 -0
  177. package/dist/src/utils/resolveConvention.d.ts +6 -1
  178. package/dist/src/utils/streamingSlots.d.ts +5 -0
  179. package/dist/src/vue/browser.d.ts +2 -0
  180. package/dist/src/vue/components/Image.d.ts +2 -2
  181. package/dist/src/vue/index.d.ts +2 -0
  182. package/dist/src/vue/serverDataPayload.d.ts +32 -0
  183. package/dist/src/vue/serverDataState.d.ts +9 -0
  184. package/dist/src/vue/useServerData.browser.d.ts +6 -0
  185. package/dist/src/vue/useServerData.d.ts +24 -0
  186. package/dist/svelte/browser.js +330 -3
  187. package/dist/svelte/browser.js.map +8 -4
  188. package/dist/svelte/components/AwaitSlot.svelte +5 -0
  189. package/dist/svelte/components/Head.svelte +50 -0
  190. package/dist/svelte/components/JsonLd.svelte +9 -1
  191. package/dist/svelte/components/StreamSlot.svelte +5 -0
  192. package/dist/svelte/index.js +1017 -117
  193. package/dist/svelte/index.js.map +38 -24
  194. package/dist/svelte/router/Link.svelte +40 -8
  195. package/dist/svelte/router/Route.svelte +157 -12
  196. package/dist/svelte/router/Route.svelte.d.ts +39 -5
  197. package/dist/svelte/router/Router.svelte +140 -25
  198. package/dist/svelte/router/Router.svelte.d.ts +5 -1
  199. package/dist/svelte/router/browser.ts +10 -0
  200. package/dist/svelte/router/goto.ts +15 -86
  201. package/dist/svelte/router/guards.ts +112 -0
  202. package/dist/svelte/router/index.ts +10 -0
  203. package/dist/svelte/router/lazy.ts +82 -0
  204. package/dist/svelte/router/lazyModule.ts +74 -0
  205. package/dist/svelte/router/navigation.ts +412 -0
  206. package/dist/svelte/router/pushState.ts +7 -4
  207. package/dist/svelte/router/scroll.ts +286 -0
  208. package/dist/svelte/server.js +747 -252
  209. package/dist/svelte/server.js.map +30 -20
  210. package/dist/types/angular.d.ts +2 -0
  211. package/dist/types/build.d.ts +37 -0
  212. package/dist/types/conventions.d.ts +3 -0
  213. package/dist/types/fileRoutes.d.ts +72 -0
  214. package/dist/types/fonts.d.ts +86 -0
  215. package/dist/types/formAction.d.ts +54 -0
  216. package/dist/types/globals.d.ts +10 -0
  217. package/dist/types/i18n.d.ts +185 -0
  218. package/dist/types/index.d.ts +4 -0
  219. package/dist/types/metadata.d.ts +11 -0
  220. package/dist/types/navigation.d.ts +37 -0
  221. package/dist/types/prefetch.d.ts +10 -0
  222. package/dist/types/sitemap.d.ts +4 -0
  223. package/dist/types/svelteRouter.d.ts +89 -2
  224. package/dist/types/svelteRouter.ts +112 -2
  225. package/dist/types/vue.d.ts +46 -1
  226. package/dist/utils/iconVersion.ts +34 -0
  227. package/dist/vue/browser.js +107 -11
  228. package/dist/vue/browser.js.map +7 -4
  229. package/dist/vue/components/index.js +5 -2
  230. package/dist/vue/components/index.js.map +5 -4
  231. package/dist/vue/index.js +712 -158
  232. package/dist/vue/index.js.map +32 -23
  233. package/dist/vue/server.js +310 -79
  234. package/dist/vue/server.js.map +20 -15
  235. package/package.json +9 -1
  236. package/dist/cli/prerender-mfnwsvyt.js +0 -7
  237. package/dist/src/plugins/pageRouter.d.ts +0 -1
  238. /package/dist/cli/{index-j7m8pq48.js → index-rervww4p.js} +0 -0
@@ -1,8 +1,13 @@
1
1
  <script lang="ts">
2
2
  import { onDestroy, onMount, type Snippet } from 'svelte';
3
3
  import type { LinkPrefetchMode } from '../../types/svelteRouter';
4
- import { goto } from './goto';
5
4
  import {
5
+ belongsToDocumentNavigation,
6
+ navigate,
7
+ preloadRoute
8
+ } from './navigation';
9
+ import {
10
+ canPrefetch,
6
11
  observeViewport,
7
12
  prefetch,
8
13
  resolveDefaultPrefetchMode,
@@ -26,7 +31,8 @@
26
31
  prerender?: boolean;
27
32
  /** Don't reset focus to body on navigate. */
28
33
  keepFocus?: boolean;
29
- /** Don't scroll to top on navigate. */
34
+ /** Leave the scroll position alone on navigate (no scroll to the
35
+ * top or to the `#hash` target). */
30
36
  noScroll?: boolean;
31
37
  /** Forwarded to the underlying `<a>` element. */
32
38
  class?: string;
@@ -77,19 +83,42 @@
77
83
  if (target && target !== '_self') return;
78
84
  if (rest['download'] !== undefined) return;
79
85
  if (isExternal(to)) return;
86
+ // No <Router> here has a route for `to` and cross-framework
87
+ // navigation is on: another page owns the URL, so the click goes to
88
+ // the browser, where the navigation layer swaps that page in.
89
+ if (belongsToDocumentNavigation(to)) return;
80
90
 
81
91
  event.preventDefault();
82
- void goto(to, {
83
- keepFocus,
84
- noScroll,
85
- replaceState
92
+ navigate(
93
+ to,
94
+ {
95
+ keepFocus,
96
+ noScroll,
97
+ replaceState
98
+ },
99
+ 'link'
100
+ ).catch((navigationError: unknown) => {
101
+ console.error(
102
+ '[absolute] <Link> navigation failed:',
103
+ navigationError
104
+ );
86
105
  });
87
106
  };
88
107
 
108
+ // A lazy route's component chunk is part of what a prefetch warms, on
109
+ // the same triggers (hover, viewport, pointerdown) and under the same
110
+ // data-saver / opt-out rules as the document prefetch.
111
+ const preloadLazyRoute = () => {
112
+ if (canPrefetch()) preloadRoute(to);
113
+ };
114
+
89
115
  const handlePointerEnter = () => {
90
116
  if (resolvedMode() === 'none' || isExternal(to)) return;
91
117
  hoverHandle?.cancel();
92
- hoverHandle = scheduleHoverPrefetch(to, { prerender });
118
+ hoverHandle = scheduleHoverPrefetch(to, {
119
+ onPrefetch: preloadLazyRoute,
120
+ prerender
121
+ });
93
122
  };
94
123
 
95
124
  const handlePointerLeave = () => {
@@ -102,6 +131,7 @@
102
131
  hoverHandle?.cancel();
103
132
  hoverHandle = null;
104
133
  prefetch(to, { kind: 'route' });
134
+ preloadLazyRoute();
105
135
  if (prerender) speculate(to);
106
136
  };
107
137
 
@@ -109,7 +139,9 @@
109
139
  if (resolvedMode() !== 'viewport' || !anchor) return;
110
140
  if (isExternal(to)) return;
111
141
 
112
- unobserveViewport = observeViewport(anchor, to);
142
+ unobserveViewport = observeViewport(anchor, to, {
143
+ onVisible: preloadLazyRoute
144
+ });
113
145
  });
114
146
 
115
147
  onDestroy(() => {
@@ -1,17 +1,52 @@
1
1
  <script lang="ts" generics="Path extends string">
2
2
  import { getContext, onDestroy, type Snippet } from 'svelte';
3
- import type { ExtractRouteParams } from '../../types/svelteRouter';
3
+ import type {
4
+ ExtractRouteParams,
5
+ LazyRouteLoader,
6
+ RouteGuard,
7
+ SvelteRouterSsrState
8
+ } from '../../types/svelteRouter';
4
9
  import type { RouterRegistry } from './Router.svelte';
10
+ import { forgetLazy, lazyCacheKey, loadLazy, peekLazy } from './lazy';
11
+ import type { LazyComponent } from './lazyModule';
5
12
  import { compilePattern, joinBasepath } from './matchPath';
6
13
 
7
14
  const ROUTER_CONTEXT_KEY = Symbol.for('absolutejs.svelte-router');
15
+ const ROUTER_SSR_KEY = Symbol.for('absolutejs.svelte-router.ssr');
8
16
 
9
17
  type RouteProps = {
10
18
  path: Path;
11
- content: Snippet<[ExtractRouteParams<Path>]>;
19
+ /** Eager route: the snippet to render while the route is active. */
20
+ content?: Snippet<[ExtractRouteParams<Path>]>;
21
+ /** Lazy route: loads the component the first time the route is
22
+ * needed — `() => import('./Page.svelte')`. The build splits it
23
+ * into its own chunk; the server awaits it so the HTML is complete
24
+ * and hydration matches. The component receives the route params
25
+ * (and `props`) as props. */
26
+ loader?: LazyRouteLoader;
27
+ /** Extra props for a lazy route's component. */
28
+ props?: Record<string, unknown>;
29
+ /** Rendered while a lazy route's component loads in the browser. */
30
+ pending?: Snippet;
31
+ /** Rendered when a lazy route's loader rejects. `retry` calls the
32
+ * loader again. */
33
+ error?: Snippet<[unknown, () => void]>;
34
+ /** Runs before navigating onto this route, after the router's
35
+ * guards. Runs during the server render too (a redirect becomes a
36
+ * 302). Return `false` to cancel, a URL or `redirect(...)` to
37
+ * redirect, nothing to allow. */
38
+ guard?: RouteGuard;
12
39
  };
13
40
 
14
- let { path, content }: RouteProps = $props();
41
+ let {
42
+ path,
43
+ content,
44
+ loader,
45
+ props: extraProps,
46
+ pending,
47
+ error,
48
+ guard
49
+ }: RouteProps = $props();
15
50
 
16
51
  const registry = getContext<RouterRegistry | undefined>(ROUTER_CONTEXT_KEY);
17
52
  if (!registry) {
@@ -20,25 +55,34 @@
20
55
  'Wrap your routes in `<Router url={...}>` (server) or `<Router>` (client).'
21
56
  );
22
57
  }
58
+ const ssr = getContext<SvelteRouterSsrState | undefined>(ROUTER_SSR_KEY);
23
59
 
24
60
  const id = registry.nextRouteId();
25
61
  const registrationOrder = Number(id.slice(1));
62
+ const fullPath = $derived(joinBasepath(registry.basepath, path));
26
63
 
27
- // Register synchronously at script-body time so SSR (which doesn't
28
- // run $effect) sees the registration before the template runs.
29
- registry.register(id, {
64
+ const createEntry = () => ({
65
+ fullPath: joinBasepath(registry.basepath, path),
66
+ getGuard: () => guard,
67
+ getLoader: () => loader,
30
68
  pattern: compilePattern(joinBasepath(registry.basepath, path)),
31
69
  registrationOrder
32
70
  });
33
71
 
72
+ // Register synchronously at script-body time so SSR (which doesn't
73
+ // run $effect) sees the registration before the template runs.
74
+ registry.register(id, createEntry());
75
+
34
76
  $effect(() => {
35
- registry.register(id, {
36
- pattern: compilePattern(joinBasepath(registry.basepath, path)),
37
- registrationOrder
38
- });
77
+ registry.register(id, createEntry());
39
78
  });
40
79
 
41
- onDestroy(() => registry.deregister(id));
80
+ // The server render runs onDestroy as soon as the HTML is produced, but
81
+ // the router's guards run after that (between render passes) and need
82
+ // the full route table — so only the browser deregisters.
83
+ onDestroy(() => {
84
+ if (typeof window !== 'undefined') registry.deregister(id);
85
+ });
42
86
 
43
87
  // The Router computes the active match across all registered Routes
44
88
  // (specificity-ranked). Each Route checks if it's the winner and
@@ -47,8 +91,109 @@
47
91
  // that section instead of getting hoisted to the Router's root.
48
92
  const match = $derived(registry.getActiveMatch());
49
93
  const isActive = $derived(match?.id === id);
94
+
95
+ // ---- Lazy routes ---------------------------------------------------
96
+ //
97
+ // Browser: the component comes from the lazy cache — loaded on an
98
+ // earlier visit, by a <Link> prefetch, or (first hydration) by the
99
+ // hydration entry, which loads the modules the server rendered before
100
+ // it hydrates. Otherwise the effect below loads it while `pending`
101
+ // renders.
102
+ //
103
+ // Server: the first render pass registers the load with the page
104
+ // handler, which awaits it and renders again with the component
105
+ // resolved, so the HTML holds the real component.
106
+ const cacheKey = $derived(loader ? lazyCacheKey(fullPath, loader) : '');
107
+ // Read synchronously at creation so the first (hydrating) render
108
+ // already has the component when it is available.
109
+ const peekInitialComponent = () =>
110
+ !ssr && loader
111
+ ? peekLazy(lazyCacheKey(fullPath, loader), fullPath)
112
+ : undefined;
113
+ let loadedComponent = $state<LazyComponent | undefined>(
114
+ peekInitialComponent()
115
+ );
116
+ let loadFailure = $state<{ error: unknown } | undefined>(undefined);
117
+
118
+ const serverLazy = (active: boolean) => {
119
+ if (!ssr || !loader || !active) return undefined;
120
+ const resolved = ssr.resolved.get(fullPath);
121
+ if (!resolved) ssr.load(fullPath, loader);
122
+ // No `error` snippet: fail the render like any other SSR error.
123
+ if (resolved && 'error' in resolved && !error) throw resolved.error;
124
+
125
+ return resolved;
126
+ };
127
+
128
+ const serverResolved = $derived(serverLazy(isActive));
129
+ const Loaded = $derived(
130
+ ssr
131
+ ? serverResolved && 'component' in serverResolved
132
+ ? serverResolved.component
133
+ : undefined
134
+ : loadedComponent
135
+ );
136
+ const failure = $derived(
137
+ ssr
138
+ ? serverResolved && 'error' in serverResolved
139
+ ? { error: serverResolved.error }
140
+ : undefined
141
+ : loadFailure
142
+ );
143
+
144
+ const reportLoadFailure = (reason: unknown) => {
145
+ loadFailure = { error: reason };
146
+ // No `error` snippet: surface it like any uncaught error (the dev
147
+ // overlay shows it) instead of failing silently.
148
+ if (!error) {
149
+ queueMicrotask(() => {
150
+ throw reason;
151
+ });
152
+ }
153
+ };
154
+
155
+ $effect(() => {
156
+ if (!loader || !isActive || loadFailure) return;
157
+ const key = cacheKey;
158
+ const cached = peekLazy(key, fullPath);
159
+ if (cached) {
160
+ if (loadedComponent !== cached) loadedComponent = cached;
161
+
162
+ return;
163
+ }
164
+ if (loadedComponent) loadedComponent = undefined;
165
+ let live = true;
166
+ loadLazy(key, loader).then(
167
+ (component) => {
168
+ if (live) loadedComponent = component;
169
+ },
170
+ (reason: unknown) => {
171
+ if (live) reportLoadFailure(reason);
172
+ }
173
+ );
174
+
175
+ return () => {
176
+ live = false;
177
+ };
178
+ });
179
+
180
+ const retry = () => {
181
+ forgetLazy(cacheKey);
182
+ loadFailure = undefined;
183
+ loadedComponent = undefined;
184
+ };
50
185
  </script>
51
186
 
52
187
  {#if isActive && match}
53
- {@render content(match.params as ExtractRouteParams<Path>)}
188
+ {#if loader}
189
+ {#if Loaded}
190
+ <Loaded {...extraProps ?? {}} {...match.params} />
191
+ {:else if failure}
192
+ {@render error?.(failure.error, retry)}
193
+ {:else if pending}
194
+ {@render pending()}
195
+ {/if}
196
+ {:else if content}
197
+ {@render content(match.params as ExtractRouteParams<Path>)}
198
+ {/if}
54
199
  {/if}
@@ -1,13 +1,47 @@
1
1
  import { SvelteComponent, type Snippet } from 'svelte';
2
- import type { ExtractRouteParams } from '../../types/svelteRouter';
2
+ import type {
3
+ ExtractRouteParams,
4
+ LazyRouteLoader,
5
+ RouteGuard
6
+ } from '../../types/svelteRouter';
3
7
 
4
- type RouteProps<Path extends string> = {
8
+ type RouteBaseProps<Path extends string> = {
5
9
  path: Path;
10
+ /** Runs before navigating onto this route, after the router's guards
11
+ * (and during the server render, where a redirect becomes a 302). */
12
+ guard?: RouteGuard;
13
+ };
14
+
15
+ /** Eager route: render a snippet while the route is active. */
16
+ type EagerRouteProps<Path extends string> = RouteBaseProps<Path> & {
6
17
  content: Snippet<[ExtractRouteParams<Path>]>;
18
+ loader?: never;
19
+ props?: never;
20
+ pending?: never;
21
+ error?: never;
7
22
  };
8
23
 
9
- class Route<Path extends string = string> extends SvelteComponent<
10
- RouteProps<Path>
11
- > {}
24
+ /** Lazy route: load the component on first use —
25
+ * `loader={() => import('./Page.svelte')}`. It receives the route params
26
+ * (plus `props`) as props. */
27
+ type LazyRouteProps<
28
+ Path extends string,
29
+ Props extends Record<string, unknown>
30
+ > = RouteBaseProps<Path> & {
31
+ content?: never;
32
+ loader: LazyRouteLoader<Props>;
33
+ props?: Omit<Props, keyof ExtractRouteParams<Path>>;
34
+ pending?: Snippet;
35
+ error?: Snippet<[unknown, () => void]>;
36
+ };
37
+
38
+ type RouteProps<Path extends string, Props extends Record<string, unknown>> =
39
+ | EagerRouteProps<Path>
40
+ | LazyRouteProps<Path, Props>;
41
+
42
+ class Route<
43
+ Path extends string = string,
44
+ Props extends Record<string, unknown> = ExtractRouteParams<Path>
45
+ > extends SvelteComponent<RouteProps<Path, Props>> {}
12
46
 
13
47
  export { Route as default };
@@ -2,8 +2,17 @@
2
2
  type RouteEntry = {
3
3
  // reactive — read by the winner-resolver
4
4
  pattern: ReturnType<typeof import('./matchPath').compilePattern>;
5
+ // basepath-joined path pattern, the lazy-route cache key
6
+ fullPath: string;
5
7
  // stable — assigned at registration time, used as tiebreaker
6
8
  registrationOrder: number;
9
+ // read on demand so a changed prop is always seen
10
+ getGuard: () =>
11
+ | import('../../types/svelteRouter').RouteGuard
12
+ | undefined;
13
+ getLoader: () =>
14
+ | import('../../types/svelteRouter').LazyRouteLoader
15
+ | undefined;
7
16
  };
8
17
 
9
18
  type ActiveMatch = {
@@ -27,13 +36,27 @@
27
36
 
28
37
  <script lang="ts">
29
38
  import { getContext, onMount, setContext, type Snippet } from 'svelte';
30
- import type { RouterMode } from '../../types/svelteRouter';
31
- import { setRouterMode } from './goto';
32
- import { hashPathnameOf } from './hashMode';
39
+ import type {
40
+ RouteGuard,
41
+ RouterMode,
42
+ SvelteRouterSsrState
43
+ } from '../../types/svelteRouter';
44
+ import { type GuardEntry, runGuardChain } from './guards';
45
+ import { buildHashHref, hashPathnameOf } from './hashMode';
46
+ import { lazyCacheKey, loadLazy } from './lazy';
33
47
  import { joinBasepath, matchPattern } from './matchPath';
34
- import { page, seedPage, setPage } from './page.js';
48
+ import {
49
+ enterInitialRoute,
50
+ handleTraversal,
51
+ registerRouter,
52
+ restoreInitialScroll,
53
+ setRouterMode
54
+ } from './navigation';
55
+ import { page, seedPage } from './page.js';
56
+ import { activateScrollRestoration, stampInitialEntry } from './scroll';
35
57
 
36
58
  const ROUTER_CONTEXT_KEY = Symbol.for('absolutejs.svelte-router');
59
+ const ROUTER_SSR_KEY = Symbol.for('absolutejs.svelte-router.ssr');
37
60
 
38
61
  type RouterProps = {
39
62
  /** SSR URL passthrough. On the server, the page handler forwards
@@ -46,6 +69,12 @@
46
69
  /** `'history'` (default, clean URLs) or `'hash'` (`/#/path`,
47
70
  * for static deploys). */
48
71
  mode?: RouterMode;
72
+ /** Runs before every navigation onto this router's routes (the
73
+ * outermost router: every navigation), before the matched
74
+ * `<Route guard>`. Runs during the server render too, where a
75
+ * redirect becomes a 302. Return `false` to cancel, a URL or
76
+ * `redirect(...)` to redirect, nothing to allow. */
77
+ guard?: RouteGuard;
49
78
  children?: Snippet;
50
79
  };
51
80
 
@@ -53,10 +82,12 @@
53
82
  url,
54
83
  basepath = '',
55
84
  mode = 'history',
85
+ guard,
56
86
  children
57
87
  }: RouterProps = $props();
58
88
 
59
89
  const parent = getContext<RouterRegistry | undefined>(ROUTER_CONTEXT_KEY);
90
+ const ssr = getContext<SvelteRouterSsrState | undefined>(ROUTER_SSR_KEY);
60
91
  const stackedBasepath = parent
61
92
  ? joinBasepath(parent.basepath, basepath)
62
93
  : basepath === ''
@@ -77,14 +108,14 @@
77
108
  const routes = $state(new Map<string, RouteEntry>());
78
109
  let routeCounter = 0;
79
110
 
80
- const computeWinner = (): ActiveMatch | null => {
111
+ const computeWinnerFor = (pathname: string): ActiveMatch | null => {
81
112
  let bestId: string | null = null;
82
113
  let bestParams: Record<string, string | undefined> | null = null;
83
114
  let bestScore = -Infinity;
84
115
  let bestOrder = Infinity;
85
116
 
86
117
  for (const [id, entry] of routes) {
87
- const match = matchPattern(entry.pattern, page.url.pathname);
118
+ const match = matchPattern(entry.pattern, pathname);
88
119
  if (!match.matched) continue;
89
120
 
90
121
  if (
@@ -118,7 +149,7 @@
118
149
  deregister: (id) => {
119
150
  routes.delete(id);
120
151
  },
121
- getActiveMatch: () => computeWinner(),
152
+ getActiveMatch: () => computeWinnerFor(page.url.pathname),
122
153
  mode: stackedMode,
123
154
  nextRouteId: () => `r${routeCounter++}`,
124
155
  register: (id, entry) => {
@@ -128,6 +159,39 @@
128
159
 
129
160
  setContext<RouterRegistry>(ROUTER_CONTEXT_KEY, registry);
130
161
 
162
+ // Guards for a navigation to `target`: this router's own guard (the
163
+ // outermost one always; a nested one only when one of its routes
164
+ // matches), then the matched route's guard.
165
+ const guardsFor = (target: URL) => {
166
+ const winner = computeWinnerFor(target.pathname);
167
+ const entries: GuardEntry[] = [];
168
+ const routerGuard = guard;
169
+ if (routerGuard && (isOutermost || winner)) {
170
+ entries.push({ guard: routerGuard, params: winner?.params ?? {} });
171
+ }
172
+ const routeGuard = winner
173
+ ? routes.get(winner.id)?.getGuard()
174
+ : undefined;
175
+ if (winner && routeGuard) {
176
+ entries.push({ guard: routeGuard, params: winner.params });
177
+ }
178
+
179
+ return entries;
180
+ };
181
+
182
+ const matches = (target: URL) => computeWinnerFor(target.pathname) !== null;
183
+
184
+ const preload = (target: URL) => {
185
+ const winner = computeWinnerFor(target.pathname);
186
+ const entry = winner ? routes.get(winner.id) : undefined;
187
+ const loader = entry?.getLoader();
188
+ if (!entry || !loader) return;
189
+ loadLazy(lazyCacheKey(entry.fullPath, loader), loader).catch(() => {
190
+ // A failed prefetch is retried (and reported) by the Route
191
+ // itself if the user actually navigates there.
192
+ });
193
+ };
194
+
131
195
  if (isOutermost) {
132
196
  setRouterMode(stackedMode);
133
197
 
@@ -145,31 +209,80 @@
145
209
  seedPage(initial);
146
210
  }
147
211
 
212
+ // Server render: once this pass has registered every Route, run this
213
+ // router's guards for the request URL. The page handler awaits them
214
+ // before rendering again; a redirect becomes a 302. The URL is captured
215
+ // now — `page` is shared module state and other requests may render
216
+ // before the guards run.
217
+ if (ssr) {
218
+ const renderUrl = new URL(page.url.href);
219
+ const requestUrl = new URL(url ?? '/', 'http://localhost/');
220
+ const guardKey = `${stackedBasepath}\u0000${renderUrl.href}`;
221
+ ssr.afterRender(async () => {
222
+ if (ssr.outcome() !== undefined) return;
223
+ if (ssr.settledGuards.has(guardKey)) return;
224
+ ssr.settledGuards.add(guardKey);
225
+ const decision = await runGuardChain(guardsFor(renderUrl), {
226
+ from: null,
227
+ request: ssr.request,
228
+ to: renderUrl,
229
+ type: 'enter'
230
+ });
231
+ if (decision.kind === 'cancel') ssr.setOutcome({ kind: 'cancel' });
232
+ if (decision.kind !== 'redirect') return;
233
+ const target = new URL(decision.location, renderUrl);
234
+ const location =
235
+ stackedMode === 'hash'
236
+ ? `${requestUrl.pathname}${requestUrl.search}${buildHashHref(target.pathname + target.search)}`
237
+ : target.origin === renderUrl.origin
238
+ ? `${target.pathname}${target.search}${target.hash}`
239
+ : target.href;
240
+ ssr.setOutcome({ kind: 'redirect', location });
241
+ });
242
+ }
243
+
148
244
  onMount(() => {
149
- if (!isOutermost) return;
245
+ const unregister = registerRouter({ guardsFor, matches, preload });
246
+ if (!isOutermost) return unregister;
150
247
 
151
- const onPopState = (event: PopStateEvent) => {
248
+ const initialEntry = stampInitialEntry();
249
+ const deactivateScroll = activateScrollRestoration();
250
+ const navigationEntry = performance.getEntriesByType('navigation')[0];
251
+ const navigationKind =
252
+ navigationEntry && 'type' in navigationEntry
253
+ ? navigationEntry.type
254
+ : undefined;
255
+ if (
256
+ initialEntry.scroll &&
257
+ (navigationKind === 'reload' || navigationKind === 'back_forward')
258
+ ) {
259
+ void restoreInitialScroll(initialEntry.scroll);
260
+ }
261
+ if (stackedMode === 'hash') void enterInitialRoute(page.url);
262
+
263
+ const routableUrl = () => {
152
264
  const next = new URL(window.location.href);
153
- const routable =
154
- stackedMode === 'hash' ? hashPathnameOf(next) : next.pathname;
155
265
  const synthetic = new URL(next.href);
156
- synthetic.pathname = routable;
157
- setPage({
158
- params: {},
159
- state: event.state ?? null,
160
- url: synthetic
161
- });
266
+ if (stackedMode === 'hash')
267
+ synthetic.pathname = hashPathnameOf(next);
268
+
269
+ return synthetic;
162
270
  };
163
271
 
272
+ let lastTraversalHref = '';
273
+ const onPopState = (event: PopStateEvent) => {
274
+ lastTraversalHref = window.location.href;
275
+ void handleTraversal(event.state, routableUrl());
276
+ };
277
+
278
+ // Browsers fire `popstate` for hash changes too; `hashchange` only
279
+ // catches the rare engine that doesn't, so skip URLs already handled.
164
280
  const onHashChange = () => {
165
- const next = new URL(window.location.href);
166
- const synthetic = new URL(next.href);
167
- synthetic.pathname = hashPathnameOf(next);
168
- setPage({
169
- params: {},
170
- state: window.history.state ?? null,
171
- url: synthetic
172
- });
281
+ if (window.location.href === lastTraversalHref) return;
282
+ const next = routableUrl();
283
+ if (next.href === page.url.href) return;
284
+ lastTraversalHref = window.location.href;
285
+ void handleTraversal(window.history.state, next);
173
286
  };
174
287
 
175
288
  window.addEventListener('popstate', onPopState);
@@ -178,6 +291,8 @@
178
291
  }
179
292
 
180
293
  return () => {
294
+ unregister();
295
+ deactivateScroll();
181
296
  window.removeEventListener('popstate', onPopState);
182
297
  if (stackedMode === 'hash') {
183
298
  window.removeEventListener('hashchange', onHashChange);
@@ -1,10 +1,14 @@
1
1
  import { SvelteComponent, type Snippet } from 'svelte';
2
- import type { RouterMode } from '../../types/svelteRouter';
2
+ import type { RouteGuard, RouterMode } from '../../types/svelteRouter';
3
3
 
4
4
  type RouterProps = {
5
5
  url?: string;
6
6
  basepath?: string;
7
7
  mode?: RouterMode;
8
+ /** Runs before every navigation onto this router's routes (the
9
+ * outermost router: every navigation), and during the server render,
10
+ * where a redirect becomes a 302. */
11
+ guard?: RouteGuard;
8
12
  children?: Snippet;
9
13
  };
10
14
 
@@ -2,13 +2,23 @@
2
2
  // their .svelte path; this entry exposes only the runtime API.
3
3
 
4
4
  export { goto } from './goto';
5
+ export { NavigationRedirect, redirect } from './guards';
6
+ export { beforeNavigate, preloadRoute } from './navigation';
5
7
  export { page } from './page.js';
6
8
  export { pushState, replaceState } from './pushState';
7
9
 
8
10
  export type {
9
11
  ExtractRouteParams,
10
12
  GotoOptions,
13
+ LazyRouteLoader,
14
+ LazyRouteModule,
11
15
  LinkPrefetchMode,
16
+ NavigationContext,
17
+ NavigationOutcome,
18
+ NavigationRedirectTarget,
19
+ NavigationType,
12
20
  PageState,
21
+ RouteGuard,
22
+ RouteGuardResult,
13
23
  RouterMode
14
24
  } from '../../types/svelteRouter';