@real-router/svelte 0.10.1 → 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.
- package/README.md +63 -11
- package/dist/RouterProvider.svelte +12 -3
- package/dist/components/Await.svelte +48 -0
- package/dist/components/Await.svelte.d.ts +50 -0
- package/dist/components/ClientOnly.svelte +22 -0
- package/dist/components/ClientOnly.svelte.d.ts +8 -0
- package/dist/components/HttpStatusCode.svelte +63 -0
- package/dist/components/HttpStatusCode.svelte.d.ts +45 -0
- package/dist/components/HttpStatusProvider.svelte +45 -0
- package/dist/components/HttpStatusProvider.svelte.d.ts +30 -0
- package/dist/components/RouteView.helpers.d.ts +1 -0
- package/dist/components/RouteView.helpers.js +16 -0
- package/dist/components/RouteView.svelte +1 -25
- package/dist/components/RouteView.svelte.d.ts +0 -1
- package/dist/components/ServerOnly.svelte +22 -0
- package/dist/components/ServerOnly.svelte.d.ts +8 -0
- package/dist/components/Streamed.svelte +37 -0
- package/dist/components/Streamed.svelte.d.ts +46 -0
- package/dist/composables/useDeferred.svelte.d.ts +24 -0
- package/dist/composables/useDeferred.svelte.js +34 -0
- package/dist/composables/useRoute.svelte.d.ts +8 -1
- package/dist/context.d.ts +1 -0
- package/dist/context.js +1 -0
- package/dist/dom-utils/__test-helpers/expected-fragment.d.ts +30 -0
- package/dist/dom-utils/__test-helpers/expected-fragment.js +43 -0
- package/dist/dom-utils/__test-helpers/index.d.ts +8 -0
- package/dist/dom-utils/__test-helpers/index.js +8 -0
- package/dist/dom-utils/link-utils.d.ts +23 -0
- package/dist/dom-utils/link-utils.js +106 -5
- package/dist/dom-utils/route-announcer.js +51 -2
- package/dist/dom-utils/scroll-restore.d.ts +38 -1
- package/dist/dom-utils/scroll-restore.js +144 -12
- package/dist/ssr.d.ts +9 -0
- package/dist/ssr.js +17 -0
- package/dist/types.d.ts +23 -0
- package/dist/utils/createHttpStatusSink.d.ts +28 -0
- package/dist/utils/createHttpStatusSink.js +3 -0
- package/package.json +10 -5
- package/src/RouterProvider.svelte +12 -3
- package/src/components/Await.svelte +48 -0
- package/src/components/ClientOnly.svelte +22 -0
- package/src/components/HttpStatusCode.svelte +63 -0
- package/src/components/HttpStatusProvider.svelte +45 -0
- package/src/components/RouteView.helpers.ts +24 -0
- package/src/components/RouteView.svelte +1 -25
- package/src/components/ServerOnly.svelte +22 -0
- package/src/components/Streamed.svelte +37 -0
- package/src/composables/useDeferred.svelte.ts +41 -0
- package/src/composables/useIsActiveRoute.svelte.ts +1 -1
- package/src/composables/useRoute.svelte.ts +11 -7
- package/src/context.ts +2 -0
- package/src/ssr.ts +28 -0
- package/src/types.ts +23 -0
- 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
|
|
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
|
-
| `
|
|
246
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
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,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
|
|
|
@@ -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,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;
|