@real-router/svelte 0.13.1 → 0.13.3

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 (41) hide show
  1. package/dist/RouterProvider.svelte +8 -0
  2. package/dist/components/Link.svelte +2 -0
  3. package/dist/components/RouteView.svelte +2 -0
  4. package/dist/dom-utils/link-utils.js +3 -3
  5. package/dist/dom-utils/scroll-spy.js +1 -0
  6. package/package.json +11 -11
  7. package/dist/dom-utils/__test-helpers/expected-fragment.d.ts +0 -30
  8. package/dist/dom-utils/__test-helpers/expected-fragment.js +0 -43
  9. package/dist/dom-utils/__test-helpers/index.d.ts +0 -8
  10. package/dist/dom-utils/__test-helpers/index.js +0 -8
  11. package/src/RouterProvider.svelte +0 -112
  12. package/src/actions/link.svelte.ts +0 -88
  13. package/src/components/Await.svelte +0 -48
  14. package/src/components/ClientOnly.svelte +0 -22
  15. package/src/components/HttpStatusCode.svelte +0 -63
  16. package/src/components/HttpStatusProvider.svelte +0 -45
  17. package/src/components/Lazy.svelte +0 -55
  18. package/src/components/Link.svelte +0 -95
  19. package/src/components/RouteView.helpers.ts +0 -24
  20. package/src/components/RouteView.svelte +0 -35
  21. package/src/components/RouterErrorBoundary.svelte +0 -47
  22. package/src/components/ServerOnly.svelte +0 -22
  23. package/src/components/Streamed.svelte +0 -37
  24. package/src/composables/useDeferred.svelte.ts +0 -41
  25. package/src/composables/useIsActiveRoute.svelte.ts +0 -30
  26. package/src/composables/useNavigator.svelte.ts +0 -6
  27. package/src/composables/useRoute.svelte.ts +0 -25
  28. package/src/composables/useRouteEnter.svelte.ts +0 -120
  29. package/src/composables/useRouteExit.svelte.ts +0 -113
  30. package/src/composables/useRouteNode.svelte.ts +0 -18
  31. package/src/composables/useRouteUtils.svelte.ts +0 -12
  32. package/src/composables/useRouter.svelte.ts +0 -6
  33. package/src/composables/useRouterTransition.svelte.ts +0 -15
  34. package/src/constants.ts +0 -7
  35. package/src/context.ts +0 -24
  36. package/src/createReactiveSource.svelte.ts +0 -21
  37. package/src/createRouteContext.svelte.ts +0 -27
  38. package/src/index.ts +0 -60
  39. package/src/ssr.ts +0 -28
  40. package/src/types.ts +0 -46
  41. package/src/utils/createHttpStatusSink.ts +0 -31
@@ -99,12 +99,20 @@
99
99
  return () => vt.destroy();
100
100
  });
101
101
 
102
+ // svelte-ignore state_referenced_locally
103
+ // The router instance is stable for the provider lifetime.
102
104
  const navigator = getNavigator(router);
105
+ // svelte-ignore state_referenced_locally
106
+ // The route source intentionally captures the initial router instance.
103
107
  const source = createRouteSource(router);
104
108
  const reactive = createReactiveSource(source);
105
109
  const routeContext = createRouteContext(navigator, reactive);
106
110
 
111
+ // svelte-ignore state_referenced_locally
112
+ // Context exposes the same stable router instance for this provider.
107
113
  setContext(ROUTER_KEY, router);
114
+ // svelte-ignore state_referenced_locally
115
+ // Context exposes the navigator derived once from the stable router.
108
116
  setContext(NAVIGATOR_KEY, navigator);
109
117
  setContext(ROUTE_KEY, routeContext);
110
118
  </script>
@@ -49,6 +49,8 @@
49
49
  const router = useRouter();
50
50
  // Hash-aware active (#532): tab links sharing routeName but differing in
51
51
  // hash should only light up the matching variant.
52
+ // svelte-ignore state_referenced_locally
53
+ // Active-route state is captured at mount; href remains reactive separately.
52
54
  const activeState = useIsActiveRoute(
53
55
  routeName,
54
56
  routeParams,
@@ -18,6 +18,8 @@
18
18
  [key: string]: Snippet | string | undefined;
19
19
  } = $props();
20
20
 
21
+ // svelte-ignore state_referenced_locally
22
+ // The node source is selected once for this mounted RouteView.
21
23
  const routeContext = useRouteNode(nodeName);
22
24
  </script>
23
25
 
@@ -115,10 +115,10 @@ export function navigateWithHash(router, routeName, routeParams, hash, extraOpti
115
115
  // for the slow-path branch.
116
116
  const WHITESPACE_PROBE = /\s/;
117
117
  const WHITESPACE_SPLIT = /\S+/g;
118
+ // `value` is always a truthy class string: both call sites narrow it first
119
+ // (`if (isActive && activeClassName)` / `if (!baseClassName) return …`), so the
120
+ // former `if (!value) return []` guard was unreachable dead code (#809).
118
121
  function parseTokens(value) {
119
- if (!value) {
120
- return [];
121
- }
122
122
  // Hot-path fast-path (audit-2026-05-17 §8b #1): >99% of active-class
123
123
  // inputs at `<Link>` emit are single-token strings like `"active"` or
124
124
  // `"is-current"` — no whitespace, no leading/trailing pad. Skip the
@@ -69,6 +69,7 @@ const createUrlPluginDetector = (router, onMissing) => {
69
69
  // boolean, a hypothetical multi-fire would double-warn.
70
70
  let detectionConsumed = false;
71
71
  detectionUnsub = router.subscribe(({ route }) => {
72
+ /* v8 ignore next 3 -- @preserve: the multi-fire is hypothetical (see above) — the real router never invokes a subscriber synchronously twice before unsub; defensive guard, not testable without a contract-violating fake */
72
73
  if (detectionConsumed) {
73
74
  return;
74
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/svelte",
3
- "version": "0.13.1",
3
+ "version": "0.13.3",
4
4
  "type": "module",
5
5
  "description": "Svelte 5 integration for Real-Router",
6
6
  "svelte": "./dist/index.js",
@@ -18,8 +18,7 @@
18
18
  }
19
19
  },
20
20
  "files": [
21
- "dist",
22
- "src"
21
+ "dist"
23
22
  ],
24
23
  "homepage": "https://github.com/greydragon888/real-router",
25
24
  "repository": {
@@ -49,21 +48,22 @@
49
48
  "license": "MIT",
50
49
  "sideEffects": false,
51
50
  "dependencies": {
52
- "@real-router/core": "^0.55.0",
53
- "@real-router/route-utils": "^0.2.2",
54
- "@real-router/sources": "^0.8.4"
51
+ "@real-router/core": "^0.56.0",
52
+ "@real-router/route-utils": "^0.2.3",
53
+ "@real-router/sources": "^0.8.5"
55
54
  },
56
55
  "devDependencies": {
57
- "@sveltejs/package": "2.5.7",
56
+ "@sveltejs/package": "2.5.8",
58
57
  "@sveltejs/vite-plugin-svelte": "6.2.4",
59
58
  "@testing-library/jest-dom": "6.9.1",
60
59
  "@testing-library/svelte": "5.3.1",
61
60
  "@testing-library/user-event": "14.6.1",
62
61
  "eslint-plugin-svelte": "3.19.0",
62
+ "rimraf": "6.1.3",
63
63
  "svelte": "5.56.1",
64
- "svelte-check": "4.5.0",
65
- "svelte-eslint-parser": "1.7.1",
66
- "@real-router/browser-plugin": "^0.17.5"
64
+ "svelte-check": "4.6.0",
65
+ "svelte-eslint-parser": "1.8.0",
66
+ "@real-router/browser-plugin": "^0.17.6"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "svelte": ">=5.7.0"
@@ -74,6 +74,6 @@
74
74
  "test:stress": "vitest run --config vitest.config.stress.mts",
75
75
  "type-check": "svelte-check --tsconfig ./tsconfig.json",
76
76
  "lint": "eslint --cache src/ tests/ --fix --max-warnings 0",
77
- "bundle": "svelte-package -i src -o dist"
77
+ "bundle": "svelte-package -i src -o dist && rimraf dist/dom-utils/__test-helpers dist/dom-utils/CLAUDE.md"
78
78
  }
79
79
  }
@@ -1,30 +0,0 @@
1
- /**
2
- * Test helper: mirror of `encodeFragmentInline` from `link-utils.ts` used by
3
- * property tests in every adapter (`packages/{vue,preact,react,solid}/tests/
4
- * property/linkUtils.properties.ts` and `packages/svelte/tests/property/
5
- * buildHref.properties.ts`).
6
- *
7
- * Why a mirror, not an import of the real function: a property test that uses
8
- * the production implementation to compute its own expected value is a
9
- * tautology — any regression in `encodeFragmentInline` would be invisible.
10
- * We hand-roll the same algorithm so the test asserts an independent
11
- * derivation. Drift between this mirror and `encodeFragmentInline` is exactly
12
- * the signal property tests are supposed to surface.
13
- *
14
- * This file lives under `__test-helpers/` so the sync-dom-utils script can
15
- * exclude it from the Angular copy (it ships no production code), and so
16
- * bundlers (tsdown, svelte-package) tree-shake it out — nothing in
17
- * `src/index.ts` of any adapter imports from this directory.
18
- */
19
- /**
20
- * Compute the expected fragment portion of an href for a given raw hash input.
21
- *
22
- * Mirrors the contract of `encodeFragmentInline`:
23
- * 1. If input contains `%XX`, try `decodeURIComponent` → `encodeURI` (idempotent
24
- * re-encoding so consumers can copy-paste `location.hash` back in without
25
- * `%20` becoming `%2520`).
26
- * 2. If `decodeURIComponent` throws (malformed `%XX`), fall through to plain
27
- * `encodeURI` on the original input.
28
- * 3. Defensive `#` → `%23` (encodeURI does not encode `#`).
29
- */
30
- export declare function computeExpectedFragment(rawHash: string): string;
@@ -1,43 +0,0 @@
1
- /**
2
- * Test helper: mirror of `encodeFragmentInline` from `link-utils.ts` used by
3
- * property tests in every adapter (`packages/{vue,preact,react,solid}/tests/
4
- * property/linkUtils.properties.ts` and `packages/svelte/tests/property/
5
- * buildHref.properties.ts`).
6
- *
7
- * Why a mirror, not an import of the real function: a property test that uses
8
- * the production implementation to compute its own expected value is a
9
- * tautology — any regression in `encodeFragmentInline` would be invisible.
10
- * We hand-roll the same algorithm so the test asserts an independent
11
- * derivation. Drift between this mirror and `encodeFragmentInline` is exactly
12
- * the signal property tests are supposed to surface.
13
- *
14
- * This file lives under `__test-helpers/` so the sync-dom-utils script can
15
- * exclude it from the Angular copy (it ships no production code), and so
16
- * bundlers (tsdown, svelte-package) tree-shake it out — nothing in
17
- * `src/index.ts` of any adapter imports from this directory.
18
- */
19
- const PERCENT_ESCAPE_PROBE = /%[\dA-Fa-f]{2}/;
20
- /**
21
- * Compute the expected fragment portion of an href for a given raw hash input.
22
- *
23
- * Mirrors the contract of `encodeFragmentInline`:
24
- * 1. If input contains `%XX`, try `decodeURIComponent` → `encodeURI` (idempotent
25
- * re-encoding so consumers can copy-paste `location.hash` back in without
26
- * `%20` becoming `%2520`).
27
- * 2. If `decodeURIComponent` throws (malformed `%XX`), fall through to plain
28
- * `encodeURI` on the original input.
29
- * 3. Defensive `#` → `%23` (encodeURI does not encode `#`).
30
- */
31
- export function computeExpectedFragment(rawHash) {
32
- let roundtrip = rawHash;
33
- if (PERCENT_ESCAPE_PROBE.test(rawHash)) {
34
- try {
35
- roundtrip = decodeURIComponent(rawHash);
36
- }
37
- catch {
38
- // Malformed %XX — encodeFragmentInline falls through to plain
39
- // encodeURI on the original input, so do the same here.
40
- }
41
- }
42
- return encodeURI(roundtrip).replaceAll("#", "%23");
43
- }
@@ -1,8 +0,0 @@
1
- /**
2
- * Test-only barrel. Intentionally NOT re-exported from
3
- * `shared/dom-utils/index.ts` so the helpers don't leak into adapters'
4
- * public API surface. Property tests import from this path directly:
5
- *
6
- * import { computeExpectedFragment } from "../../src/dom-utils/__test-helpers";
7
- */
8
- export { computeExpectedFragment } from "./expected-fragment.js";
@@ -1,8 +0,0 @@
1
- /**
2
- * Test-only barrel. Intentionally NOT re-exported from
3
- * `shared/dom-utils/index.ts` so the helpers don't leak into adapters'
4
- * public API surface. Property tests import from this path directly:
5
- *
6
- * import { computeExpectedFragment } from "../../src/dom-utils/__test-helpers";
7
- */
8
- export { computeExpectedFragment } from "./expected-fragment.js";
@@ -1,112 +0,0 @@
1
- <script lang="ts">
2
- import { getNavigator } from "@real-router/core";
3
- import { createRouteSource } from "@real-router/sources";
4
- import {
5
- createRouteAnnouncer,
6
- createScrollRestoration,
7
- createScrollSpy,
8
- createViewTransitions,
9
- } from "./dom-utils";
10
- import { setContext, untrack } from "svelte";
11
-
12
- import { createReactiveSource } from "./createReactiveSource.svelte";
13
- import { createRouteContext } from "./createRouteContext.svelte";
14
- import { NAVIGATOR_KEY, ROUTE_KEY, ROUTER_KEY } from "./context";
15
-
16
- import type { ScrollRestorationOptions, ScrollSpyOptions } from "./dom-utils";
17
- import type { Router } from "@real-router/core";
18
- import type { Snippet } from "svelte";
19
-
20
- let {
21
- router,
22
- children,
23
- announceNavigation,
24
- scrollRestoration,
25
- scrollSpy,
26
- viewTransitions,
27
- }: {
28
- router: Router;
29
- children: Snippet;
30
- announceNavigation?: boolean | undefined;
31
- scrollRestoration?: ScrollRestorationOptions | undefined;
32
- scrollSpy?: ScrollSpyOptions | undefined;
33
- viewTransitions?: boolean | undefined;
34
- } = $props();
35
-
36
- $effect(() => {
37
- if (!announceNavigation) return;
38
- const announcer = createRouteAnnouncer(router);
39
- return () => announcer.destroy();
40
- });
41
-
42
- // $derived memoizes by === so inline `{ mode: "restore" }` doesn't thrash:
43
- // each parent re-render produces a new object ref, but .mode stays "restore"
44
- // → $derived returns the same primitive → $effect doesn't re-run.
45
- const srEnabled = $derived(scrollRestoration !== undefined);
46
- const srMode = $derived(scrollRestoration?.mode);
47
- const srAnchor = $derived(scrollRestoration?.anchorScrolling);
48
- const srBehavior = $derived(scrollRestoration?.behavior);
49
- const srStorageKey = $derived(scrollRestoration?.storageKey);
50
-
51
- $effect(() => {
52
- if (!srEnabled) return;
53
- // Pin primitive $derived deps as explicit dependencies of this effect
54
- // BEFORE constructing the utility. The four `void srX` reads make
55
- // intent unambiguous: even if `createScrollRestoration` throws after
56
- // partial argument evaluation (e.g. invalid `mode` rejected), every
57
- // srMode/srAnchor/srBehavior/srStorageKey is already in this effect's
58
- // dependency set — the next change to any of them re-runs the effect
59
- // and the utility gets rebuilt. Without these reads, the dependency
60
- // tracking would depend on Svelte's argument-evaluation order inside
61
- // the factory call, which is brittle. Non-primitive refs (like
62
- // `scrollContainer` — a DOM element that changes ref every render but
63
- // is identity-equal in practice) are deliberately read via `untrack`
64
- // to keep this effect from re-running on every parent re-render.
65
- void srMode;
66
- void srAnchor;
67
- void srBehavior;
68
- void srStorageKey;
69
- const sr = createScrollRestoration(router, {
70
- mode: srMode,
71
- anchorScrolling: srAnchor,
72
- behavior: srBehavior,
73
- storageKey: srStorageKey,
74
- scrollContainer: untrack(() => scrollRestoration?.scrollContainer),
75
- });
76
- return () => sr.destroy();
77
- });
78
-
79
- const spyEnabled = $derived(
80
- scrollSpy !== undefined && scrollSpy.selector !== "",
81
- );
82
- const spySelector = $derived(scrollSpy?.selector);
83
- const spyRootMargin = $derived(scrollSpy?.rootMargin);
84
-
85
- $effect(() => {
86
- if (!spyEnabled || !spySelector) return;
87
- void spyRootMargin;
88
- const spy = createScrollSpy(router, {
89
- selector: spySelector,
90
- rootMargin: spyRootMargin,
91
- scrollContainer: untrack(() => scrollSpy?.scrollContainer),
92
- });
93
- return () => spy.destroy();
94
- });
95
-
96
- $effect(() => {
97
- if (!viewTransitions) return;
98
- const vt = createViewTransitions(router);
99
- return () => vt.destroy();
100
- });
101
-
102
- const navigator = getNavigator(router);
103
- const source = createRouteSource(router);
104
- const reactive = createReactiveSource(source);
105
- const routeContext = createRouteContext(navigator, reactive);
106
-
107
- setContext(ROUTER_KEY, router);
108
- setContext(NAVIGATOR_KEY, navigator);
109
- setContext(ROUTE_KEY, routeContext);
110
- </script>
111
-
112
- {@render children()}
@@ -1,88 +0,0 @@
1
- import type { ActionReturn } from "svelte/action";
2
- import type { Router, Params, NavigationOptions } from "@real-router/core";
3
- import { ROUTER_KEY, getContextOrThrow } from "../context";
4
- import { EMPTY_OPTIONS, EMPTY_PARAMS, NOOP } from "../constants";
5
- import { shouldNavigate, applyLinkA11y } from "../dom-utils";
6
-
7
- export interface LinkActionParams {
8
- name: string;
9
- params?: Params;
10
- options?: NavigationOptions;
11
- }
12
-
13
- type LinkAction = (
14
- node: HTMLElement,
15
- params: LinkActionParams,
16
- ) => ActionReturn<LinkActionParams>;
17
-
18
- /**
19
- * Factory function that captures router context during component initialization.
20
- * Must be called during component init (not inside event handlers or effects).
21
- *
22
- * @returns Action function for use with `use:` directive
23
- * @throws Error if called outside RouterProvider
24
- *
25
- * @example
26
- * ```svelte
27
- * <script>
28
- * import { createLinkAction } from '@real-router/svelte';
29
- * const link = createLinkAction();
30
- * </script>
31
- *
32
- * <button use:link={{ name: 'home' }}>Home</button>
33
- * <a use:link={{ name: 'users', params: { id: '123' } }}>User Profile</a>
34
- * ```
35
- */
36
- export function createLinkAction(): LinkAction {
37
- const router = getContextOrThrow<Router>(ROUTER_KEY, "createLinkAction");
38
-
39
- return function link(
40
- node: HTMLElement,
41
- params: LinkActionParams,
42
- ): ActionReturn<LinkActionParams> {
43
- let currentParams = params;
44
-
45
- applyLinkA11y(node);
46
-
47
- function navigate() {
48
- router
49
- .navigate(
50
- currentParams.name,
51
- currentParams.params ?? EMPTY_PARAMS,
52
- currentParams.options ?? EMPTY_OPTIONS,
53
- )
54
- .catch(NOOP);
55
- }
56
-
57
- function handleClick(evt: MouseEvent) {
58
- if (!shouldNavigate(evt)) return;
59
- if (
60
- node instanceof HTMLAnchorElement &&
61
- node.getAttribute("target") === "_blank"
62
- ) {
63
- return;
64
- }
65
- evt.preventDefault();
66
- navigate();
67
- }
68
-
69
- function handleKeyDown(evt: KeyboardEvent) {
70
- if (evt.key === "Enter" && !(node instanceof HTMLButtonElement)) {
71
- navigate();
72
- }
73
- }
74
-
75
- node.addEventListener("click", handleClick);
76
- node.addEventListener("keydown", handleKeyDown);
77
-
78
- return {
79
- update(newParams: LinkActionParams) {
80
- currentParams = newParams;
81
- },
82
- destroy() {
83
- node.removeEventListener("click", handleClick);
84
- node.removeEventListener("keydown", handleKeyDown);
85
- },
86
- };
87
- };
88
- }
@@ -1,48 +0,0 @@
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}
@@ -1,22 +0,0 @@
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}
@@ -1,63 +0,0 @@
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>
@@ -1,45 +0,0 @@
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()}
@@ -1,55 +0,0 @@
1
- <script lang="ts">
2
- import type { Component } from "svelte";
3
-
4
- let {
5
- loader,
6
- fallback,
7
- }: {
8
- loader: () => Promise<{ default: Component }>;
9
- fallback?: Component | undefined;
10
- } = $props();
11
-
12
- type LazyState =
13
- | { status: "loading" }
14
- | { status: "ready"; component: Component }
15
- | { status: "error"; error: Error };
16
-
17
- let state = $state<LazyState>({ status: "loading" });
18
-
19
- $effect(() => {
20
- state = { status: "loading" };
21
- let active = true;
22
-
23
- loader()
24
- .then((module) => {
25
- if (!active) return;
26
- if (!module || typeof module.default === "undefined") {
27
- throw new Error(
28
- "[real-router] Lazy loader resolved without a `default` export.",
29
- );
30
- }
31
- state = { status: "ready", component: module.default };
32
- })
33
- .catch((err: unknown) => {
34
- if (!active) return;
35
- state = {
36
- status: "error",
37
- error: err instanceof Error ? err : new Error(String(err)),
38
- };
39
- });
40
-
41
- return () => {
42
- active = false;
43
- };
44
- });
45
- </script>
46
-
47
- {#if state.status === "loading" && fallback}
48
- {@const Fallback = fallback}
49
- <Fallback />
50
- {:else if state.status === "error"}
51
- <p>Error loading component: {state.error.message}</p>
52
- {:else if state.status === "ready"}
53
- {@const LoadedComponent = state.component}
54
- <LoadedComponent />
55
- {/if}