@real-router/svelte 0.10.0 → 0.11.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 (54) hide show
  1. package/README.md +63 -11
  2. package/dist/RouterProvider.svelte +12 -3
  3. package/dist/components/Await.svelte +48 -0
  4. package/dist/components/Await.svelte.d.ts +50 -0
  5. package/dist/components/ClientOnly.svelte +22 -0
  6. package/dist/components/ClientOnly.svelte.d.ts +8 -0
  7. package/dist/components/HttpStatusCode.svelte +63 -0
  8. package/dist/components/HttpStatusCode.svelte.d.ts +45 -0
  9. package/dist/components/HttpStatusProvider.svelte +45 -0
  10. package/dist/components/HttpStatusProvider.svelte.d.ts +30 -0
  11. package/dist/components/RouteView.helpers.d.ts +1 -0
  12. package/dist/components/RouteView.helpers.js +16 -0
  13. package/dist/components/RouteView.svelte +1 -25
  14. package/dist/components/RouteView.svelte.d.ts +0 -1
  15. package/dist/components/ServerOnly.svelte +22 -0
  16. package/dist/components/ServerOnly.svelte.d.ts +8 -0
  17. package/dist/components/Streamed.svelte +37 -0
  18. package/dist/components/Streamed.svelte.d.ts +46 -0
  19. package/dist/composables/useDeferred.svelte.d.ts +24 -0
  20. package/dist/composables/useDeferred.svelte.js +34 -0
  21. package/dist/composables/useRoute.svelte.d.ts +8 -1
  22. package/dist/context.d.ts +1 -0
  23. package/dist/context.js +1 -0
  24. package/dist/dom-utils/__test-helpers/expected-fragment.d.ts +30 -0
  25. package/dist/dom-utils/__test-helpers/expected-fragment.js +43 -0
  26. package/dist/dom-utils/__test-helpers/index.d.ts +8 -0
  27. package/dist/dom-utils/__test-helpers/index.js +8 -0
  28. package/dist/dom-utils/link-utils.d.ts +23 -0
  29. package/dist/dom-utils/link-utils.js +106 -5
  30. package/dist/dom-utils/route-announcer.js +51 -2
  31. package/dist/dom-utils/scroll-restore.d.ts +38 -1
  32. package/dist/dom-utils/scroll-restore.js +144 -12
  33. package/dist/ssr.d.ts +9 -0
  34. package/dist/ssr.js +17 -0
  35. package/dist/types.d.ts +23 -0
  36. package/dist/utils/createHttpStatusSink.d.ts +28 -0
  37. package/dist/utils/createHttpStatusSink.js +3 -0
  38. package/package.json +10 -5
  39. package/src/RouterProvider.svelte +12 -3
  40. package/src/components/Await.svelte +48 -0
  41. package/src/components/ClientOnly.svelte +22 -0
  42. package/src/components/HttpStatusCode.svelte +63 -0
  43. package/src/components/HttpStatusProvider.svelte +45 -0
  44. package/src/components/RouteView.helpers.ts +24 -0
  45. package/src/components/RouteView.svelte +1 -25
  46. package/src/components/ServerOnly.svelte +22 -0
  47. package/src/components/Streamed.svelte +37 -0
  48. package/src/composables/useDeferred.svelte.ts +41 -0
  49. package/src/composables/useIsActiveRoute.svelte.ts +1 -1
  50. package/src/composables/useRoute.svelte.ts +11 -7
  51. package/src/context.ts +2 -0
  52. package/src/ssr.ts +28 -0
  53. package/src/types.ts +23 -0
  54. package/src/utils/createHttpStatusSink.ts +31 -0
package/README.md CHANGED
@@ -61,11 +61,13 @@ npm install @real-router/svelte @real-router/core @real-router/browser-plugin
61
61
 
62
62
  All composables must be called during component initialization (not inside `$effect` or event handlers). Reactive composables return `{ current: T }` getter objects — read `.current` inside a template or `$derived` to register a reactive dependency.
63
63
 
64
+ `useRoute()` returns a **non-nullable** `route.current` (typed as `State<P>`) and throws when the router has no active state (unstarted, stopped, disposed). `useRouteNode(name)` keeps its nullable `current` — node inactivity is a legitimate business state, not a lifecycle error.
65
+
64
66
  | Composable | Returns | Reactive? |
65
67
  | ----------------------- | --------------------------------------------------------------- | ------------------------------------------ |
66
68
  | `useRouter()` | `Router` | Never |
67
69
  | `useNavigator()` | `Navigator` | Never (stable ref, safe to use directly) |
68
- | `useRoute()` | `{ navigator, route: { current }, previousRoute: { current } }` | `.current` on every navigation |
70
+ | `useRoute()` | `{ navigator, route: { current: State<P> }, previousRoute: { current } }` — throws if no active state | `.current` on every navigation |
69
71
  | `useRouteNode(name)` | `{ navigator, route: { current }, previousRoute: { current } }` | `.current` when node activates/deactivates |
70
72
  | `useRouteUtils()` | `RouteUtils` | Never |
71
73
  | `useRouterTransition()` | `{ current: RouterTransitionSnapshot }` | `.current` on transition start/end |
@@ -118,7 +120,9 @@ All composables must be called during component initialization (not inside `$eff
118
120
  <script lang="ts">
119
121
  import { useRouteExit } from "@real-router/svelte";
120
122
 
121
- let el: HTMLDivElement;
123
+ // `let el = $state<HTMLDivElement | null>(null)` under Svelte 5 strict mode —
124
+ // the `bind:this` target must be reactive for binding to land.
125
+ let el = $state<HTMLDivElement | null>(null);
122
126
 
123
127
  useRouteExit(async ({ signal }) => {
124
128
  if (!el) return;
@@ -239,14 +243,26 @@ Declarative route matching. Renders the snippet whose name matches the active ro
239
243
 
240
244
  **Props:**
241
245
 
242
- | Prop | Type | Description |
243
- | ----------- | --------- | ------------------------------------------- |
244
- | `nodeName` | `string` | Route node to match against. `""` for root. |
245
- | `notFound` | `Snippet` | Rendered when route is `UNKNOWN_ROUTE` |
246
- | `[segment]` | `Snippet` | Named snippet matching a route segment |
246
+ | Prop | Type | Description |
247
+ | ----------- | --------- | -------------------------------------------------------------------------------------------- |
248
+ | `nodeName` | `string` | Route node to match against. `""` for root. |
249
+ | `self` | `Snippet` | Rendered when the active route is exactly `nodeName` (no descendant segments) |
250
+ | `notFound` | `Snippet` | Rendered when route is `UNKNOWN_ROUTE` |
251
+ | `[segment]` | `Snippet` | Named snippet matching a route segment |
247
252
 
248
253
  Snippet names must be valid JavaScript identifiers and match the first segment of the active route after `nodeName`. For a route `users.profile` with `nodeName=""`, the snippet named `users` matches.
249
254
 
255
+ **`self` snippet:** distinguishes the exact-match case from any descendant. With `nodeName="users"`, the `users` segment snippet matches `users`, `users.list`, `users.profile`, etc. — but `{#snippet self()}` only renders when the active route is `"users"` exactly. Both `self` and `notFound` are **reserved** snippet names: a literal route named `notFound` or `self` is never matched as a regular segment.
256
+
257
+ ```svelte
258
+ <!-- nested: render UsersIndex on /users, list on /users/list -->
259
+ <RouteView nodeName="users">
260
+ {#snippet self()}<UsersIndex /> {/snippet}
261
+ {#snippet list()}<UsersList /> {/snippet}
262
+ {#snippet profile()}<UserProfile /> {/snippet}
263
+ </RouteView>
264
+ ```
265
+
250
266
  > **Note:** `keepAlive` is not supported. Svelte has no equivalent of React's `<Activity>` API or Vue's `<KeepAlive>`. Components are destroyed when navigating away.
251
267
 
252
268
  ### `<RouterErrorBoundary>`
@@ -281,6 +297,33 @@ Auto-resets on next successful navigation. Works with both `<Link>` and imperati
281
297
 
282
298
  **`onError` signature:** `(error, toRoute, fromRoute) => void`. Receives the `RouterError`, the attempted destination (`State | null`), and the previously active route (`State | null`). A throwing `onError` is caught by the boundary, logged via `console.error`, and never breaks reactivity.
283
299
 
300
+ ### `<ClientOnly>` / `<ServerOnly>`
301
+
302
+ Paired SSR-aware boundaries. `<ClientOnly>` renders the `fallback` snippet on the server (and on the client first paint, to match SSR HTML), then swaps in the `children` snippet after mount. `<ServerOnly>` is the symmetric inverse.
303
+
304
+ ```svelte
305
+ <script lang="ts">
306
+ import { ClientOnly, ServerOnly } from "@real-router/svelte";
307
+ </script>
308
+
309
+ <ClientOnly>
310
+ {#snippet children()}
311
+ <BrowserApiWidget />
312
+ {/snippet}
313
+ {#snippet fallback()}
314
+ <Skeleton />
315
+ {/snippet}
316
+ </ClientOnly>
317
+
318
+ <ServerOnly>
319
+ {#snippet children()}
320
+ <SeoMetaStrip />
321
+ {/snippet}
322
+ </ServerOnly>
323
+ ```
324
+
325
+ Implementation: `$state(false)` + `$effect(() => mounted = true)`. The Svelte compiler emits the rune as a no-op on the server, so server-side rendering naturally lands on the SSR-side branch. End-to-end dogfooding lives in [`examples/web/svelte/ssr-examples/ssr/`](../../examples/web/svelte/ssr-examples/ssr/) (see `e2e/ssr-boundaries.spec.ts`).
326
+
284
327
  ## Actions
285
328
 
286
329
  ### `createLinkAction`
@@ -302,7 +345,10 @@ Factory function that creates a low-level action for adding navigation to any el
302
345
  Go Home
303
346
  </button>
304
347
 
305
- <div use:link={{ name: "settings", params: {}, options: { replace: true } }} role="link" tabindex="0">
348
+ <!-- For non-anchor / non-button elements you can omit role="link" + tabindex="0":
349
+ applyLinkA11y adds them automatically. Explicit attrs in the example below are
350
+ redundant but harmless. -->
351
+ <div use:link={{ name: "settings", params: {}, options: { replace: true } }}>
306
352
  Settings
307
353
  </div>
308
354
  ```
@@ -315,7 +361,9 @@ Factory function that creates a low-level action for adding navigation to any el
315
361
  | `params` | `Params` | `{}` | Route parameters |
316
362
  | `options` | `object` | `{}` | Navigation options (replace, etc.) |
317
363
 
318
- The action automatically adds `role="link"` + `tabindex="0"` to non-interactive elements for accessibility. It handles click events and Enter key navigation.
364
+ The action automatically adds `role="link"` + `tabindex="0"` to non-interactive elements for accessibility (skipping `<a>` / `<button>` which already convey link semantics). It handles click events and Enter key navigation.
365
+
366
+ > **Hash asymmetry vs `<Link hash>`:** `createLinkAction` does **not** accept a `hash` parameter; `<Link hash="x">` does (#532). Use `<Link>` when a hash-aware variant is needed (tab-style UIs, same-route different-fragment navigation through `navigateWithHash`). For pure `use:link` callers, attach a click handler that calls `router.navigate(name, params, { force: true, hash: "x" })` manually if hash control is required.
319
367
 
320
368
  ## Reactive Primitives
321
369
 
@@ -448,9 +496,13 @@ Full documentation: [Wiki](https://github.com/greydragon888/real-router/wiki)
448
496
 
449
497
  ## Examples
450
498
 
451
- 16 runnable examples — each is a standalone Vite app. Run: `cd examples/web/svelte/basic && pnpm dev`
499
+ 24 runnable examples — each is a standalone Vite app. Run: `cd examples/web/svelte/basic && pnpm dev`
500
+
501
+ **Core:** [basic](../../examples/web/svelte/basic) · [nested-routes](../../examples/web/svelte/nested-routes) · [auth-guards](../../examples/web/svelte/auth-guards) · [data-loading](../../examples/web/svelte/data-loading) · [lazy-loading](../../examples/web/svelte/lazy-loading) · [async-guards](../../examples/web/svelte/async-guards) · [hash-routing](../../examples/web/svelte/hash-routing) · [persistent-params](../../examples/web/svelte/persistent-params) · [error-handling](../../examples/web/svelte/error-handling) · [dynamic-routes](../../examples/web/svelte/dynamic-routes) · [link-action](../../examples/web/svelte/link-action) · [lazy-loading-svelte](../../examples/web/svelte/lazy-loading-svelte) · [snippets-routing](../../examples/web/svelte/snippets-routing) · [reactive-source](../../examples/web/svelte/reactive-source) · [search-schema](../../examples/web/svelte/search-schema) · [combined](../../examples/web/svelte/combined)
502
+
503
+ **Animations:** [motion-animations](../../examples/web/svelte/animation-examples/motion-animations) · [page-animations](../../examples/web/svelte/animation-examples/page-animations) · [route-animations](../../examples/web/svelte/animation-examples/route-animations) · [view-transitions](../../examples/web/svelte/animation-examples/view-transitions)
452
504
 
453
- [basic](../../examples/web/svelte/basic) · [nested-routes](../../examples/web/svelte/nested-routes) · [auth-guards](../../examples/web/svelte/auth-guards) · [data-loading](../../examples/web/svelte/data-loading) · [lazy-loading](../../examples/web/svelte/lazy-loading) · [async-guards](../../examples/web/svelte/async-guards) · [hash-routing](../../examples/web/svelte/hash-routing) · [persistent-params](../../examples/web/svelte/persistent-params) · [error-handling](../../examples/web/svelte/error-handling) · [dynamic-routes](../../examples/web/svelte/dynamic-routes) · [link-action](../../examples/web/svelte/link-action) · [lazy-loading-svelte](../../examples/web/svelte/lazy-loading-svelte) · [snippets-routing](../../examples/web/svelte/snippets-routing) · [reactive-source](../../examples/web/svelte/reactive-source) · [search-schema](../../examples/web/svelte/search-schema) · [combined](../../examples/web/svelte/combined)
505
+ **Server-side rendering:** [ssr](../../examples/web/svelte/ssr-examples/ssr) · [ssr-streaming](../../examples/web/svelte/ssr-examples/ssr-streaming) · [ssr-mixed](../../examples/web/svelte/ssr-examples/ssr-mixed) · [ssg](../../examples/web/svelte/ssr-examples/ssg)
454
506
 
455
507
  ## Related Packages
456
508
 
@@ -47,9 +47,18 @@
47
47
 
48
48
  $effect(() => {
49
49
  if (!srEnabled) return;
50
- // Read scrollRestoration object props via `untrack` for non-primitive
51
- // refs that naturally change each render. Primitive $derived memos
52
- // (mode/anchor/behavior/storageKey) drive re-runs.
50
+ // Pin primitive $derived deps as explicit dependencies of this effect
51
+ // BEFORE constructing the utility. The four `void srX` reads make
52
+ // intent unambiguous: even if `createScrollRestoration` throws after
53
+ // partial argument evaluation (e.g. invalid `mode` rejected), every
54
+ // srMode/srAnchor/srBehavior/srStorageKey is already in this effect's
55
+ // dependency set — the next change to any of them re-runs the effect
56
+ // and the utility gets rebuilt. Without these reads, the dependency
57
+ // tracking would depend on Svelte's argument-evaluation order inside
58
+ // the factory call, which is brittle. Non-primitive refs (like
59
+ // `scrollContainer` — a DOM element that changes ref every render but
60
+ // is identity-equal in practice) are deliberately read via `untrack`
61
+ // to keep this effect from re-running on every parent re-render.
53
62
  void srMode;
54
63
  void srAnchor;
55
64
  void srBehavior;
@@ -0,0 +1,48 @@
1
+ <!--
2
+ @component
3
+ Reads `useDeferred(name)` and renders the `children` snippet with the
4
+ resolved value via Svelte's native `{#await}` block. Optional `fallback`
5
+ snippet shown while the promise is pending; rejection bubbles to the
6
+ nearest `{:catch}` handler in the surrounding `{#await}` chain (or
7
+ `<Streamed>`).
8
+
9
+ ```svelte
10
+ <Await name="reviews">
11
+ {#snippet children(reviews)}
12
+ <ReviewList items={reviews} />
13
+ {/snippet}
14
+ {#snippet fallback()}
15
+ <Spinner />
16
+ {/snippet}
17
+ </Await>
18
+ ```
19
+ -->
20
+ <script lang="ts" generics="T">
21
+ import { useDeferred } from "../composables/useDeferred.svelte";
22
+
23
+ import type { Snippet } from "svelte";
24
+
25
+ interface Props {
26
+ /** Deferred key declared in the loader's `defer({ deferred: { <name>: ... } })`. */
27
+ name: string;
28
+ /** Render snippet for the resolved value. */
29
+ children: Snippet<[T]>;
30
+ /** Snippet shown while the promise is pending. */
31
+ fallback?: Snippet;
32
+ }
33
+
34
+ let { name, children, fallback }: Props = $props();
35
+
36
+ // `useDeferred(name)` reads `state.context.ssrDataDeferred[name]` —
37
+ // wrap in `$derived` so a dynamic `name` prop re-resolves the promise
38
+ // (vs. capturing the initial value at component init).
39
+ const promise = $derived(useDeferred<T>(name));
40
+ </script>
41
+
42
+ {#await promise}
43
+ {#if fallback}
44
+ {@render fallback()}
45
+ {/if}
46
+ {:then value}
47
+ {@render children(value)}
48
+ {/await}
@@ -0,0 +1,50 @@
1
+ import type { Snippet } from "svelte";
2
+ declare function $$render<T>(): {
3
+ props: {
4
+ /** Deferred key declared in the loader's `defer({ deferred: { <name>: ... } })`. */
5
+ name: string;
6
+ /** Render snippet for the resolved value. */
7
+ children: Snippet<[T]>;
8
+ /** Snippet shown while the promise is pending. */
9
+ fallback?: Snippet;
10
+ };
11
+ exports: {};
12
+ bindings: "";
13
+ slots: {};
14
+ events: {};
15
+ };
16
+ declare class __sveltets_Render<T> {
17
+ props(): ReturnType<typeof $$render<T>>['props'];
18
+ events(): ReturnType<typeof $$render<T>>['events'];
19
+ slots(): ReturnType<typeof $$render<T>>['slots'];
20
+ bindings(): "";
21
+ exports(): {};
22
+ }
23
+ interface $$IsomorphicComponent {
24
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
25
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
26
+ } & ReturnType<__sveltets_Render<T>['exports']>;
27
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
28
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
29
+ }
30
+ /**
31
+ * Reads `useDeferred(name)` and renders the `children` snippet with the
32
+ * resolved value via Svelte's native `{#await}` block. Optional `fallback`
33
+ * snippet shown while the promise is pending; rejection bubbles to the
34
+ * nearest `{:catch}` handler in the surrounding `{#await}` chain (or
35
+ * `<Streamed>`).
36
+ *
37
+ * ```svelte
38
+ * <Await name="reviews">
39
+ * {#snippet children(reviews)}
40
+ * <ReviewList items={reviews} />
41
+ * {/snippet}
42
+ * {#snippet fallback()}
43
+ * <Spinner />
44
+ * {/snippet}
45
+ * </Await>
46
+ * ```
47
+ */
48
+ declare const Await: $$IsomorphicComponent;
49
+ type Await<T> = InstanceType<typeof Await<T>>;
50
+ export default Await;
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+
4
+ interface Props {
5
+ children: Snippet;
6
+ fallback?: Snippet;
7
+ }
8
+
9
+ let { children, fallback }: Props = $props();
10
+
11
+ let mounted = $state(false);
12
+
13
+ $effect(() => {
14
+ mounted = true;
15
+ });
16
+ </script>
17
+
18
+ {#if mounted}
19
+ {@render children()}
20
+ {:else if fallback}
21
+ {@render fallback()}
22
+ {/if}
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from "svelte";
2
+ interface Props {
3
+ children: Snippet;
4
+ fallback?: Snippet;
5
+ }
6
+ declare const ClientOnly: import("svelte").Component<Props, {}, "">;
7
+ type ClientOnly = ReturnType<typeof ClientOnly>;
8
+ export default ClientOnly;
@@ -0,0 +1,63 @@
1
+ <!--
2
+ @component
3
+ Render-time HTTP status declaration. Mount inside a route component (typical
4
+ use case: a glob `*` route's NotFound page) when the status is decided by
5
+ the rendered tree rather than a loader.
6
+
7
+ Writes `code` to the nearest `<HttpStatusProvider>`'s sink during component
8
+ init and renders nothing. With no provider mounted (the standard
9
+ client-side case) the component is a silent no-op — same component tree
10
+ hydrates without touching the DOM or warning about mismatches.
11
+
12
+ Loader-driven errors (`LoaderNotFound` → 404, `LoaderRedirect` → 30x) keep
13
+ working as before; this component covers render-time decisions only.
14
+
15
+ Last write wins when several `<HttpStatusCode />` instances mount in the
16
+ same render pass — sink reflects the last component that ran.
17
+
18
+ ```svelte
19
+ <HttpStatusCode code={404} />
20
+ ```
21
+
22
+ **Streaming SSR ({#await}):** Svelte 5 stable does NOT chunk-stream HTTP
23
+ for `{#await}` — the server emits the pending branch and returns the full
24
+ response immediately, async resolution happens client-side. So the sink
25
+ is always written by the time `await render(App, ...)` resolves, regardless
26
+ of where `<HttpStatusCode />` is mounted. (This is RSC-like, not React 19
27
+ / Solid streaming.) No ordering concern.
28
+
29
+ **Hydration symmetry:** Svelte 5's hydration walker tolerates `{#if}`-branch
30
+ asymmetry between server and client (verified by `ssr/` e2e — no warnings
31
+ fire when SSR has the wrapper but CSR doesn't). The example's `App.svelte`
32
+ uses `{#if httpStatusSink}` so the wrapper is server-only; this is safe in
33
+ Svelte but would be a hydration mismatch in Vue/Solid.
34
+
35
+ **Valid `code` range:** Node's `res.end()` throws `Invalid status code` on
36
+ `NaN`, `0`, negative values, or values `> 999` — this surfaces as a 5xx /
37
+ dropped connection, not silent corruption. Pass a real HTTP status integer
38
+ (commonly 4xx/5xx; 100-999 is what Node accepts).
39
+ -->
40
+ <script lang="ts">
41
+ import { getContext } from "svelte";
42
+
43
+ import { HTTP_STATUS_KEY } from "../context";
44
+
45
+ import type { HttpStatusSink } from "../utils/createHttpStatusSink";
46
+
47
+ interface Props {
48
+ /** HTTP status to apply to the response. Common values: 404, 410, 451, 503. */
49
+ code: number;
50
+ }
51
+
52
+ let { code }: Props = $props();
53
+
54
+ const sink = getContext<HttpStatusSink | undefined>(HTTP_STATUS_KEY);
55
+
56
+ if (sink) {
57
+ // svelte-ignore state_referenced_locally
58
+ // Intentional one-time write at component init: the sink is read by the
59
+ // server after `await render()` and a single value is the contract.
60
+ // Consumers that need to update the code mid-render should remount.
61
+ sink.code = code;
62
+ }
63
+ </script>
@@ -0,0 +1,45 @@
1
+ interface Props {
2
+ /** HTTP status to apply to the response. Common values: 404, 410, 451, 503. */
3
+ code: number;
4
+ }
5
+ /**
6
+ * Render-time HTTP status declaration. Mount inside a route component (typical
7
+ * use case: a glob `*` route's NotFound page) when the status is decided by
8
+ * the rendered tree rather than a loader.
9
+ *
10
+ * Writes `code` to the nearest `<HttpStatusProvider>`'s sink during component
11
+ * init and renders nothing. With no provider mounted (the standard
12
+ * client-side case) the component is a silent no-op — same component tree
13
+ * hydrates without touching the DOM or warning about mismatches.
14
+ *
15
+ * Loader-driven errors (`LoaderNotFound` → 404, `LoaderRedirect` → 30x) keep
16
+ * working as before; this component covers render-time decisions only.
17
+ *
18
+ * Last write wins when several `<HttpStatusCode />` instances mount in the
19
+ * same render pass — sink reflects the last component that ran.
20
+ *
21
+ * ```svelte
22
+ * <HttpStatusCode code={404} />
23
+ * ```
24
+ *
25
+ * **Streaming SSR ({#await}):** Svelte 5 stable does NOT chunk-stream HTTP
26
+ * for `{#await}` — the server emits the pending branch and returns the full
27
+ * response immediately, async resolution happens client-side. So the sink
28
+ * is always written by the time `await render(App, ...)` resolves, regardless
29
+ * of where `<HttpStatusCode />` is mounted. (This is RSC-like, not React 19
30
+ * / Solid streaming.) No ordering concern.
31
+ *
32
+ * **Hydration symmetry:** Svelte 5's hydration walker tolerates `{#if}`-branch
33
+ * asymmetry between server and client (verified by `ssr/` e2e — no warnings
34
+ * fire when SSR has the wrapper but CSR doesn't). The example's `App.svelte`
35
+ * uses `{#if httpStatusSink}` so the wrapper is server-only; this is safe in
36
+ * Svelte but would be a hydration mismatch in Vue/Solid.
37
+ *
38
+ * **Valid `code` range:** Node's `res.end()` throws `Invalid status code` on
39
+ * `NaN`, `0`, negative values, or values `> 999` — this surfaces as a 5xx /
40
+ * dropped connection, not silent corruption. Pass a real HTTP status integer
41
+ * (commonly 4xx/5xx; 100-999 is what Node accepts).
42
+ */
43
+ declare const HttpStatusCode: import("svelte").Component<Props, {}, "">;
44
+ type HttpStatusCode = ReturnType<typeof HttpStatusCode>;
45
+ export default HttpStatusCode;
@@ -0,0 +1,45 @@
1
+ <!--
2
+ @component
3
+ Wraps an SSR tree with a render-scoped `HttpStatusSink`. `<HttpStatusCode />`
4
+ reads the sink via `getContext` and writes its `code` to it during component
5
+ init. Read `sink.code` after `await render()` to set the HTTP response
6
+ status.
7
+
8
+ ```svelte
9
+ <script lang="ts">
10
+ import {
11
+ HttpStatusProvider,
12
+ createHttpStatusSink,
13
+ } from "@real-router/svelte/ssr";
14
+
15
+ const sink = createHttpStatusSink();
16
+ </script>
17
+
18
+ <HttpStatusProvider {sink}>
19
+ <App />
20
+ </HttpStatusProvider>
21
+ ```
22
+ -->
23
+ <script lang="ts">
24
+ import { setContext } from "svelte";
25
+
26
+ import { HTTP_STATUS_KEY } from "../context";
27
+
28
+ import type { HttpStatusSink } from "../utils/createHttpStatusSink";
29
+ import type { Snippet } from "svelte";
30
+
31
+ interface Props {
32
+ sink: HttpStatusSink;
33
+ children: Snippet;
34
+ }
35
+
36
+ let { sink, children }: Props = $props();
37
+
38
+ // svelte-ignore state_referenced_locally
39
+ // The sink reference is captured once at provider init — replacing the sink
40
+ // mid-render isn't a supported usage pattern (the server reads it once
41
+ // after `await render()`).
42
+ setContext(HTTP_STATUS_KEY, sink);
43
+ </script>
44
+
45
+ {@render children()}
@@ -0,0 +1,30 @@
1
+ import type { HttpStatusSink } from "../utils/createHttpStatusSink";
2
+ import type { Snippet } from "svelte";
3
+ interface Props {
4
+ sink: HttpStatusSink;
5
+ children: Snippet;
6
+ }
7
+ /**
8
+ * Wraps an SSR tree with a render-scoped `HttpStatusSink`. `<HttpStatusCode />`
9
+ * reads the sink via `getContext` and writes its `code` to it during component
10
+ * init. Read `sink.code` after `await render()` to set the HTTP response
11
+ * status.
12
+ *
13
+ * ```svelte
14
+ * <script lang="ts">
15
+ * import {
16
+ * HttpStatusProvider,
17
+ * createHttpStatusSink,
18
+ * } from "@real-router/svelte/ssr";
19
+ *
20
+ * const sink = createHttpStatusSink();
21
+ * </script>
22
+ *
23
+ * <HttpStatusProvider {sink}>
24
+ * <App />
25
+ * </HttpStatusProvider>
26
+ * ```
27
+ */
28
+ declare const HttpStatusProvider: import("svelte").Component<Props, {}, "">;
29
+ type HttpStatusProvider = ReturnType<typeof HttpStatusProvider>;
30
+ export default HttpStatusProvider;
@@ -0,0 +1 @@
1
+ export declare function getActiveSegment(routeName: string, node: string, snippets: Record<string, unknown>): string;
@@ -0,0 +1,16 @@
1
+ import { startsWithSegment } from "@real-router/route-utils";
2
+ // Snippet names reserved by RouteView for non-segment slots. Iteration in
3
+ // `getActiveSegment` skips these so they don't accidentally match a route.
4
+ const RESERVED_SLOT_NAMES = new Set(["self", "notFound"]);
5
+ export function getActiveSegment(routeName, node, snippets) {
6
+ const prefix = node ? `${node}.` : "";
7
+ for (const segment in snippets) {
8
+ if (RESERVED_SLOT_NAMES.has(segment)) {
9
+ continue;
10
+ }
11
+ if (startsWithSegment(routeName, prefix + segment)) {
12
+ return segment;
13
+ }
14
+ }
15
+ return "";
16
+ }
@@ -1,32 +1,8 @@
1
- <script lang="ts" module>
2
- import { startsWithSegment } from "@real-router/route-utils";
3
-
4
- // Snippet names reserved by RouteView for non-segment slots. Iteration in
5
- // `getActiveSegment` skips these so they don't accidentally match a route.
6
- const RESERVED_SLOT_NAMES = new Set(["self", "notFound"]);
7
-
8
- export function getActiveSegment(
9
- routeName: string,
10
- node: string,
11
- snippets: Record<string, unknown>,
12
- ): string {
13
- const prefix = node ? `${node}.` : "";
14
-
15
- for (const segment in snippets) {
16
- if (RESERVED_SLOT_NAMES.has(segment)) continue;
17
- if (startsWithSegment(routeName, prefix + segment)) {
18
- return segment;
19
- }
20
- }
21
-
22
- return "";
23
- }
24
- </script>
25
-
26
1
  <script lang="ts">
27
2
  import { UNKNOWN_ROUTE } from "@real-router/core";
28
3
 
29
4
  import { useRouteNode } from "../composables/useRouteNode.svelte";
5
+ import { getActiveSegment } from "./RouteView.helpers";
30
6
 
31
7
  import type { Snippet } from "svelte";
32
8
 
@@ -1,4 +1,3 @@
1
- export declare function getActiveSegment(routeName: string, node: string, snippets: Record<string, unknown>): string;
2
1
  import type { Snippet } from "svelte";
3
2
  type $$ComponentProps = {
4
3
  nodeName: string;
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+
4
+ interface Props {
5
+ children: Snippet;
6
+ fallback?: Snippet;
7
+ }
8
+
9
+ let { children, fallback }: Props = $props();
10
+
11
+ let mounted = $state(false);
12
+
13
+ $effect(() => {
14
+ mounted = true;
15
+ });
16
+ </script>
17
+
18
+ {#if !mounted}
19
+ {@render children()}
20
+ {:else if fallback}
21
+ {@render fallback()}
22
+ {/if}
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from "svelte";
2
+ interface Props {
3
+ children: Snippet;
4
+ fallback?: Snippet;
5
+ }
6
+ declare const ServerOnly: import("svelte").Component<Props, {}, "">;
7
+ type ServerOnly = ReturnType<typeof ServerOnly>;
8
+ export default ServerOnly;
@@ -0,0 +1,37 @@
1
+ <!--
2
+ @component
3
+ Cross-adapter alias for Svelte's `{#await}` boundary. Renders the `fallback`
4
+ snippet while a `pending` Promise prop is unresolved, then `children` (with
5
+ the resolved value) once it settles. Symmetric naming with the
6
+ React/Preact/Solid/Vue/Angular `<Streamed>` components — pick `<Streamed>`
7
+ for cross-framework consistency, or use `{#await}` directly when team
8
+ conventions prefer that.
9
+
10
+ Svelte 5 has **no progressive HTTP-flush** in SSR (one TCP frame, late-
11
+ resolving promises ship in the final body) — the `{#await}` block on the
12
+ client retains its native streaming-after-hydration semantics. See
13
+ `examples/web/svelte/ssr-examples/ssr-streaming/README.md` for the
14
+ end-to-end story.
15
+ -->
16
+ <script lang="ts" generics="T">
17
+ import type { Snippet } from "svelte";
18
+
19
+ interface Props {
20
+ /** Promise to await — typically `useDeferred(key)`. */
21
+ pending: Promise<T>;
22
+ /** Render snippet for the resolved value. */
23
+ children: Snippet<[T]>;
24
+ /** Snippet shown while the promise is pending. */
25
+ fallback?: Snippet;
26
+ }
27
+
28
+ let { pending, children, fallback }: Props = $props();
29
+ </script>
30
+
31
+ {#await pending}
32
+ {#if fallback}
33
+ {@render fallback()}
34
+ {/if}
35
+ {:then value}
36
+ {@render children(value)}
37
+ {/await}
@@ -0,0 +1,46 @@
1
+ import type { Snippet } from "svelte";
2
+ declare function $$render<T>(): {
3
+ props: {
4
+ /** Promise to await — typically `useDeferred(key)`. */
5
+ pending: Promise<T>;
6
+ /** Render snippet for the resolved value. */
7
+ children: Snippet<[T]>;
8
+ /** Snippet shown while the promise is pending. */
9
+ fallback?: Snippet;
10
+ };
11
+ exports: {};
12
+ bindings: "";
13
+ slots: {};
14
+ events: {};
15
+ };
16
+ declare class __sveltets_Render<T> {
17
+ props(): ReturnType<typeof $$render<T>>['props'];
18
+ events(): ReturnType<typeof $$render<T>>['events'];
19
+ slots(): ReturnType<typeof $$render<T>>['slots'];
20
+ bindings(): "";
21
+ exports(): {};
22
+ }
23
+ interface $$IsomorphicComponent {
24
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
25
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
26
+ } & ReturnType<__sveltets_Render<T>['exports']>;
27
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
28
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
29
+ }
30
+ /**
31
+ * Cross-adapter alias for Svelte's `{#await}` boundary. Renders the `fallback`
32
+ * snippet while a `pending` Promise prop is unresolved, then `children` (with
33
+ * the resolved value) once it settles. Symmetric naming with the
34
+ * React/Preact/Solid/Vue/Angular `<Streamed>` components — pick `<Streamed>`
35
+ * for cross-framework consistency, or use `{#await}` directly when team
36
+ * conventions prefer that.
37
+ *
38
+ * Svelte 5 has **no progressive HTTP-flush** in SSR (one TCP frame, late-
39
+ * resolving promises ship in the final body) — the `{#await}` block on the
40
+ * client retains its native streaming-after-hydration semantics. See
41
+ * `examples/web/svelte/ssr-examples/ssr-streaming/README.md` for the
42
+ * end-to-end story.
43
+ */
44
+ declare const Streamed: $$IsomorphicComponent;
45
+ type Streamed<T> = InstanceType<typeof Streamed<T>>;
46
+ export default Streamed;