@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.
- 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
|
@@ -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,41 @@
|
|
|
1
|
+
import { useRoute } from "./useRoute.svelte";
|
|
2
|
+
|
|
3
|
+
interface DeferredContext {
|
|
4
|
+
ssrDataDeferred?: Record<string, Promise<unknown>>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const NEVER_PROMISE = new Promise<never>(() => {
|
|
8
|
+
// Intentionally never resolves — surfaces a forever-pending {#await} block
|
|
9
|
+
// when a key is requested that the loader never declared.
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read a deferred promise published by `defer({ deferred: { <key>: Promise } })`
|
|
14
|
+
* inside an SSR data loader. Returns the Promise — feed straight into Svelte's
|
|
15
|
+
* native `{#await}` block, or use `<Await name="key">` (this package) for the
|
|
16
|
+
* cross-adapter shape.
|
|
17
|
+
*
|
|
18
|
+
* ```svelte
|
|
19
|
+
* <script>
|
|
20
|
+
* import { useDeferred } from "@real-router/svelte/ssr";
|
|
21
|
+
* const reviewsPromise = useDeferred("reviews");
|
|
22
|
+
* </script>
|
|
23
|
+
*
|
|
24
|
+
* {#await reviewsPromise}
|
|
25
|
+
* <Spinner />
|
|
26
|
+
* {:then reviews}
|
|
27
|
+
* <ReviewList items={reviews} />
|
|
28
|
+
* {/await}
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Returns a forever-pending promise when the key is missing — surfaces
|
|
32
|
+
* loader/consumer key drift as a visible {#await} loading state rather than
|
|
33
|
+
* a silent runtime error.
|
|
34
|
+
*/
|
|
35
|
+
export function useDeferred<T = unknown>(key: string): Promise<T> {
|
|
36
|
+
const { route } = useRoute();
|
|
37
|
+
const context = route.current.context as DeferredContext;
|
|
38
|
+
const deferred = context.ssrDataDeferred;
|
|
39
|
+
|
|
40
|
+
return (deferred?.[key] ?? NEVER_PROMISE) as Promise<T>;
|
|
41
|
+
}
|
|
@@ -3,10 +3,16 @@ import { ROUTE_KEY, getContextOrThrow } from "../context";
|
|
|
3
3
|
import type { RouteContext } from "../types";
|
|
4
4
|
import type { Params, State } from "@real-router/core";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
/**
|
|
7
|
+
* `useRoute()`'s return type: same shape as `RouteContext<P>` but with
|
|
8
|
+
* `route.current` narrowed to non-nullable `State<P>` (the composable's
|
|
9
|
+
* `if (!ctx.route.current) throw` guard makes this safe).
|
|
10
|
+
*/
|
|
11
|
+
type ActiveRouteContext<P extends Params> = Omit<RouteContext<P>, "route"> & {
|
|
12
|
+
route: { readonly current: State<P> };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const useRoute = <P extends Params = Params>(): ActiveRouteContext<P> => {
|
|
10
16
|
const ctx = getContextOrThrow<RouteContext>(ROUTE_KEY, "useRoute");
|
|
11
17
|
|
|
12
18
|
if (!ctx.route.current) {
|
|
@@ -15,7 +21,5 @@ export const useRoute = <P extends Params = Params>(): Omit<
|
|
|
15
21
|
);
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
return ctx as
|
|
19
|
-
route: { readonly current: State<P> };
|
|
20
|
-
};
|
|
24
|
+
return ctx as ActiveRouteContext<P>;
|
|
21
25
|
};
|
package/src/context.ts
CHANGED
|
@@ -6,6 +6,8 @@ export const NAVIGATOR_KEY = "real-router:navigator";
|
|
|
6
6
|
|
|
7
7
|
export const ROUTE_KEY = "real-router:route";
|
|
8
8
|
|
|
9
|
+
export const HTTP_STATUS_KEY = "real-router:http-status-sink";
|
|
10
|
+
|
|
9
11
|
// The type parameter is used by the caller to narrow the return type.
|
|
10
12
|
// ESLint's no-unnecessary-type-parameters sees only a single textual use of T
|
|
11
13
|
// (the return type) — but each call site supplies a different T, so it is not
|
package/src/ssr.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SSR-feature entry — Svelte 5+
|
|
2
|
+
//
|
|
3
|
+
// Server-side and SSR-aware components/composables. Mirror of `@real-router/react/ssr`
|
|
4
|
+
// — same exports, Svelte-native idioms (`{#await}` block under the hood,
|
|
5
|
+
// `$state` rune for ClientOnly/ServerOnly, useDeferred returns Promise<T>
|
|
6
|
+
// for direct use with native `{#await}`).
|
|
7
|
+
|
|
8
|
+
// Components
|
|
9
|
+
export { default as ClientOnly } from "./components/ClientOnly.svelte";
|
|
10
|
+
|
|
11
|
+
export { default as ServerOnly } from "./components/ServerOnly.svelte";
|
|
12
|
+
|
|
13
|
+
export { default as Await } from "./components/Await.svelte";
|
|
14
|
+
|
|
15
|
+
export { default as Streamed } from "./components/Streamed.svelte";
|
|
16
|
+
|
|
17
|
+
export { default as HttpStatusCode } from "./components/HttpStatusCode.svelte";
|
|
18
|
+
|
|
19
|
+
export { default as HttpStatusProvider } from "./components/HttpStatusProvider.svelte";
|
|
20
|
+
|
|
21
|
+
// Composables
|
|
22
|
+
export { useDeferred } from "./composables/useDeferred.svelte";
|
|
23
|
+
|
|
24
|
+
// Utilities
|
|
25
|
+
export { createHttpStatusSink } from "./utils/createHttpStatusSink";
|
|
26
|
+
|
|
27
|
+
// Types
|
|
28
|
+
export type { HttpStatusSink } from "./utils/createHttpStatusSink";
|
package/src/types.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
Params,
|
|
5
5
|
State,
|
|
6
6
|
} from "@real-router/core";
|
|
7
|
+
import type { Snippet } from "svelte";
|
|
7
8
|
|
|
8
9
|
export interface RouteContext<P extends Params = Params> {
|
|
9
10
|
readonly navigator: Navigator;
|
|
@@ -11,7 +12,18 @@ export interface RouteContext<P extends Params = Params> {
|
|
|
11
12
|
readonly previousRoute: { readonly current: State | undefined };
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Props accepted by `<Link>`. Mirrors the inline prop shape in
|
|
17
|
+
* `src/components/Link.svelte` — any prop landed by `Link.svelte` is also
|
|
18
|
+
* declared here, including the rest-props index signature for arbitrary
|
|
19
|
+
* HTML attributes spread onto the rendered `<a>`.
|
|
20
|
+
*/
|
|
14
21
|
export interface LinkProps<P extends Params = Params> {
|
|
22
|
+
/**
|
|
23
|
+
* All other props are spread onto the rendered `<a>` element. Use this for
|
|
24
|
+
* `aria-*`, `data-*`, `id`, `title`, and any other native attributes.
|
|
25
|
+
*/
|
|
26
|
+
readonly [key: string]: unknown;
|
|
15
27
|
readonly routeName: string;
|
|
16
28
|
readonly routeParams?: P;
|
|
17
29
|
readonly routeOptions?: NavigationOptions;
|
|
@@ -19,5 +31,16 @@ export interface LinkProps<P extends Params = Params> {
|
|
|
19
31
|
readonly activeClassName?: string;
|
|
20
32
|
readonly activeStrict?: boolean;
|
|
21
33
|
readonly ignoreQueryParams?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* URL fragment (decoded, no leading "#") — #532.
|
|
36
|
+
* - `undefined` → preserve current `state.context.url.hash` on click.
|
|
37
|
+
* - `""` → clear the hash.
|
|
38
|
+
* - `"value"` → set the hash; same-route different-hash clicks route through
|
|
39
|
+
* `navigateWithHash`, which adds `force: true, hashChange: true` to
|
|
40
|
+
* bypass core's SAME_STATES check.
|
|
41
|
+
*/
|
|
42
|
+
readonly hash?: string;
|
|
22
43
|
readonly target?: string;
|
|
44
|
+
readonly children?: Snippet;
|
|
45
|
+
readonly onclick?: (evt: MouseEvent) => void;
|
|
23
46
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render-scoped HTTP status sink. Created per request on the server, passed to
|
|
3
|
+
* `<HttpStatusProvider sink={...}>`, and read after `await render()` from
|
|
4
|
+
* `svelte/server` to apply the value to the HTTP response.
|
|
5
|
+
*
|
|
6
|
+
* Last write wins: if the rendered tree mounts more than one
|
|
7
|
+
* `<HttpStatusCode />`, the value reflects the last component that ran during
|
|
8
|
+
* the render pass.
|
|
9
|
+
*
|
|
10
|
+
* No-op on the client — `<HttpStatusCode />` reads the optional injected sink
|
|
11
|
+
* and skips the write when no provider is mounted, so the same component tree
|
|
12
|
+
* can be hydrated without changing behaviour.
|
|
13
|
+
*
|
|
14
|
+
* Constraints:
|
|
15
|
+
* - **Per-request only.** Don't share a sink across requests; the rendered
|
|
16
|
+
* tree mutates `code` in place. Module-level singletons leak status
|
|
17
|
+
* between concurrent requests.
|
|
18
|
+
* - **Don't `Object.freeze` the sink.** The component writes to `.code`;
|
|
19
|
+
* freezing makes the assignment throw under ESM strict mode.
|
|
20
|
+
* - **Hydration is tolerant.** Svelte 5's hydration walker accepts
|
|
21
|
+
* `{#if}`-branch asymmetry between server and client (verified by `ssr/`
|
|
22
|
+
* e2e), so the example app uses a server-only provider wrapper. This
|
|
23
|
+
* contrasts with Vue/Solid, which require symmetric provider mounting.
|
|
24
|
+
*/
|
|
25
|
+
export interface HttpStatusSink {
|
|
26
|
+
code: number | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createHttpStatusSink(): HttpStatusSink {
|
|
30
|
+
return { code: undefined };
|
|
31
|
+
}
|