@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.
@@ -0,0 +1,89 @@
1
+ import type { GotoOptions, RouterMode } from '../../types/svelteRouter';
2
+ import { buildHashHref } from './hashMode';
3
+ import { setPage } from './page.svelte';
4
+ import { consumePrefetch } from './prefetchCache';
5
+ import { withViewTransition } from './viewTransitions';
6
+
7
+ let activeMode: RouterMode = 'history';
8
+
9
+ /**
10
+ * Internal — called by Router.svelte on mount so navigation primitives
11
+ * know which URL strategy to use. Hash mode rewrites the URL bar to
12
+ * `#/path` instead of `/path`.
13
+ */
14
+ export const setRouterMode = (mode: RouterMode) => {
15
+ activeMode = mode;
16
+ };
17
+
18
+ const resolveAbsoluteUrl = (target: string) => {
19
+ if (typeof window === 'undefined') {
20
+ // Programmatic goto on the server is rare but we tolerate it as a
21
+ // way for tests to drive the router without a real DOM.
22
+ return new URL(target, 'http://localhost/');
23
+ }
24
+
25
+ return new URL(target, window.location.href);
26
+ };
27
+
28
+ const isExternal = (target: URL) => {
29
+ if (typeof window === 'undefined') return false;
30
+
31
+ return target.origin !== window.location.origin;
32
+ };
33
+
34
+ const writeHistory = (target: URL, options: GotoOptions) => {
35
+ if (typeof window === 'undefined') return;
36
+
37
+ const href =
38
+ activeMode === 'hash'
39
+ ? `${window.location.pathname}${window.location.search}${buildHashHref(target.pathname + target.search)}`
40
+ : `${target.pathname}${target.search}${target.hash}`;
41
+
42
+ const method = options.replaceState
43
+ ? window.history.replaceState
44
+ : window.history.pushState;
45
+ method.call(window.history, options.state ?? null, '', href);
46
+ };
47
+
48
+ const applyScrollAndFocus = (options: GotoOptions) => {
49
+ if (typeof window === 'undefined') return;
50
+
51
+ if (!options.noScroll) window.scrollTo({ left: 0, top: 0 });
52
+ if (!options.keepFocus && document.activeElement instanceof HTMLElement) {
53
+ document.activeElement.blur();
54
+ }
55
+ };
56
+
57
+ /**
58
+ * Programmatically navigate to a URL. Updates `page.url`, writes history,
59
+ * and (when supported) wraps the swap in `document.startViewTransition`.
60
+ *
61
+ * Mirrors SvelteKit's `goto` from `$app/navigation` — same name, same
62
+ * options shape, so a SvelteKit user finds the primitive familiar.
63
+ */
64
+ export const goto = async (target: string, options: GotoOptions = {}) => {
65
+ const url = resolveAbsoluteUrl(target);
66
+
67
+ if (isExternal(url)) {
68
+ // External URLs go through the browser — we don't try to SPA them.
69
+ if (typeof window !== 'undefined') {
70
+ window.location.href = url.href;
71
+ }
72
+
73
+ return;
74
+ }
75
+
76
+ consumePrefetch(target);
77
+
78
+ const mutate = () => {
79
+ writeHistory(url, options);
80
+ setPage({
81
+ params: {},
82
+ state: options.state ?? null,
83
+ url
84
+ });
85
+ applyScrollAndFocus(options);
86
+ };
87
+
88
+ await withViewTransition(mutate);
89
+ };
@@ -0,0 +1,37 @@
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
+ /**
12
+ * Extract the routable pathname from a full URL when hash mode is on.
13
+ * Returns the part after `#/`, prefixed with `/` so it parses as a
14
+ * normal pathname.
15
+ */
16
+ export const hashPathnameOf = (url: URL) => {
17
+ const hash = url.hash;
18
+ if (!hash || hash === '#') return '/';
19
+
20
+ // Tolerate both `#/foo` and `#foo`.
21
+ const trimmed = hash.startsWith('#/') ? hash.slice(2) : hash.slice(1);
22
+
23
+ if (trimmed === '') return '/';
24
+
25
+ return `/${trimmed.replace(/^\/+/, '')}`;
26
+ };
27
+
28
+ /**
29
+ * Build a hash URL from a routable pathname. Used by goto() / pushState()
30
+ * when in hash mode — converts `/dashboard/settings` to `#/dashboard/settings`
31
+ * and writes the result back to the URL.
32
+ */
33
+ export const buildHashHref = (pathname: string) => {
34
+ const trimmed = pathname.replace(/^\/+/, '');
35
+
36
+ return trimmed === '' ? '#/' : `#/${trimmed}`;
37
+ };
@@ -0,0 +1,23 @@
1
+ // Svelte components are imported by their .svelte path, not from this entry:
2
+ // import Router from '@absolutejs/absolute/svelte/router/Router.svelte';
3
+ // import Route from '@absolutejs/absolute/svelte/router/Route.svelte';
4
+ // import Link from '@absolutejs/absolute/svelte/router/Link.svelte';
5
+ //
6
+ // This entry only re-exports the non-component runtime API (programmatic
7
+ // navigation, reactive state, shallow routing). It mirrors the existing
8
+ // `@absolutejs/absolute/svelte/components/*.svelte` convention used by
9
+ // the framework's other Svelte components (Island, Image, StreamSlot,
10
+ // etc.) so user .svelte files can import the components directly via
11
+ // AbsoluteJS's Svelte compile pipeline.
12
+
13
+ export { goto } from './goto';
14
+ export { page } from './page.svelte';
15
+ export { pushState, replaceState } from './pushState';
16
+
17
+ export type {
18
+ ExtractRouteParams,
19
+ GotoOptions,
20
+ LinkPrefetchMode,
21
+ PageState,
22
+ RouterMode
23
+ } from '../../types/svelteRouter';
@@ -0,0 +1,158 @@
1
+ import type {
2
+ ExtractRouteParams,
3
+ RouteMatchResult
4
+ } from '../../types/svelteRouter';
5
+
6
+ type CompiledSegment =
7
+ | { kind: 'static'; value: string }
8
+ | { kind: 'param'; name: string; optional: boolean }
9
+ | { kind: 'wildcard' };
10
+
11
+ type CompiledPattern = {
12
+ segments: CompiledSegment[];
13
+ score: number;
14
+ };
15
+
16
+ const STATIC_SEGMENT_WEIGHT = 100;
17
+ const PARAM_SEGMENT_WEIGHT = 10;
18
+ const WILDCARD_SEGMENT_WEIGHT = 1;
19
+ const OPTIONAL_PENALTY = 1;
20
+
21
+ const splitPath = (path: string) => {
22
+ const trimmed = path.replace(/^\/+/, '').replace(/\/+$/, '');
23
+ if (trimmed === '') return [];
24
+
25
+ return trimmed.split('/');
26
+ };
27
+
28
+ const compileSegment = (raw: string): CompiledSegment => {
29
+ if (raw === '*' || raw.startsWith('*')) {
30
+ return { kind: 'wildcard' };
31
+ }
32
+
33
+ if (raw.startsWith(':')) {
34
+ const body = raw.slice(1);
35
+ const optional = body.endsWith('?');
36
+ const name = optional ? body.slice(0, -1) : body;
37
+
38
+ return { kind: 'param', name, optional };
39
+ }
40
+
41
+ return { kind: 'static', value: raw };
42
+ };
43
+
44
+ /**
45
+ * Compile a `<Route path>` pattern into segments + a specificity score.
46
+ * Higher score = more specific (longer static prefix beats parameterised).
47
+ */
48
+ export const compilePattern = (pattern: string): CompiledPattern => {
49
+ const segments = splitPath(pattern).map(compileSegment);
50
+
51
+ let score = 0;
52
+ for (const segment of segments) {
53
+ if (segment.kind === 'static') score += STATIC_SEGMENT_WEIGHT;
54
+ else if (segment.kind === 'param') {
55
+ score += PARAM_SEGMENT_WEIGHT;
56
+ if (segment.optional) score -= OPTIONAL_PENALTY;
57
+ } else if (segment.kind === 'wildcard')
58
+ score += WILDCARD_SEGMENT_WEIGHT;
59
+ }
60
+
61
+ return { segments, score };
62
+ };
63
+
64
+ /**
65
+ * Match a URL pathname against a compiled pattern. Returns the extracted
66
+ * params on a successful match, or a miss otherwise.
67
+ */
68
+ export const matchPattern = <Path extends string>(
69
+ pattern: CompiledPattern,
70
+ pathname: string
71
+ ): RouteMatchResult<ExtractRouteParams<Path>> => {
72
+ const pathSegments = splitPath(pathname);
73
+ const params: Record<string, string | undefined> = {};
74
+
75
+ let pi = 0;
76
+ for (let si = 0; si < pattern.segments.length; si++) {
77
+ const segment = pattern.segments[si];
78
+ if (!segment) continue;
79
+
80
+ if (segment.kind === 'wildcard') {
81
+ params['wildcard'] = pathSegments.slice(pi).join('/');
82
+ return {
83
+ matched: true,
84
+ params: params as ExtractRouteParams<Path>
85
+ };
86
+ }
87
+
88
+ const candidate = pathSegments[pi];
89
+
90
+ if (candidate === undefined) {
91
+ if (segment.kind === 'param' && segment.optional) {
92
+ params[segment.name] = undefined;
93
+ continue;
94
+ }
95
+
96
+ return { matched: false };
97
+ }
98
+
99
+ if (segment.kind === 'static') {
100
+ if (segment.value !== candidate) return { matched: false };
101
+ pi++;
102
+ continue;
103
+ }
104
+
105
+ // param
106
+ params[segment.name] = candidate;
107
+ pi++;
108
+ }
109
+
110
+ if (pi !== pathSegments.length) {
111
+ return { matched: false };
112
+ }
113
+
114
+ return {
115
+ matched: true,
116
+ params: params as ExtractRouteParams<Path>
117
+ };
118
+ };
119
+
120
+ /**
121
+ * Stable comparator for compiled patterns. Higher specificity sorts first.
122
+ * When two patterns have equal score, declaration order (the original index)
123
+ * decides — passed in via the `index` field on each entry.
124
+ */
125
+ export const comparePatterns = (
126
+ a: { score: number; index: number },
127
+ b: { score: number; index: number }
128
+ ) => {
129
+ if (a.score !== b.score) return b.score - a.score;
130
+
131
+ return a.index - b.index;
132
+ };
133
+
134
+ /**
135
+ * Join a basepath stack with a child pattern, producing an absolute pattern
136
+ * that the route matcher can compile against an incoming pathname.
137
+ *
138
+ * Handles slash edge cases:
139
+ * joinBasepath('', '/users') → '/users'
140
+ * joinBasepath('/portal', '/users') → '/portal/users'
141
+ * joinBasepath('/portal/', '/users') → '/portal/users'
142
+ * joinBasepath('/portal', 'users') → '/portal/users'
143
+ * joinBasepath('/portal', '/') → '/portal'
144
+ */
145
+ export const joinBasepath = (basepath: string, pattern: string) => {
146
+ const trimmedBase = basepath.replace(/\/+$/, '');
147
+ const trimmedPattern = pattern.replace(/^\/+/, '');
148
+
149
+ if (trimmedPattern === '') {
150
+ return trimmedBase === '' ? '/' : trimmedBase;
151
+ }
152
+
153
+ if (trimmedBase === '') {
154
+ return `/${trimmedPattern}`;
155
+ }
156
+
157
+ return `${trimmedBase}/${trimmedPattern}`;
158
+ };
@@ -0,0 +1,57 @@
1
+ import type { PageState } from '../../types/svelteRouter';
2
+
3
+ const initialUrl = () => {
4
+ if (typeof window !== 'undefined') {
5
+ return new URL(window.location.href);
6
+ }
7
+
8
+ // On the server we don't know the URL yet — Router.svelte initializes
9
+ // it from its `url` prop. Use a placeholder that Router.svelte will
10
+ // overwrite immediately.
11
+ return new URL('http://localhost/');
12
+ };
13
+
14
+ const initialState = (): PageState => ({
15
+ params: {},
16
+ state: undefined,
17
+ url: initialUrl()
18
+ });
19
+
20
+ const inner = $state<PageState>(initialState());
21
+
22
+ /**
23
+ * Reactive route state. Mirrors SvelteKit's `page` from `$app/state`:
24
+ *
25
+ * import { page } from '@absolutejs/absolute/svelte/router';
26
+ * page.url.pathname // current path (reactive)
27
+ * page.url.searchParams // parsed URLSearchParams (reactive)
28
+ * page.params.id // active route params (reactive)
29
+ * page.state // history.state for the current entry
30
+ *
31
+ * Backed by `$state`. Direct property access in templates re-renders.
32
+ */
33
+ export const page = inner;
34
+
35
+ /**
36
+ * Internal — only Router.svelte and the navigation primitives call this.
37
+ * Replaces the entire page state in one assignment so subscribers fire
38
+ * once per navigation rather than once per mutated field.
39
+ */
40
+ export const setPage = (next: Partial<PageState>) => {
41
+ if (next.url !== undefined) inner.url = next.url;
42
+ if (next.params !== undefined) inner.params = next.params;
43
+ if (next.state !== undefined) inner.state = next.state;
44
+ };
45
+
46
+ /**
47
+ * Internal — used during SSR to seed the page state before render so
48
+ * `<Route>` blocks see the correct `page.url`.
49
+ */
50
+ export const seedPage = (
51
+ url: URL,
52
+ params: Record<string, string | undefined> = {}
53
+ ) => {
54
+ inner.url = url;
55
+ inner.params = params;
56
+ inner.state = undefined;
57
+ };
@@ -0,0 +1,89 @@
1
+ const PREFETCH_CACHE_LIMIT = 16;
2
+ const HOVER_DEBOUNCE_MS = 250;
3
+
4
+ type CacheEntry = {
5
+ url: string;
6
+ promise: Promise<Response>;
7
+ };
8
+
9
+ const cache = new Map<string, CacheEntry>();
10
+
11
+ const isSlowConnection = () => {
12
+ if (typeof navigator === 'undefined') return false;
13
+
14
+ const connection = (
15
+ navigator as Navigator & {
16
+ connection?: { saveData?: boolean };
17
+ }
18
+ ).connection;
19
+
20
+ return connection?.saveData === true;
21
+ };
22
+
23
+ const prefersReducedData = () => {
24
+ if (typeof window === 'undefined' || !window.matchMedia) return false;
25
+
26
+ return window.matchMedia('(prefers-reduced-data: reduce)').matches;
27
+ };
28
+
29
+ const evictOldest = () => {
30
+ const oldest = cache.keys().next();
31
+ if (oldest.done) return;
32
+ cache.delete(oldest.value);
33
+ };
34
+
35
+ /**
36
+ * Prefetch a URL into the in-memory cache. No-op if the user has signalled
37
+ * data-saver / reduced-data, or if the URL is already cached.
38
+ */
39
+ export const prefetch = (url: string) => {
40
+ if (typeof fetch === 'undefined') return;
41
+ if (isSlowConnection() || prefersReducedData()) return;
42
+ if (cache.has(url)) return;
43
+
44
+ while (cache.size >= PREFETCH_CACHE_LIMIT) evictOldest();
45
+
46
+ const promise = fetch(url, { credentials: 'same-origin' }).catch(
47
+ () => new Response(null, { status: 0 })
48
+ );
49
+ cache.set(url, { promise, url });
50
+ };
51
+
52
+ /**
53
+ * Consume a cached prefetch entry on actual navigation, removing it from
54
+ * the cache. Returns the cached Promise<Response> or undefined.
55
+ */
56
+ export const consumePrefetch = (url: string) => {
57
+ const entry = cache.get(url);
58
+ if (!entry) return undefined;
59
+ cache.delete(url);
60
+
61
+ return entry.promise;
62
+ };
63
+
64
+ export const clearPrefetchCache = () => {
65
+ cache.clear();
66
+ };
67
+
68
+ type HoverHandle = {
69
+ cancel: () => void;
70
+ };
71
+
72
+ /**
73
+ * Wrap a prefetch trigger in a hover-debounce so glancing across many links
74
+ * doesn't fire a fetch storm. The returned handle's `cancel()` aborts the
75
+ * pending hover prefetch (e.g. on `pointerleave`).
76
+ */
77
+ export const scheduleHoverPrefetch = (url: string): HoverHandle => {
78
+ if (typeof window === 'undefined') {
79
+ return { cancel: () => {} };
80
+ }
81
+
82
+ const timer = window.setTimeout(() => {
83
+ prefetch(url);
84
+ }, HOVER_DEBOUNCE_MS);
85
+
86
+ return {
87
+ cancel: () => window.clearTimeout(timer)
88
+ };
89
+ };
@@ -0,0 +1,35 @@
1
+ import { setPage } from './page.svelte';
2
+
3
+ const resolveTarget = (target: string) => {
4
+ if (typeof window === 'undefined')
5
+ return new URL(target, 'http://localhost/');
6
+
7
+ return new URL(target, window.location.href);
8
+ };
9
+
10
+ /**
11
+ * Shallow routing: update the URL bar and `page.state` without re-running
12
+ * `<Route>` matching. Useful for modals / drawers / overlays that want a
13
+ * shareable URL without swapping the active route's content.
14
+ *
15
+ * Mirrors SvelteKit's `pushState` from `$app/navigation`.
16
+ */
17
+ export const pushState = (target: string, state: unknown) => {
18
+ if (typeof window === 'undefined') return;
19
+
20
+ const url = resolveTarget(target);
21
+ window.history.pushState(state, '', url.href);
22
+ setPage({ state, url });
23
+ };
24
+
25
+ /**
26
+ * Same as `pushState` but uses `history.replaceState`. Mirrors SvelteKit's
27
+ * `replaceState` from `$app/navigation`.
28
+ */
29
+ export const replaceState = (target: string, state: unknown) => {
30
+ if (typeof window === 'undefined') return;
31
+
32
+ const url = resolveTarget(target);
33
+ window.history.replaceState(state, '', url.href);
34
+ setPage({ state, url });
35
+ };
@@ -0,0 +1,31 @@
1
+ type StartViewTransition = (callback: () => void | Promise<void>) => {
2
+ finished: Promise<void>;
3
+ };
4
+
5
+ const supportsViewTransitions = () => {
6
+ if (typeof document === 'undefined') return false;
7
+
8
+ return (
9
+ typeof (document as { startViewTransition?: StartViewTransition })
10
+ .startViewTransition === 'function'
11
+ );
12
+ };
13
+
14
+ /**
15
+ * Wrap a state mutation in `document.startViewTransition` when supported.
16
+ * Falls through to a synchronous call otherwise. Reduced-motion users get
17
+ * instant swaps via the browser's own handling of `prefers-reduced-motion`.
18
+ */
19
+ export const withViewTransition = async (
20
+ mutate: () => void | Promise<void>
21
+ ) => {
22
+ if (!supportsViewTransitions()) {
23
+ await mutate();
24
+
25
+ return;
26
+ }
27
+
28
+ const start = (document as { startViewTransition: StartViewTransition })
29
+ .startViewTransition;
30
+ await start(() => mutate()).finished;
31
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Public type surface for the AbsoluteJS Svelte router.
3
+ *
4
+ * `ExtractRouteParams<P>` walks a path-pattern string literal and produces
5
+ * a typed `params` shape:
6
+ *
7
+ * ExtractRouteParams<'/users/:id'> → { id: string }
8
+ * ExtractRouteParams<'/users/:id/posts/:pid'> → { id: string; pid: string }
9
+ * ExtractRouteParams<'/users/:id?'> → { id: string | undefined }
10
+ * ExtractRouteParams<'/files/*'> → { wildcard: string }
11
+ * ExtractRouteParams<'/dashboard'> → Record<string, never>
12
+ *
13
+ * Edge cases:
14
+ * - Optional params (`:name?`) appear as `string | undefined`
15
+ * - Wildcard tail (`*`) is exposed as the `wildcard` key
16
+ * - Same-name twice in one path is intentionally not detected at the type
17
+ * level (it's a logic bug in the user's pattern, not something the type
18
+ * system meaningfully rescues).
19
+ */
20
+ type IsOptionalParamSegment<Segment extends string> = Segment extends `${string}?` ? true : false;
21
+ type StripOptionalSuffix<Segment extends string> = Segment extends `${infer Name}?` ? Name : Segment;
22
+ type WildcardSegment = '*' | `*${string}`;
23
+ type ParseSegment<Segment extends string> = Segment extends WildcardSegment ? {
24
+ wildcard: string;
25
+ } : Segment extends `:${infer Name}` ? IsOptionalParamSegment<Name> extends true ? {
26
+ [K in StripOptionalSuffix<Name>]: string | undefined;
27
+ } : {
28
+ [K in Name]: string;
29
+ } : Record<never, never>;
30
+ type SplitPath<Path extends string> = Path extends `/${infer Rest}` ? SplitPath<Rest> : Path extends `${infer Head}/${infer Tail}` ? ParseSegment<Head> & SplitPath<Tail> : ParseSegment<Path>;
31
+ type Simplify<T> = {
32
+ [K in keyof T]: T[K];
33
+ } & {};
34
+ export type ExtractRouteParams<Path extends string> = string extends Path ? Record<string, string> : Simplify<SplitPath<Path>> extends infer Result ? keyof Result extends never ? Record<string, never> : Result : never;
35
+ export type RouteMatch<Params extends Record<string, unknown>> = {
36
+ matched: true;
37
+ params: Params;
38
+ };
39
+ export type RouteMiss = {
40
+ matched: false;
41
+ };
42
+ export type RouteMatchResult<Params extends Record<string, unknown>> = RouteMatch<Params> | RouteMiss;
43
+ export type RouterMode = 'history' | 'hash';
44
+ export type GotoOptions = {
45
+ /** Use `history.replaceState` instead of `pushState`. */
46
+ replaceState?: boolean;
47
+ /** Don't reset focus to body on navigation. */
48
+ keepFocus?: boolean;
49
+ /** Don't scroll to top on navigation. */
50
+ noScroll?: boolean;
51
+ /** Value attached to `history.state`. */
52
+ state?: unknown;
53
+ };
54
+ export type LinkPrefetchMode = 'hover' | 'viewport' | 'none';
55
+ export type PageState = {
56
+ url: URL;
57
+ params: Record<string, string | undefined>;
58
+ state: unknown;
59
+ };
60
+ export type RouterContextValue = {
61
+ /** Stacked basepath from outer to inner Router (joined). */
62
+ basepath: string;
63
+ mode: RouterMode;
64
+ };
65
+ export {};
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Public type surface for the AbsoluteJS Svelte router.
3
+ *
4
+ * `ExtractRouteParams<P>` walks a path-pattern string literal and produces
5
+ * a typed `params` shape:
6
+ *
7
+ * ExtractRouteParams<'/users/:id'> → { id: string }
8
+ * ExtractRouteParams<'/users/:id/posts/:pid'> → { id: string; pid: string }
9
+ * ExtractRouteParams<'/users/:id?'> → { id: string | undefined }
10
+ * ExtractRouteParams<'/files/*'> → { wildcard: string }
11
+ * ExtractRouteParams<'/dashboard'> → Record<string, never>
12
+ *
13
+ * Edge cases:
14
+ * - Optional params (`:name?`) appear as `string | undefined`
15
+ * - Wildcard tail (`*`) is exposed as the `wildcard` key
16
+ * - Same-name twice in one path is intentionally not detected at the type
17
+ * level (it's a logic bug in the user's pattern, not something the type
18
+ * system meaningfully rescues).
19
+ */
20
+
21
+ type IsOptionalParamSegment<Segment extends string> =
22
+ Segment extends `${string}?` ? true : false;
23
+
24
+ type StripOptionalSuffix<Segment extends string> =
25
+ Segment extends `${infer Name}?` ? Name : Segment;
26
+
27
+ type WildcardSegment = '*' | `*${string}`;
28
+
29
+ type ParseSegment<Segment extends string> = Segment extends WildcardSegment
30
+ ? { wildcard: string }
31
+ : Segment extends `:${infer Name}`
32
+ ? IsOptionalParamSegment<Name> extends true
33
+ ? { [K in StripOptionalSuffix<Name>]: string | undefined }
34
+ : { [K in Name]: string }
35
+ : Record<never, never>;
36
+
37
+ type SplitPath<Path extends string> = Path extends `/${infer Rest}`
38
+ ? SplitPath<Rest>
39
+ : Path extends `${infer Head}/${infer Tail}`
40
+ ? ParseSegment<Head> & SplitPath<Tail>
41
+ : ParseSegment<Path>;
42
+
43
+ type Simplify<T> = { [K in keyof T]: T[K] } & {};
44
+
45
+ export type ExtractRouteParams<Path extends string> = string extends Path
46
+ ? Record<string, string>
47
+ : Simplify<SplitPath<Path>> extends infer Result
48
+ ? keyof Result extends never
49
+ ? Record<string, never>
50
+ : Result
51
+ : never;
52
+
53
+ export type RouteMatch<Params extends Record<string, unknown>> = {
54
+ matched: true;
55
+ params: Params;
56
+ };
57
+
58
+ export type RouteMiss = {
59
+ matched: false;
60
+ };
61
+
62
+ export type RouteMatchResult<Params extends Record<string, unknown>> =
63
+ | RouteMatch<Params>
64
+ | RouteMiss;
65
+
66
+ export type RouterMode = 'history' | 'hash';
67
+
68
+ export type GotoOptions = {
69
+ /** Use `history.replaceState` instead of `pushState`. */
70
+ replaceState?: boolean;
71
+ /** Don't reset focus to body on navigation. */
72
+ keepFocus?: boolean;
73
+ /** Don't scroll to top on navigation. */
74
+ noScroll?: boolean;
75
+ /** Value attached to `history.state`. */
76
+ state?: unknown;
77
+ };
78
+
79
+ export type LinkPrefetchMode = 'hover' | 'viewport' | 'none';
80
+
81
+ export type PageState = {
82
+ url: URL;
83
+ params: Record<string, string | undefined>;
84
+ state: unknown;
85
+ };
86
+
87
+ export type RouterContextValue = {
88
+ /** Stacked basepath from outer to inner Router (joined). */
89
+ basepath: string;
90
+ mode: RouterMode;
91
+ };