@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.
- package/dist/RouterProvider.svelte +8 -0
- package/dist/components/Link.svelte +2 -0
- package/dist/components/RouteView.svelte +2 -0
- package/dist/dom-utils/link-utils.js +3 -3
- package/dist/dom-utils/scroll-spy.js +1 -0
- package/package.json +11 -11
- package/dist/dom-utils/__test-helpers/expected-fragment.d.ts +0 -30
- package/dist/dom-utils/__test-helpers/expected-fragment.js +0 -43
- package/dist/dom-utils/__test-helpers/index.d.ts +0 -8
- package/dist/dom-utils/__test-helpers/index.js +0 -8
- package/src/RouterProvider.svelte +0 -112
- package/src/actions/link.svelte.ts +0 -88
- package/src/components/Await.svelte +0 -48
- package/src/components/ClientOnly.svelte +0 -22
- package/src/components/HttpStatusCode.svelte +0 -63
- package/src/components/HttpStatusProvider.svelte +0 -45
- package/src/components/Lazy.svelte +0 -55
- package/src/components/Link.svelte +0 -95
- package/src/components/RouteView.helpers.ts +0 -24
- package/src/components/RouteView.svelte +0 -35
- package/src/components/RouterErrorBoundary.svelte +0 -47
- package/src/components/ServerOnly.svelte +0 -22
- package/src/components/Streamed.svelte +0 -37
- package/src/composables/useDeferred.svelte.ts +0 -41
- package/src/composables/useIsActiveRoute.svelte.ts +0 -30
- package/src/composables/useNavigator.svelte.ts +0 -6
- package/src/composables/useRoute.svelte.ts +0 -25
- package/src/composables/useRouteEnter.svelte.ts +0 -120
- package/src/composables/useRouteExit.svelte.ts +0 -113
- package/src/composables/useRouteNode.svelte.ts +0 -18
- package/src/composables/useRouteUtils.svelte.ts +0 -12
- package/src/composables/useRouter.svelte.ts +0 -6
- package/src/composables/useRouterTransition.svelte.ts +0 -15
- package/src/constants.ts +0 -7
- package/src/context.ts +0 -24
- package/src/createReactiveSource.svelte.ts +0 -21
- package/src/createRouteContext.svelte.ts +0 -27
- package/src/index.ts +0 -60
- package/src/ssr.ts +0 -28
- package/src/types.ts +0 -46
- 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,
|
|
@@ -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.
|
|
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.
|
|
53
|
-
"@real-router/route-utils": "^0.2.
|
|
54
|
-
"@real-router/sources": "^0.8.
|
|
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.
|
|
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.
|
|
65
|
-
"svelte-eslint-parser": "1.
|
|
66
|
-
"@real-router/browser-plugin": "^0.17.
|
|
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}
|