cross-router-svelte 1.0.1
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/LICENSE.md +7 -0
- package/dist/Form.svelte +81 -0
- package/dist/Form.svelte.d.ts +10 -0
- package/dist/Link.svelte +102 -0
- package/dist/Link.svelte.d.ts +14 -0
- package/dist/RouterView.svelte +147 -0
- package/dist/RouterView.svelte.d.ts +11 -0
- package/dist/context.svelte.d.ts +21 -0
- package/dist/context.svelte.js +46 -0
- package/dist/hooks.d.ts +12 -0
- package/dist/hooks.js +116 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +43 -0
- package/dist/routes.d.ts +25 -0
- package/dist/routes.js +32 -0
- package/dist/svelte-env.d.ts +6 -0
- package/package.json +36 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2026 Bricklou
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/dist/Form.svelte
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang='ts'>
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import type { HTMLFormAttributes } from 'svelte/elements'
|
|
4
|
+
import { getRouter } from './context.svelte'
|
|
5
|
+
|
|
6
|
+
// ============================================================
|
|
7
|
+
// PROPS
|
|
8
|
+
// ============================================================
|
|
9
|
+
|
|
10
|
+
interface Props extends HTMLFormAttributes {
|
|
11
|
+
// Called before submission — return false to cancel
|
|
12
|
+
onBeforeSubmit?: (formData: FormData) => boolean | Promise<boolean>
|
|
13
|
+
|
|
14
|
+
// Called after submission completes (success or error)
|
|
15
|
+
onAfterSubmit?: () => void
|
|
16
|
+
|
|
17
|
+
children: Snippet
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
action,
|
|
22
|
+
class: className,
|
|
23
|
+
id,
|
|
24
|
+
enctype,
|
|
25
|
+
onBeforeSubmit,
|
|
26
|
+
onAfterSubmit,
|
|
27
|
+
children,
|
|
28
|
+
}: Props = $props()
|
|
29
|
+
|
|
30
|
+
// ============================================================
|
|
31
|
+
// ROUTER ACCESS
|
|
32
|
+
// ============================================================
|
|
33
|
+
|
|
34
|
+
const router = getRouter()
|
|
35
|
+
|
|
36
|
+
// ============================================================
|
|
37
|
+
// SUBMIT HANDLER
|
|
38
|
+
// ============================================================
|
|
39
|
+
|
|
40
|
+
async function handleSubmit(event: SubmitEvent) {
|
|
41
|
+
event.preventDefault()
|
|
42
|
+
|
|
43
|
+
const form = event.currentTarget as HTMLFormElement
|
|
44
|
+
const formData = new FormData(form)
|
|
45
|
+
|
|
46
|
+
// Include the submitter's name/value if present
|
|
47
|
+
// (e.g. multiple submit buttons with different values)
|
|
48
|
+
const submitter = event.submitter as HTMLButtonElement | null
|
|
49
|
+
if (submitter?.name) {
|
|
50
|
+
formData.set(submitter.name, submitter.value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Pre-submit hook — cancel if returns false
|
|
54
|
+
if (onBeforeSubmit) {
|
|
55
|
+
const shouldContinue = await onBeforeSubmit(formData)
|
|
56
|
+
if (shouldContinue === false)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Resolve action path — defaults to current pathname
|
|
61
|
+
const actionPath = action ?? window.location.pathname
|
|
62
|
+
|
|
63
|
+
await router.submit(formData, {
|
|
64
|
+
action: actionPath,
|
|
65
|
+
method: 'POST',
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
onAfterSubmit?.()
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<form
|
|
73
|
+
method='POST'
|
|
74
|
+
action={action ?? '#'}
|
|
75
|
+
class={className}
|
|
76
|
+
{id}
|
|
77
|
+
{enctype}
|
|
78
|
+
onsubmit={handleSubmit}
|
|
79
|
+
>
|
|
80
|
+
{@render children()}
|
|
81
|
+
</form>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLFormAttributes } from 'svelte/elements';
|
|
3
|
+
interface Props extends HTMLFormAttributes {
|
|
4
|
+
onBeforeSubmit?: (formData: FormData) => boolean | Promise<boolean>;
|
|
5
|
+
onAfterSubmit?: () => void;
|
|
6
|
+
children: Snippet;
|
|
7
|
+
}
|
|
8
|
+
declare const Form: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type Form = ReturnType<typeof Form>;
|
|
10
|
+
export default Form;
|
package/dist/Link.svelte
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script lang='ts'>
|
|
2
|
+
import type { NavigateOptions } from '@cross-router/core'
|
|
3
|
+
import type { Snippet } from 'svelte'
|
|
4
|
+
import type { HTMLAnchorAttributes } from 'svelte/elements'
|
|
5
|
+
import { getRouter, routerState } from './context.svelte'
|
|
6
|
+
|
|
7
|
+
// ============================================================
|
|
8
|
+
// PROPS
|
|
9
|
+
// ============================================================
|
|
10
|
+
|
|
11
|
+
interface Props extends HTMLAnchorAttributes {
|
|
12
|
+
// Target path — required
|
|
13
|
+
href: string
|
|
14
|
+
|
|
15
|
+
// Navigate options
|
|
16
|
+
replace?: boolean
|
|
17
|
+
viewTransition?: boolean
|
|
18
|
+
state?: unknown
|
|
19
|
+
|
|
20
|
+
// Active link styling
|
|
21
|
+
// Applied when the current pathname starts with href
|
|
22
|
+
activeClass?: string
|
|
23
|
+
// Applied only when href matches exactly
|
|
24
|
+
exactActiveClass?: string
|
|
25
|
+
|
|
26
|
+
// Content
|
|
27
|
+
children: Snippet
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
href,
|
|
32
|
+
replace = false,
|
|
33
|
+
viewTransition = false,
|
|
34
|
+
state,
|
|
35
|
+
activeClass = '',
|
|
36
|
+
exactActiveClass = '',
|
|
37
|
+
target,
|
|
38
|
+
children,
|
|
39
|
+
...props
|
|
40
|
+
}: Props = $props()
|
|
41
|
+
|
|
42
|
+
// ============================================================
|
|
43
|
+
// ROUTER ACCESS
|
|
44
|
+
// ============================================================
|
|
45
|
+
|
|
46
|
+
const router = getRouter()
|
|
47
|
+
|
|
48
|
+
// ============================================================
|
|
49
|
+
// ACTIVE STATE
|
|
50
|
+
// ============================================================
|
|
51
|
+
|
|
52
|
+
const isActive = $derived(
|
|
53
|
+
routerState.current?.location.pathname.startsWith(href) ?? false,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const isExactActive = $derived(
|
|
57
|
+
routerState.current?.location.pathname === href,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// ============================================================
|
|
61
|
+
// CLICK HANDLER
|
|
62
|
+
// Intercepts clicks to use client-side navigation.
|
|
63
|
+
// Respects modifier keys (Ctrl, Meta, Shift) and target="_blank"
|
|
64
|
+
// so the browser handles those normally.
|
|
65
|
+
// ============================================================
|
|
66
|
+
|
|
67
|
+
function handleClick(event: MouseEvent) {
|
|
68
|
+
// Let the browser handle modified clicks and external targets
|
|
69
|
+
if (
|
|
70
|
+
event.defaultPrevented
|
|
71
|
+
|| event.metaKey
|
|
72
|
+
|| event.ctrlKey
|
|
73
|
+
|| event.shiftKey
|
|
74
|
+
|| event.altKey
|
|
75
|
+
|| target === '_blank'
|
|
76
|
+
) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
event.preventDefault()
|
|
81
|
+
|
|
82
|
+
const opts: NavigateOptions = { replace, viewTransition, state }
|
|
83
|
+
|
|
84
|
+
if (replace) {
|
|
85
|
+
router.replace(href, opts)
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
router.navigate(href, opts)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<a
|
|
94
|
+
{href}
|
|
95
|
+
{target}
|
|
96
|
+
class={{ [activeClass]: isActive, [exactActiveClass]: isExactActive }}
|
|
97
|
+
onclick={handleClick}
|
|
98
|
+
aria-current={isExactActive ? 'page' : undefined}
|
|
99
|
+
{...props}
|
|
100
|
+
>
|
|
101
|
+
{@render children()}
|
|
102
|
+
</a>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
|
3
|
+
interface Props extends HTMLAnchorAttributes {
|
|
4
|
+
href: string;
|
|
5
|
+
replace?: boolean;
|
|
6
|
+
viewTransition?: boolean;
|
|
7
|
+
state?: unknown;
|
|
8
|
+
activeClass?: string;
|
|
9
|
+
exactActiveClass?: string;
|
|
10
|
+
children: Snippet;
|
|
11
|
+
}
|
|
12
|
+
declare const Link: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type Link = ReturnType<typeof Link>;
|
|
14
|
+
export default Link;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<script lang='ts' module>
|
|
2
|
+
export type SvelteRouteComponent = typeof import('svelte').SvelteComponent
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang='ts'>
|
|
6
|
+
import type { RouterInstance } from 'cross-router-core'
|
|
7
|
+
import type { Component } from 'svelte'
|
|
8
|
+
import { onDestroy, onMount, untrack } from 'svelte'
|
|
9
|
+
import {
|
|
10
|
+
routerState,
|
|
11
|
+
setMatchDepthContext,
|
|
12
|
+
setRouter,
|
|
13
|
+
} from './context.svelte'
|
|
14
|
+
import RouterView from './RouterView.svelte'
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
router?: RouterInstance
|
|
18
|
+
_depth?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const props: Props = $props()
|
|
22
|
+
|
|
23
|
+
const isRoot = untrack(() => props._depth === undefined)
|
|
24
|
+
const depth = untrack(() => props._depth ?? 0)
|
|
25
|
+
const nextDepth = depth + 1
|
|
26
|
+
|
|
27
|
+
// ── Reactive state ───────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
if (isRoot) {
|
|
30
|
+
const router = untrack(() => props.router)
|
|
31
|
+
|
|
32
|
+
if (!router) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
'[RouterView] Root <RouterView> requires a `router` prop.\nUsage: <RouterView router={myRouter} />',
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setRouter(router)
|
|
39
|
+
|
|
40
|
+
// Subscribe BEFORE initialize() so we never miss state updates.
|
|
41
|
+
const unsubscribe = router.subscribe((newState) => {
|
|
42
|
+
routerState.current = newState
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
routerState.current = router.state
|
|
46
|
+
|
|
47
|
+
setMatchDepthContext({
|
|
48
|
+
depth,
|
|
49
|
+
get match() { return routerState.current?.matches[depth]! },
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Initialize LAST — it fires setState({ status: 'loading' }) synchronously
|
|
53
|
+
// which immediately re-renders children. All context must be set first.
|
|
54
|
+
onMount(() => {
|
|
55
|
+
router.initialize()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
onDestroy(() => {
|
|
59
|
+
unsubscribe()
|
|
60
|
+
router.destroy()
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Set depth context synchronously so child hooks work on first render.
|
|
65
|
+
setMatchDepthContext({
|
|
66
|
+
depth,
|
|
67
|
+
get match() { return routerState.current?.matches[depth]! },
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Debug ────────────────────────────────────────────────────
|
|
72
|
+
$effect(() => {
|
|
73
|
+
try {
|
|
74
|
+
const val = localStorage.getItem('cross-router:debug')
|
|
75
|
+
if (!val)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
catch { return }
|
|
79
|
+
|
|
80
|
+
const s = routerState.current
|
|
81
|
+
if (!s)
|
|
82
|
+
return
|
|
83
|
+
// eslint-disable-next-line no-console
|
|
84
|
+
console.log(
|
|
85
|
+
`%c[RouterView depth=${depth}] status=${s.status} matches=${s.matches.length}`,
|
|
86
|
+
'color:#a855f7;font-weight:bold',
|
|
87
|
+
s.matches.map(m => m.route.id),
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// ── Derived values ───────────────────────────────────────────
|
|
92
|
+
const currentState = $derived(routerState.current)
|
|
93
|
+
const match = $derived(currentState?.matches[depth] ?? null)
|
|
94
|
+
const hasChild = $derived(currentState !== null && (currentState?.matches.length ?? 0) > nextDepth)
|
|
95
|
+
|
|
96
|
+
// --- Foreign renderer mount
|
|
97
|
+
let foreignTarget: HTMLElement | null = $state(null)
|
|
98
|
+
|
|
99
|
+
$effect(() => {
|
|
100
|
+
const renderer = match?.route.__renderer
|
|
101
|
+
const component = match?.route.component
|
|
102
|
+
if (!renderer || !component || !foreignTarget)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
return renderer.mount(component, foreignTarget)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// ── View transition ──────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
$effect(() => {
|
|
111
|
+
if (currentState?.viewTransition && 'startViewTransition' in document) {
|
|
112
|
+
document.startViewTransition(() => {})
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
{#if currentState === null}
|
|
118
|
+
<!-- Router not yet initialized -->
|
|
119
|
+
{:else if currentState.status === 'error' && !currentState.matches.length}
|
|
120
|
+
<!-- Global unhandled error — no errorComponent in tree -->
|
|
121
|
+
{:else if match}
|
|
122
|
+
{@const renderer = match.route.__renderer}
|
|
123
|
+
{@const RouteComponent = match.route.component as Component | undefined}
|
|
124
|
+
{@const ErrorComponent = match.route.errorComponent as Component | undefined}
|
|
125
|
+
|
|
126
|
+
{#if match.status === 'error' && ErrorComponent}
|
|
127
|
+
<ErrorComponent error={match.error} />
|
|
128
|
+
{:else if renderer && RouteComponent}
|
|
129
|
+
<!-- Foreign framework component - mountded via its adapter renderer -->
|
|
130
|
+
<div bind:this={foreignTarget}></div>
|
|
131
|
+
{#if hasChild}
|
|
132
|
+
<RouterView _depth={nextDepth} />
|
|
133
|
+
{/if}
|
|
134
|
+
{:else if RouteComponent}
|
|
135
|
+
<RouteComponent>
|
|
136
|
+
{#snippet outlet()}
|
|
137
|
+
{#if hasChild}
|
|
138
|
+
<RouterView _depth={nextDepth} />
|
|
139
|
+
{/if}
|
|
140
|
+
{/snippet}
|
|
141
|
+
</RouteComponent>
|
|
142
|
+
{:else if hasChild}
|
|
143
|
+
<!-- Pathless/componentless layout route — no component to render,
|
|
144
|
+
just pass through to the next depth immediately -->
|
|
145
|
+
<RouterView _depth={nextDepth} />
|
|
146
|
+
{/if}
|
|
147
|
+
{/if}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type SvelteRouteComponent = typeof import('svelte').SvelteComponent;
|
|
2
|
+
import type { RouterInstance } from 'cross-router-core';
|
|
3
|
+
import type { Component } from 'svelte';
|
|
4
|
+
import RouterView from './RouterView.svelte';
|
|
5
|
+
interface Props {
|
|
6
|
+
router?: RouterInstance;
|
|
7
|
+
_depth?: number;
|
|
8
|
+
}
|
|
9
|
+
declare const RouterView: Component<Props, {}, "">;
|
|
10
|
+
type RouterView = ReturnType<typeof RouterView>;
|
|
11
|
+
export default RouterView;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AnyRouteMatch, NavigationState, RouterInstance } from 'cross-router-core';
|
|
2
|
+
declare const ROUTER_SYMBOL: unique symbol;
|
|
3
|
+
declare const ROUTER_STATE_SYMBOL: unique symbol;
|
|
4
|
+
declare global {
|
|
5
|
+
interface Window {
|
|
6
|
+
[ROUTER_SYMBOL]: RouterInstance | null;
|
|
7
|
+
[ROUTER_STATE_SYMBOL]: NavigationState | null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export declare function setRouter(router: RouterInstance): void;
|
|
11
|
+
export declare function getRouter(): RouterInstance;
|
|
12
|
+
export declare const routerState: {
|
|
13
|
+
current: NavigationState | null;
|
|
14
|
+
};
|
|
15
|
+
export interface MatchDepthContext {
|
|
16
|
+
readonly depth: number;
|
|
17
|
+
readonly match: AnyRouteMatch;
|
|
18
|
+
}
|
|
19
|
+
export declare function setMatchDepthContext(ctx: MatchDepthContext): void;
|
|
20
|
+
export declare function getMatchDepthContext(): MatchDepthContext | null;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
// ============================================================
|
|
3
|
+
// ROUTER SINGLETON
|
|
4
|
+
// Module-level — works outside components (loaders, actions, ts files).
|
|
5
|
+
// Set once at app startup by the framework adapter init.
|
|
6
|
+
// ============================================================
|
|
7
|
+
var ROUTER_SYMBOL = Symbol.for('cross-router-instance');
|
|
8
|
+
var ROUTER_STATE_SYMBOL = Symbol.for('cross-router-state');
|
|
9
|
+
window[ROUTER_SYMBOL] = null;
|
|
10
|
+
export function setRouter(router) {
|
|
11
|
+
window[ROUTER_SYMBOL] = router;
|
|
12
|
+
}
|
|
13
|
+
export function getRouter() {
|
|
14
|
+
var _router = window[ROUTER_SYMBOL];
|
|
15
|
+
if (!_router) {
|
|
16
|
+
throw new Error('[router] No router instance found. Make sure you called setRouter() before using any router hooks.');
|
|
17
|
+
}
|
|
18
|
+
return _router;
|
|
19
|
+
}
|
|
20
|
+
// ── Router state ─────────────────────────────────────────────
|
|
21
|
+
// Exported as a reactive object rather than via a getter function.
|
|
22
|
+
//
|
|
23
|
+
// $derived(getRouterState()) does NOT work across module boundaries:
|
|
24
|
+
// Svelte's tracker only intercepts $state reads that happen during
|
|
25
|
+
// the synchronous evaluation of the current reactive computation.
|
|
26
|
+
// A call to getRouterState() dispatches into another module's scope;
|
|
27
|
+
// the read of _state happens inside that function body, outside the
|
|
28
|
+
// tracker's view — so the derived never re-runs when state changes.
|
|
29
|
+
//
|
|
30
|
+
// Exporting a plain object whose property IS the $state variable
|
|
31
|
+
// lets consumers write `routerState` directly inside their
|
|
32
|
+
// $derived / template, which Svelte does track correctly.
|
|
33
|
+
export var routerState = $state({ current: null });
|
|
34
|
+
// ============================================================
|
|
35
|
+
// MATCH DEPTH CONTEXT
|
|
36
|
+
// Set by each <RouterView> level so useLoaderData() knows
|
|
37
|
+
// which match in the array it belongs to.
|
|
38
|
+
// ============================================================
|
|
39
|
+
var MATCH_DEPTH_KEY = 'cross-router-depth';
|
|
40
|
+
export function setMatchDepthContext(ctx) {
|
|
41
|
+
setContext(MATCH_DEPTH_KEY, ctx);
|
|
42
|
+
}
|
|
43
|
+
export function getMatchDepthContext() {
|
|
44
|
+
var _a;
|
|
45
|
+
return (_a = getContext(MATCH_DEPTH_KEY)) !== null && _a !== void 0 ? _a : null;
|
|
46
|
+
}
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AnyRouteMatch, Location, NavigateOptions, NavigationState, SubmitOptions } from '@cross-router/core';
|
|
2
|
+
export declare function useRouterState(): () => NavigationState;
|
|
3
|
+
export declare function useNavigationStatus(): () => NavigationState['status'];
|
|
4
|
+
export declare function useLocation(): () => Location;
|
|
5
|
+
export declare function useMatch(): () => AnyRouteMatch;
|
|
6
|
+
export declare function useParams<TParams extends Record<string, string> = Record<string, string>>(): () => TParams;
|
|
7
|
+
export declare function useLoaderData<TData = unknown>(): () => TData;
|
|
8
|
+
export declare function useActionData<TData = unknown>(): () => TData | undefined;
|
|
9
|
+
export declare function useNavigate(): (to: string, opts?: NavigateOptions) => Promise<void>;
|
|
10
|
+
export declare function useSubmit(): (formData: FormData, opts: SubmitOptions) => Promise<void>;
|
|
11
|
+
export declare function useIsTransitioning(): () => boolean;
|
|
12
|
+
export declare function useMatches(): () => AnyRouteMatch[];
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { getMatchDepthContext, getRouter, routerState, } from './context.svelte';
|
|
2
|
+
// ============================================================
|
|
3
|
+
// HOOKS
|
|
4
|
+
//
|
|
5
|
+
// All hooks must be called inside a Svelte component
|
|
6
|
+
// (during component initialisation) — same rule as Svelte's
|
|
7
|
+
// own getContext().
|
|
8
|
+
//
|
|
9
|
+
// Exception: useNavigate() and useSubmit() return stable
|
|
10
|
+
// function references that can be stored and called anywhere.
|
|
11
|
+
// ============================================================
|
|
12
|
+
// ============================================================
|
|
13
|
+
// useRouterState
|
|
14
|
+
// Returns the full reactive NavigationState.
|
|
15
|
+
// Use this as an escape hatch — prefer the scoped hooks below.
|
|
16
|
+
// ============================================================
|
|
17
|
+
export function useRouterState() {
|
|
18
|
+
return function () { return routerState.current; };
|
|
19
|
+
}
|
|
20
|
+
// ============================================================
|
|
21
|
+
// useNavigationStatus
|
|
22
|
+
// Reactive shortcut for the current navigation status.
|
|
23
|
+
// Useful for global loading indicators.
|
|
24
|
+
// ============================================================
|
|
25
|
+
export function useNavigationStatus() {
|
|
26
|
+
return function () { return routerState.current.status; };
|
|
27
|
+
}
|
|
28
|
+
// ============================================================
|
|
29
|
+
// useLocation
|
|
30
|
+
// Reactive current location.
|
|
31
|
+
// ============================================================
|
|
32
|
+
export function useLocation() {
|
|
33
|
+
return function () { return routerState.current.location; };
|
|
34
|
+
}
|
|
35
|
+
// ============================================================
|
|
36
|
+
// useMatch
|
|
37
|
+
// Returns the RouteMatch for the current component's depth.
|
|
38
|
+
// Throws if called outside a RouterView context.
|
|
39
|
+
// ============================================================
|
|
40
|
+
export function useMatch() {
|
|
41
|
+
var depthCtx = getMatchDepthContext();
|
|
42
|
+
if (!depthCtx) {
|
|
43
|
+
throw new Error('[router] useMatch() must be called inside a route component '
|
|
44
|
+
+ 'rendered by <RouterView>.');
|
|
45
|
+
}
|
|
46
|
+
var depth = depthCtx.depth;
|
|
47
|
+
return function () {
|
|
48
|
+
var _a;
|
|
49
|
+
var match = (_a = routerState.current) === null || _a === void 0 ? void 0 : _a.matches[depth];
|
|
50
|
+
if (!match) {
|
|
51
|
+
throw new Error("[router] useMatch() could not find a match at depth ".concat(depth, ". ")
|
|
52
|
+
+ "This is likely a bug in RouterView.");
|
|
53
|
+
}
|
|
54
|
+
return match;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// ============================================================
|
|
58
|
+
// useParams
|
|
59
|
+
// Reactive params for the current route.
|
|
60
|
+
// Typed via generic — useParams<{ id: string }>()
|
|
61
|
+
// ============================================================
|
|
62
|
+
export function useParams() {
|
|
63
|
+
var getMatch = useMatch();
|
|
64
|
+
return function () { return getMatch().params; };
|
|
65
|
+
}
|
|
66
|
+
// ============================================================
|
|
67
|
+
// useLoaderData
|
|
68
|
+
// Reactive loader data for the current route.
|
|
69
|
+
// Typed via generic — useLoaderData<MyLoaderReturnType>()
|
|
70
|
+
// ============================================================
|
|
71
|
+
export function useLoaderData() {
|
|
72
|
+
var getMatch = useMatch();
|
|
73
|
+
return function () { return getMatch().loaderData; };
|
|
74
|
+
}
|
|
75
|
+
// ============================================================
|
|
76
|
+
// useActionData
|
|
77
|
+
// Reactive action data from the last form submission.
|
|
78
|
+
// Not scoped to depth — actions return a single global result.
|
|
79
|
+
// ============================================================
|
|
80
|
+
export function useActionData() {
|
|
81
|
+
return function () { var _a; return (_a = routerState.current) === null || _a === void 0 ? void 0 : _a.actionData; };
|
|
82
|
+
}
|
|
83
|
+
// ============================================================
|
|
84
|
+
// useNavigate
|
|
85
|
+
// Returns a stable navigate() function reference.
|
|
86
|
+
// Safe to call outside component lifecycle (event handlers, etc.)
|
|
87
|
+
// ============================================================
|
|
88
|
+
export function useNavigate() {
|
|
89
|
+
var router = getRouter();
|
|
90
|
+
return function (to, opts) { return router.navigate(to, opts); };
|
|
91
|
+
}
|
|
92
|
+
// ============================================================
|
|
93
|
+
// useSubmit
|
|
94
|
+
// Returns a stable submit() function reference.
|
|
95
|
+
// Useful for programmatic form submission outside <Form>.
|
|
96
|
+
// ============================================================
|
|
97
|
+
export function useSubmit() {
|
|
98
|
+
var router = getRouter();
|
|
99
|
+
return function (formData, opts) { return router.submit(formData, opts); };
|
|
100
|
+
}
|
|
101
|
+
// ============================================================
|
|
102
|
+
// useIsTransitioning
|
|
103
|
+
// True while a navigation is in flight.
|
|
104
|
+
// Useful for showing route-level skeleton states.
|
|
105
|
+
// ============================================================
|
|
106
|
+
export function useIsTransitioning() {
|
|
107
|
+
return function () { var _a, _b; return (_b = (_a = routerState.current) === null || _a === void 0 ? void 0 : _a.isTransitioning) !== null && _b !== void 0 ? _b : false; };
|
|
108
|
+
}
|
|
109
|
+
// ============================================================
|
|
110
|
+
// useMatches
|
|
111
|
+
// Returns the full ordered match array (root → leaf).
|
|
112
|
+
// Useful for breadcrumbs, tab bars, etc.
|
|
113
|
+
// ============================================================
|
|
114
|
+
export function useMatches() {
|
|
115
|
+
return function () { var _a, _b; return (_b = (_a = routerState.current) === null || _a === void 0 ? void 0 : _a.matches) !== null && _b !== void 0 ? _b : []; };
|
|
116
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { getRouter, setRouter } from './context.svelte';
|
|
2
|
+
export { default as Form } from './Form.svelte';
|
|
3
|
+
export { useActionData, useIsTransitioning, useLoaderData, useLocation, useMatch, useMatches, useNavigate, useNavigationStatus, useParams, useRouterState, useSubmit, } from './hooks';
|
|
4
|
+
export { default as Link } from './Link.svelte';
|
|
5
|
+
export { default as RouterView } from './RouterView.svelte';
|
|
6
|
+
export { route, svelteRenderer } from './routes';
|
|
7
|
+
export type { ActionArgs, ActionFn, AnyMiddleware, AnyRouteDefinition, AnyRouteMatch, History, InferSearch, LoaderArgs, LoaderFn, Location, Middleware, MiddlewareArgs, NavigateOptions, NavigationState, NavigationStatus, NextFn, ParamsFromPath, RevalidateArgs, RouteDefinition, RouteMatch, RouterContextProvider, RouteRegistry, RouterInstance, RouterOptions, SearchSchema, SubmitOptions, } from 'cross-router-core';
|
|
8
|
+
export { createBrowserHistory, createBrowserRouter, createMemoryHistory, createRegistry, createRouter, defineMiddleware, isRedirect, MiddlewareError, redirect, Redirect, RegistryError, } from 'cross-router-core';
|
|
9
|
+
/**
|
|
10
|
+
* Enable router debug logging in the browser console.
|
|
11
|
+
* @param scopes - 'all' or comma-separated: 'router,registry,matcher,loader,middleware'
|
|
12
|
+
*/
|
|
13
|
+
export declare function enableDebug(scopes?: string): void;
|
|
14
|
+
export declare function disableDebug(): void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// cross-router-svelte — public API
|
|
3
|
+
// ============================================================
|
|
4
|
+
// ============================================================
|
|
5
|
+
// COMPONENTS
|
|
6
|
+
// ============================================================
|
|
7
|
+
export { getRouter, setRouter } from './context.svelte';
|
|
8
|
+
export { default as Form } from './Form.svelte';
|
|
9
|
+
export { useActionData, useIsTransitioning, useLoaderData, useLocation, useMatch, useMatches, useNavigate, useNavigationStatus, useParams, useRouterState, useSubmit, } from './hooks';
|
|
10
|
+
// ============================================================
|
|
11
|
+
// HOOKS
|
|
12
|
+
// All must be called during component initialisation
|
|
13
|
+
// (same constraint as Svelte's getContext)
|
|
14
|
+
// ============================================================
|
|
15
|
+
export { default as Link } from './Link.svelte';
|
|
16
|
+
// ============================================================
|
|
17
|
+
// ROUTER SINGLETON — for use outside components
|
|
18
|
+
// ============================================================
|
|
19
|
+
export { default as RouterView } from './RouterView.svelte';
|
|
20
|
+
export { route, svelteRenderer } from './routes';
|
|
21
|
+
export { createBrowserHistory, createBrowserRouter, createMemoryHistory, createRegistry,
|
|
22
|
+
// Router creation
|
|
23
|
+
createRouter,
|
|
24
|
+
// Route authoring
|
|
25
|
+
defineMiddleware, isRedirect, MiddlewareError, redirect, Redirect,
|
|
26
|
+
// Errors
|
|
27
|
+
RegistryError, } from 'cross-router-core';
|
|
28
|
+
// ── Debug ────────────────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* Enable router debug logging in the browser console.
|
|
31
|
+
* @param scopes - 'all' or comma-separated: 'router,registry,matcher,loader,middleware'
|
|
32
|
+
*/
|
|
33
|
+
export function enableDebug(scopes) {
|
|
34
|
+
if (scopes === void 0) { scopes = 'all'; }
|
|
35
|
+
localStorage.setItem('cross-router:debug', scopes === 'all' ? 'true' : scopes);
|
|
36
|
+
// eslint-disable-next-line no-console
|
|
37
|
+
console.log('[cross-router] debug enabled:', scopes, '— reload to see logs from init');
|
|
38
|
+
}
|
|
39
|
+
export function disableDebug() {
|
|
40
|
+
localStorage.removeItem('cross-router:debug');
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.log('[cross-router] debug disabled');
|
|
43
|
+
}
|
package/dist/routes.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AnyRouteDefinition, RouteDefinition, RouteRenderer } from 'cross-router-core';
|
|
2
|
+
import type { Component } from 'svelte';
|
|
3
|
+
/**
|
|
4
|
+
* The Svelte route renderer.
|
|
5
|
+
* RouterView uses this as the default - it's exported so other adapters
|
|
6
|
+
* can detect "is this already a Svelte-tagged route?" if needed
|
|
7
|
+
*/
|
|
8
|
+
export declare const svelteRenderer: RouteRenderer;
|
|
9
|
+
/**
|
|
10
|
+
* Tag route definitions as Svelte-rendered
|
|
11
|
+
*
|
|
12
|
+
* This is the identity function for the host adapter - Svelte routes don't
|
|
13
|
+
* strictly need tagging since RouterView handles them natively, but calling
|
|
14
|
+
* `routes()` makes the framework ownership explicit and consistent with
|
|
15
|
+
* `routes()` from other adapters
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* import { routes } from 'cross-router-svelte
|
|
19
|
+
*
|
|
20
|
+
* const appRoutes = [
|
|
21
|
+
* ...routes([{ id: 'home', path: '/', component: HomePage }]),
|
|
22
|
+
* ...otherRouter([ id: 'widget', path: '/widget', component: OtherWidget ])
|
|
23
|
+
* ]
|
|
24
|
+
*/
|
|
25
|
+
export declare function route(defs: RouteDefinition<any, any, any, any, Component>[]): AnyRouteDefinition[];
|
package/dist/routes.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { tag } from 'cross-router-core';
|
|
2
|
+
import { mount, unmount } from 'svelte';
|
|
3
|
+
/**
|
|
4
|
+
* The Svelte route renderer.
|
|
5
|
+
* RouterView uses this as the default - it's exported so other adapters
|
|
6
|
+
* can detect "is this already a Svelte-tagged route?" if needed
|
|
7
|
+
*/
|
|
8
|
+
export var svelteRenderer = {
|
|
9
|
+
mount: function (component, target) {
|
|
10
|
+
var instance = mount(component, { target: target });
|
|
11
|
+
return function () { return unmount(instance); };
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Tag route definitions as Svelte-rendered
|
|
16
|
+
*
|
|
17
|
+
* This is the identity function for the host adapter - Svelte routes don't
|
|
18
|
+
* strictly need tagging since RouterView handles them natively, but calling
|
|
19
|
+
* `routes()` makes the framework ownership explicit and consistent with
|
|
20
|
+
* `routes()` from other adapters
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* import { routes } from 'cross-router-svelte
|
|
24
|
+
*
|
|
25
|
+
* const appRoutes = [
|
|
26
|
+
* ...routes([{ id: 'home', path: '/', component: HomePage }]),
|
|
27
|
+
* ...otherRouter([ id: 'widget', path: '/widget', component: OtherWidget ])
|
|
28
|
+
* ]
|
|
29
|
+
*/
|
|
30
|
+
export function route(defs) {
|
|
31
|
+
return tag(svelteRenderer, defs);
|
|
32
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cross-router-svelte",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.1",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"import": "./dist/index.js",
|
|
9
|
+
"svelte": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"svelte": "^5.53.7",
|
|
19
|
+
"cross-router-core": "1.0.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@sveltejs/package": "^2.5.7",
|
|
23
|
+
"@types/node": "^24.12.0",
|
|
24
|
+
"eslint-plugin-svelte": "^3.15.0",
|
|
25
|
+
"svelte": "^5.53.7",
|
|
26
|
+
"typescript": "^5.9.3",
|
|
27
|
+
"vitest": "^4.0.18"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"linkDirectory": true
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "svelte-package",
|
|
34
|
+
"typecheck": "tsc --noEmit"
|
|
35
|
+
}
|
|
36
|
+
}
|