@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
@@ -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
+ }
@@ -11,7 +11,7 @@ export function useIsActiveRoute(
11
11
  strict: boolean,
12
12
  ignoreQueryParams: boolean,
13
13
  hash?: string,
14
- ): { readonly current: boolean } {
14
+ ) {
15
15
  const router = useRouter();
16
16
 
17
17
  // The `hash` argument (#532) participates in the cache key when defined.
@@ -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
- export const useRoute = <P extends Params = Params>(): Omit<
7
- RouteContext<P>,
8
- "route"
9
- > & { route: { readonly current: State<P> } } => {
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 Omit<RouteContext<P>, "route"> & {
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
+ }