@absolutejs/absolute 0.19.0-beta.819 → 0.19.0-beta.820
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/angular/components/core/streamingSlotRegistrar.js +1 -1
- package/dist/angular/components/core/streamingSlotRegistry.js +2 -2
- package/dist/src/svelte/router/browser.d.ts +4 -0
- package/dist/src/svelte/router/goto.d.ts +15 -0
- package/dist/src/svelte/router/hashMode.d.ts +21 -0
- package/dist/src/svelte/router/index.d.ts +4 -0
- package/dist/src/svelte/router/matchPath.d.ts +50 -0
- package/dist/src/svelte/router/page.svelte.d.ts +24 -0
- package/dist/src/svelte/router/prefetchCache.d.ts +21 -0
- package/dist/src/svelte/router/pushState.d.ts +13 -0
- package/dist/src/svelte/router/viewTransitions.d.ts +6 -0
- package/dist/svelte/router/Link.svelte +124 -0
- package/dist/svelte/router/Link.svelte.d.ts +21 -0
- package/dist/svelte/router/Route.svelte +51 -0
- package/dist/svelte/router/Route.svelte.d.ts +13 -0
- package/dist/svelte/router/Router.svelte +164 -0
- package/dist/svelte/router/Router.svelte.d.ts +16 -0
- package/dist/svelte/router/browser.ts +14 -0
- package/dist/svelte/router/goto.ts +89 -0
- package/dist/svelte/router/hashMode.ts +37 -0
- package/dist/svelte/router/index.ts +23 -0
- package/dist/svelte/router/matchPath.ts +158 -0
- package/dist/svelte/router/page.svelte.ts +57 -0
- package/dist/svelte/router/prefetchCache.ts +89 -0
- package/dist/svelte/router/pushState.ts +35 -0
- package/dist/svelte/router/viewTransitions.ts +31 -0
- package/dist/types/svelteRouter.d.ts +65 -0
- package/dist/types/svelteRouter.ts +91 -0
- package/package.json +30 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __require = import.meta.require;
|
|
3
3
|
|
|
4
|
-
// .angular-partial-tmp-
|
|
4
|
+
// .angular-partial-tmp-dDyepD/src/core/streamingSlotRegistrar.ts
|
|
5
5
|
var STREAMING_SLOT_REGISTRAR_KEY = Symbol.for("absolutejs.streamingSlotRegistrar");
|
|
6
6
|
var STREAMING_SLOT_WARNING_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotWarningController");
|
|
7
7
|
var STREAMING_SLOT_COLLECTION_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotCollectionController");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __require = import.meta.require;
|
|
3
3
|
|
|
4
|
-
// .angular-partial-tmp-
|
|
4
|
+
// .angular-partial-tmp-dDyepD/src/core/streamingSlotRegistrar.ts
|
|
5
5
|
var STREAMING_SLOT_REGISTRAR_KEY = Symbol.for("absolutejs.streamingSlotRegistrar");
|
|
6
6
|
var STREAMING_SLOT_WARNING_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotWarningController");
|
|
7
7
|
var STREAMING_SLOT_COLLECTION_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotCollectionController");
|
|
@@ -48,7 +48,7 @@ var warnMissingStreamingSlotCollector = (primitiveName) => {
|
|
|
48
48
|
getWarningController()?.maybeWarn(primitiveName);
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
-
// .angular-partial-tmp-
|
|
51
|
+
// .angular-partial-tmp-dDyepD/src/core/streamingSlotRegistry.ts
|
|
52
52
|
var STREAMING_SLOT_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotAsyncLocalStorage");
|
|
53
53
|
var isObjectRecord2 = (value) => Boolean(value) && typeof value === "object";
|
|
54
54
|
var isAsyncLocalStorage = (value) => isObjectRecord2(value) && ("getStore" in value) && typeof value.getStore === "function" && ("run" in value) && typeof value.run === "function";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GotoOptions, RouterMode } from '../../../types/svelteRouter';
|
|
2
|
+
/**
|
|
3
|
+
* Internal — called by Router.svelte on mount so navigation primitives
|
|
4
|
+
* know which URL strategy to use. Hash mode rewrites the URL bar to
|
|
5
|
+
* `#/path` instead of `/path`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const setRouterMode: (mode: RouterMode) => void;
|
|
8
|
+
/**
|
|
9
|
+
* Programmatically navigate to a URL. Updates `page.url`, writes history,
|
|
10
|
+
* and (when supported) wraps the swap in `document.startViewTransition`.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors SvelteKit's `goto` from `$app/navigation` — same name, same
|
|
13
|
+
* options shape, so a SvelteKit user finds the primitive familiar.
|
|
14
|
+
*/
|
|
15
|
+
export declare const goto: (target: string, options?: GotoOptions) => Promise<void>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash mode: routing happens against `window.location.hash` with the
|
|
3
|
+
* leading `#/` stripped. Useful for static deploys (GitHub Pages, S3)
|
|
4
|
+
* where the host can't be configured to wildcard-route to one HTML file.
|
|
5
|
+
*
|
|
6
|
+
* URLs look like `https://example.com/#/dashboard/settings`. The
|
|
7
|
+
* `pathname` part stays at `/` so the server always serves the same
|
|
8
|
+
* page; `<Route>` matching looks at the hash instead.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Extract the routable pathname from a full URL when hash mode is on.
|
|
12
|
+
* Returns the part after `#/`, prefixed with `/` so it parses as a
|
|
13
|
+
* normal pathname.
|
|
14
|
+
*/
|
|
15
|
+
export declare const hashPathnameOf: (url: URL) => string;
|
|
16
|
+
/**
|
|
17
|
+
* Build a hash URL from a routable pathname. Used by goto() / pushState()
|
|
18
|
+
* when in hash mode — converts `/dashboard/settings` to `#/dashboard/settings`
|
|
19
|
+
* and writes the result back to the URL.
|
|
20
|
+
*/
|
|
21
|
+
export declare const buildHashHref: (pathname: string) => string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ExtractRouteParams, RouteMatchResult } from '../../../types/svelteRouter';
|
|
2
|
+
type CompiledSegment = {
|
|
3
|
+
kind: 'static';
|
|
4
|
+
value: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'param';
|
|
7
|
+
name: string;
|
|
8
|
+
optional: boolean;
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'wildcard';
|
|
11
|
+
};
|
|
12
|
+
type CompiledPattern = {
|
|
13
|
+
segments: CompiledSegment[];
|
|
14
|
+
score: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Compile a `<Route path>` pattern into segments + a specificity score.
|
|
18
|
+
* Higher score = more specific (longer static prefix beats parameterised).
|
|
19
|
+
*/
|
|
20
|
+
export declare const compilePattern: (pattern: string) => CompiledPattern;
|
|
21
|
+
/**
|
|
22
|
+
* Match a URL pathname against a compiled pattern. Returns the extracted
|
|
23
|
+
* params on a successful match, or a miss otherwise.
|
|
24
|
+
*/
|
|
25
|
+
export declare const matchPattern: <Path extends string>(pattern: CompiledPattern, pathname: string) => RouteMatchResult<ExtractRouteParams<Path>>;
|
|
26
|
+
/**
|
|
27
|
+
* Stable comparator for compiled patterns. Higher specificity sorts first.
|
|
28
|
+
* When two patterns have equal score, declaration order (the original index)
|
|
29
|
+
* decides — passed in via the `index` field on each entry.
|
|
30
|
+
*/
|
|
31
|
+
export declare const comparePatterns: (a: {
|
|
32
|
+
score: number;
|
|
33
|
+
index: number;
|
|
34
|
+
}, b: {
|
|
35
|
+
score: number;
|
|
36
|
+
index: number;
|
|
37
|
+
}) => number;
|
|
38
|
+
/**
|
|
39
|
+
* Join a basepath stack with a child pattern, producing an absolute pattern
|
|
40
|
+
* that the route matcher can compile against an incoming pathname.
|
|
41
|
+
*
|
|
42
|
+
* Handles slash edge cases:
|
|
43
|
+
* joinBasepath('', '/users') → '/users'
|
|
44
|
+
* joinBasepath('/portal', '/users') → '/portal/users'
|
|
45
|
+
* joinBasepath('/portal/', '/users') → '/portal/users'
|
|
46
|
+
* joinBasepath('/portal', 'users') → '/portal/users'
|
|
47
|
+
* joinBasepath('/portal', '/') → '/portal'
|
|
48
|
+
*/
|
|
49
|
+
export declare const joinBasepath: (basepath: string, pattern: string) => string;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PageState } from '../../../types/svelteRouter';
|
|
2
|
+
/**
|
|
3
|
+
* Reactive route state. Mirrors SvelteKit's `page` from `$app/state`:
|
|
4
|
+
*
|
|
5
|
+
* import { page } from '@absolutejs/absolute/svelte/router';
|
|
6
|
+
* page.url.pathname // current path (reactive)
|
|
7
|
+
* page.url.searchParams // parsed URLSearchParams (reactive)
|
|
8
|
+
* page.params.id // active route params (reactive)
|
|
9
|
+
* page.state // history.state for the current entry
|
|
10
|
+
*
|
|
11
|
+
* Backed by `$state`. Direct property access in templates re-renders.
|
|
12
|
+
*/
|
|
13
|
+
export declare const page: PageState;
|
|
14
|
+
/**
|
|
15
|
+
* Internal — only Router.svelte and the navigation primitives call this.
|
|
16
|
+
* Replaces the entire page state in one assignment so subscribers fire
|
|
17
|
+
* once per navigation rather than once per mutated field.
|
|
18
|
+
*/
|
|
19
|
+
export declare const setPage: (next: Partial<PageState>) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Internal — used during SSR to seed the page state before render so
|
|
22
|
+
* `<Route>` blocks see the correct `page.url`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const seedPage: (url: URL, params?: Record<string, string | undefined>) => void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prefetch a URL into the in-memory cache. No-op if the user has signalled
|
|
3
|
+
* data-saver / reduced-data, or if the URL is already cached.
|
|
4
|
+
*/
|
|
5
|
+
export declare const prefetch: (url: string) => void;
|
|
6
|
+
/**
|
|
7
|
+
* Consume a cached prefetch entry on actual navigation, removing it from
|
|
8
|
+
* the cache. Returns the cached Promise<Response> or undefined.
|
|
9
|
+
*/
|
|
10
|
+
export declare const consumePrefetch: (url: string) => Promise<Response> | undefined;
|
|
11
|
+
export declare const clearPrefetchCache: () => void;
|
|
12
|
+
type HoverHandle = {
|
|
13
|
+
cancel: () => void;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Wrap a prefetch trigger in a hover-debounce so glancing across many links
|
|
17
|
+
* doesn't fire a fetch storm. The returned handle's `cancel()` aborts the
|
|
18
|
+
* pending hover prefetch (e.g. on `pointerleave`).
|
|
19
|
+
*/
|
|
20
|
+
export declare const scheduleHoverPrefetch: (url: string) => HoverHandle;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shallow routing: update the URL bar and `page.state` without re-running
|
|
3
|
+
* `<Route>` matching. Useful for modals / drawers / overlays that want a
|
|
4
|
+
* shareable URL without swapping the active route's content.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors SvelteKit's `pushState` from `$app/navigation`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const pushState: (target: string, state: unknown) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Same as `pushState` but uses `history.replaceState`. Mirrors SvelteKit's
|
|
11
|
+
* `replaceState` from `$app/navigation`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const replaceState: (target: string, state: unknown) => void;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap a state mutation in `document.startViewTransition` when supported.
|
|
3
|
+
* Falls through to a synchronous call otherwise. Reduced-motion users get
|
|
4
|
+
* instant swaps via the browser's own handling of `prefers-reduced-motion`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const withViewTransition: (mutate: () => void | Promise<void>) => Promise<void>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy, onMount, type Snippet } from 'svelte';
|
|
3
|
+
import type { LinkPrefetchMode } from '../../../types/svelteRouter';
|
|
4
|
+
import { goto } from './goto';
|
|
5
|
+
import { prefetch, scheduleHoverPrefetch } from './prefetchCache';
|
|
6
|
+
|
|
7
|
+
type LinkProps = {
|
|
8
|
+
/** Destination URL — relative or absolute. */
|
|
9
|
+
to: string;
|
|
10
|
+
/** `true` → use `history.replaceState` instead of `pushState`.
|
|
11
|
+
* Same name as SvelteKit's `goto` option. */
|
|
12
|
+
replaceState?: boolean;
|
|
13
|
+
/** `'hover'` (default) — prefetch on `pointerenter`.
|
|
14
|
+
* `'viewport'` — prefetch when the link enters the viewport.
|
|
15
|
+
* `'none'` — disable prefetch for this link. */
|
|
16
|
+
prefetch?: LinkPrefetchMode;
|
|
17
|
+
/** Don't reset focus to body on navigate. */
|
|
18
|
+
keepFocus?: boolean;
|
|
19
|
+
/** Don't scroll to top on navigate. */
|
|
20
|
+
noScroll?: boolean;
|
|
21
|
+
/** Forwarded to the underlying `<a>` element. */
|
|
22
|
+
class?: string;
|
|
23
|
+
/** Forwarded to the underlying `<a>` element. */
|
|
24
|
+
target?: string;
|
|
25
|
+
children?: Snippet;
|
|
26
|
+
/** Allow arbitrary HTML attributes through. */
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
to,
|
|
32
|
+
replaceState = false,
|
|
33
|
+
prefetch: prefetchMode = 'hover',
|
|
34
|
+
keepFocus = false,
|
|
35
|
+
noScroll = false,
|
|
36
|
+
class: classProp,
|
|
37
|
+
target,
|
|
38
|
+
children,
|
|
39
|
+
...rest
|
|
40
|
+
}: LinkProps = $props();
|
|
41
|
+
|
|
42
|
+
let anchor: HTMLAnchorElement | null = null;
|
|
43
|
+
let hoverHandle: { cancel: () => void } | null = null;
|
|
44
|
+
let viewportObserver: IntersectionObserver | null = null;
|
|
45
|
+
|
|
46
|
+
const isModifierClick = (event: MouseEvent) =>
|
|
47
|
+
event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
|
48
|
+
|
|
49
|
+
const isExternal = (href: string) => {
|
|
50
|
+
if (typeof window === 'undefined') return false;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const url = new URL(href, window.location.href);
|
|
54
|
+
|
|
55
|
+
return url.origin !== window.location.origin;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleClick = (event: MouseEvent) => {
|
|
62
|
+
if (isModifierClick(event)) return;
|
|
63
|
+
if (event.button !== 0) return;
|
|
64
|
+
if (target && target !== '_self') return;
|
|
65
|
+
if (rest['download'] !== undefined) return;
|
|
66
|
+
if (isExternal(to)) return;
|
|
67
|
+
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
void goto(to, {
|
|
70
|
+
keepFocus,
|
|
71
|
+
noScroll,
|
|
72
|
+
replaceState
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handlePointerEnter = () => {
|
|
77
|
+
if (prefetchMode !== 'hover' || isExternal(to)) return;
|
|
78
|
+
hoverHandle?.cancel();
|
|
79
|
+
hoverHandle = scheduleHoverPrefetch(to);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const handlePointerLeave = () => {
|
|
83
|
+
hoverHandle?.cancel();
|
|
84
|
+
hoverHandle = null;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
onMount(() => {
|
|
88
|
+
if (prefetchMode !== 'viewport' || !anchor) return;
|
|
89
|
+
if (typeof IntersectionObserver === 'undefined') return;
|
|
90
|
+
if (isExternal(to)) return;
|
|
91
|
+
|
|
92
|
+
viewportObserver = new IntersectionObserver(
|
|
93
|
+
(entries) => {
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (entry.isIntersecting) {
|
|
96
|
+
prefetch(to);
|
|
97
|
+
viewportObserver?.disconnect();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{ rootMargin: '128px' }
|
|
103
|
+
);
|
|
104
|
+
viewportObserver.observe(anchor);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
onDestroy(() => {
|
|
108
|
+
hoverHandle?.cancel();
|
|
109
|
+
viewportObserver?.disconnect();
|
|
110
|
+
});
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<a
|
|
114
|
+
bind:this={anchor}
|
|
115
|
+
href={to}
|
|
116
|
+
class={classProp}
|
|
117
|
+
{target}
|
|
118
|
+
onclick={handleClick}
|
|
119
|
+
onpointerenter={handlePointerEnter}
|
|
120
|
+
onpointerleave={handlePointerLeave}
|
|
121
|
+
{...rest}
|
|
122
|
+
>
|
|
123
|
+
{@render children?.()}
|
|
124
|
+
</a>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { LinkPrefetchMode } from '../../types/svelteRouter';
|
|
3
|
+
|
|
4
|
+
type LinkProps = {
|
|
5
|
+
to: string;
|
|
6
|
+
replaceState?: boolean;
|
|
7
|
+
prefetch?: LinkPrefetchMode;
|
|
8
|
+
keepFocus?: boolean;
|
|
9
|
+
noScroll?: boolean;
|
|
10
|
+
class?: string;
|
|
11
|
+
target?: string;
|
|
12
|
+
children?: Snippet;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
declare const __propDef: { props: LinkProps };
|
|
17
|
+
type Props = typeof __propDef.props;
|
|
18
|
+
|
|
19
|
+
import { SvelteComponent } from 'svelte';
|
|
20
|
+
|
|
21
|
+
export default class Link extends SvelteComponent<Props> {}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts" generics="Path extends string">
|
|
2
|
+
import { getContext, onDestroy, type Snippet } from 'svelte';
|
|
3
|
+
import type { ExtractRouteParams } from '../../../types/svelteRouter';
|
|
4
|
+
import type { RouterRegistry } from './Router.svelte';
|
|
5
|
+
import { compilePattern, joinBasepath, matchPattern } from './matchPath';
|
|
6
|
+
import { page } from './page.svelte';
|
|
7
|
+
|
|
8
|
+
const ROUTER_CONTEXT_KEY = Symbol.for('absolutejs.svelte-router');
|
|
9
|
+
|
|
10
|
+
type RouteProps = {
|
|
11
|
+
path: Path;
|
|
12
|
+
content: Snippet<[ExtractRouteParams<Path>]>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let { path, content }: RouteProps = $props();
|
|
16
|
+
|
|
17
|
+
const registry = getContext<RouterRegistry | undefined>(ROUTER_CONTEXT_KEY);
|
|
18
|
+
if (!registry) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
'<Route> must be a descendant of <Router>. ' +
|
|
21
|
+
'Wrap your routes in `<Router url={...}>` (server) or `<Router>` (client).'
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const id = registry.nextRouteId();
|
|
26
|
+
let registrationOrder = 0;
|
|
27
|
+
|
|
28
|
+
const fullPattern = $derived(joinBasepath(registry.basepath, path));
|
|
29
|
+
const compiled = $derived(compilePattern(fullPattern));
|
|
30
|
+
|
|
31
|
+
$effect(() => {
|
|
32
|
+
registrationOrder = Number(id.slice(1));
|
|
33
|
+
registry.register(id, {
|
|
34
|
+
pattern: compiled,
|
|
35
|
+
registrationOrder
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
onDestroy(() => registry.deregister(id));
|
|
40
|
+
|
|
41
|
+
const isWinner = $derived(registry.isWinner(id));
|
|
42
|
+
const match = $derived(
|
|
43
|
+
isWinner
|
|
44
|
+
? matchPattern<Path>(compiled, page.url.pathname)
|
|
45
|
+
: { matched: false as const }
|
|
46
|
+
);
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
{#if match.matched}
|
|
50
|
+
{@render content(match.params)}
|
|
51
|
+
{/if}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { ExtractRouteParams } from '../../types/svelteRouter';
|
|
3
|
+
|
|
4
|
+
type RouteProps<Path extends string> = {
|
|
5
|
+
path: Path;
|
|
6
|
+
content: Snippet<[ExtractRouteParams<Path>]>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
import { SvelteComponent } from 'svelte';
|
|
10
|
+
|
|
11
|
+
export default class Route<
|
|
12
|
+
Path extends string = string
|
|
13
|
+
> extends SvelteComponent<RouteProps<Path>> {}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
type CompiledPatternBox = {
|
|
3
|
+
// reactive — read by the winner-resolver
|
|
4
|
+
pattern: ReturnType<typeof import('./matchPath').compilePattern>;
|
|
5
|
+
// stable — assigned at registration time, used as tiebreaker
|
|
6
|
+
registrationOrder: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type RouterRegistry = {
|
|
10
|
+
basepath: string;
|
|
11
|
+
mode: import('../../../types/svelteRouter').RouterMode;
|
|
12
|
+
register: (id: string, entry: CompiledPatternBox) => void;
|
|
13
|
+
deregister: (id: string) => void;
|
|
14
|
+
isWinner: (id: string) => boolean;
|
|
15
|
+
nextRouteId: () => string;
|
|
16
|
+
};
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<script lang="ts">
|
|
20
|
+
import { getContext, onMount, setContext, type Snippet } from 'svelte';
|
|
21
|
+
import type { RouterMode } from '../../../types/svelteRouter';
|
|
22
|
+
import { setRouterMode } from './goto';
|
|
23
|
+
import { hashPathnameOf } from './hashMode';
|
|
24
|
+
import { joinBasepath, matchPattern } from './matchPath';
|
|
25
|
+
import { page, seedPage, setPage } from './page.svelte';
|
|
26
|
+
|
|
27
|
+
const ROUTER_CONTEXT_KEY = Symbol.for('absolutejs.svelte-router');
|
|
28
|
+
|
|
29
|
+
type RouterProps = {
|
|
30
|
+
/** SSR URL passthrough. On the server, the page handler forwards
|
|
31
|
+
* `request.url` here. On the client, this prop is omitted and the
|
|
32
|
+
* router reads `window.location` instead. */
|
|
33
|
+
url?: string;
|
|
34
|
+
/** Optional URL prefix the router operates under. Stacks with
|
|
35
|
+
* parent `<Router basepath>` blocks for nested routers. */
|
|
36
|
+
basepath?: string;
|
|
37
|
+
/** `'history'` (default, clean URLs) or `'hash'` (`/#/path`,
|
|
38
|
+
* for static deploys). */
|
|
39
|
+
mode?: RouterMode;
|
|
40
|
+
children?: Snippet;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let {
|
|
44
|
+
url,
|
|
45
|
+
basepath = '',
|
|
46
|
+
mode = 'history',
|
|
47
|
+
children
|
|
48
|
+
}: RouterProps = $props();
|
|
49
|
+
|
|
50
|
+
const parent = getContext<RouterRegistry | undefined>(ROUTER_CONTEXT_KEY);
|
|
51
|
+
const stackedBasepath = parent
|
|
52
|
+
? joinBasepath(parent.basepath, basepath)
|
|
53
|
+
: basepath === ''
|
|
54
|
+
? ''
|
|
55
|
+
: basepath.startsWith('/')
|
|
56
|
+
? basepath
|
|
57
|
+
: `/${basepath}`;
|
|
58
|
+
const stackedMode: RouterMode = parent?.mode ?? mode;
|
|
59
|
+
const isOutermost = parent === undefined;
|
|
60
|
+
|
|
61
|
+
// Specificity ranking across siblings: each <Route> registers with
|
|
62
|
+
// its compiled pattern + a stable mount-order index. The winner is
|
|
63
|
+
// $derived from the current URL — highest score wins; ties break by
|
|
64
|
+
// earlier registration order.
|
|
65
|
+
const routes = $state(new Map<string, CompiledPatternBox>());
|
|
66
|
+
let routeCounter = 0;
|
|
67
|
+
|
|
68
|
+
const registry: RouterRegistry = {
|
|
69
|
+
basepath: stackedBasepath,
|
|
70
|
+
deregister: (id) => {
|
|
71
|
+
routes.delete(id);
|
|
72
|
+
},
|
|
73
|
+
isWinner: (id) => winnerId === id,
|
|
74
|
+
mode: stackedMode,
|
|
75
|
+
nextRouteId: () => `r${routeCounter++}`,
|
|
76
|
+
register: (id, entry) => {
|
|
77
|
+
routes.set(id, entry);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const winnerId = $derived.by(() => {
|
|
82
|
+
let bestId: string | null = null;
|
|
83
|
+
let bestScore = -Infinity;
|
|
84
|
+
let bestOrder = Infinity;
|
|
85
|
+
|
|
86
|
+
for (const [id, entry] of routes) {
|
|
87
|
+
const match = matchPattern(entry.pattern, page.url.pathname);
|
|
88
|
+
if (!match.matched) continue;
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
entry.pattern.score > bestScore ||
|
|
92
|
+
(entry.pattern.score === bestScore &&
|
|
93
|
+
entry.registrationOrder < bestOrder)
|
|
94
|
+
) {
|
|
95
|
+
bestScore = entry.pattern.score;
|
|
96
|
+
bestOrder = entry.registrationOrder;
|
|
97
|
+
bestId = id;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return bestId;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
setContext<RouterRegistry>(ROUTER_CONTEXT_KEY, registry);
|
|
105
|
+
|
|
106
|
+
if (isOutermost) {
|
|
107
|
+
setRouterMode(stackedMode);
|
|
108
|
+
|
|
109
|
+
const baseUrl =
|
|
110
|
+
typeof window !== 'undefined' ? window.location.href : (url ?? '/');
|
|
111
|
+
const fullUrl =
|
|
112
|
+
typeof window !== 'undefined'
|
|
113
|
+
? new URL(baseUrl)
|
|
114
|
+
: new URL(baseUrl, 'http://localhost/');
|
|
115
|
+
|
|
116
|
+
const routablePathname =
|
|
117
|
+
stackedMode === 'hash' ? hashPathnameOf(fullUrl) : fullUrl.pathname;
|
|
118
|
+
const initial = new URL(fullUrl.href);
|
|
119
|
+
initial.pathname = routablePathname;
|
|
120
|
+
seedPage(initial);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
onMount(() => {
|
|
124
|
+
if (!isOutermost) return;
|
|
125
|
+
|
|
126
|
+
const onPopState = (event: PopStateEvent) => {
|
|
127
|
+
const next = new URL(window.location.href);
|
|
128
|
+
const routable =
|
|
129
|
+
stackedMode === 'hash' ? hashPathnameOf(next) : next.pathname;
|
|
130
|
+
const synthetic = new URL(next.href);
|
|
131
|
+
synthetic.pathname = routable;
|
|
132
|
+
setPage({
|
|
133
|
+
params: {},
|
|
134
|
+
state: event.state ?? null,
|
|
135
|
+
url: synthetic
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const onHashChange = () => {
|
|
140
|
+
const next = new URL(window.location.href);
|
|
141
|
+
const synthetic = new URL(next.href);
|
|
142
|
+
synthetic.pathname = hashPathnameOf(next);
|
|
143
|
+
setPage({
|
|
144
|
+
params: {},
|
|
145
|
+
state: window.history.state ?? null,
|
|
146
|
+
url: synthetic
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
window.addEventListener('popstate', onPopState);
|
|
151
|
+
if (stackedMode === 'hash') {
|
|
152
|
+
window.addEventListener('hashchange', onHashChange);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return () => {
|
|
156
|
+
window.removeEventListener('popstate', onPopState);
|
|
157
|
+
if (stackedMode === 'hash') {
|
|
158
|
+
window.removeEventListener('hashchange', onHashChange);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
{@render children?.()}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { RouterMode } from '../../types/svelteRouter';
|
|
3
|
+
|
|
4
|
+
type RouterProps = {
|
|
5
|
+
url?: string;
|
|
6
|
+
basepath?: string;
|
|
7
|
+
mode?: RouterMode;
|
|
8
|
+
children?: Snippet;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
declare const __propDef: { props: RouterProps };
|
|
12
|
+
type Props = typeof __propDef.props;
|
|
13
|
+
|
|
14
|
+
import { SvelteComponent } from 'svelte';
|
|
15
|
+
|
|
16
|
+
export default class Router extends SvelteComponent<Props> {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Browser entry — same shape as ./index.ts. Components are imported by
|
|
2
|
+
// their .svelte path; this entry exposes only the runtime API.
|
|
3
|
+
|
|
4
|
+
export { goto } from './goto';
|
|
5
|
+
export { page } from './page.svelte';
|
|
6
|
+
export { pushState, replaceState } from './pushState';
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
ExtractRouteParams,
|
|
10
|
+
GotoOptions,
|
|
11
|
+
LinkPrefetchMode,
|
|
12
|
+
PageState,
|
|
13
|
+
RouterMode
|
|
14
|
+
} from '../../types/svelteRouter';
|