@real-router/solid 0.14.2 → 0.14.4
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/cjs/index.js +0 -0
- package/dist/esm/index.mjs +0 -0
- package/dist/types/dom-utils/link-utils.d.ts.map +1 -1
- package/dist/types/dom-utils/scroll-spy.d.ts.map +1 -1
- package/package.json +7 -8
- package/src/RouterProvider.tsx +0 -112
- package/src/components/Await.tsx +0 -56
- package/src/components/ClientOnly.tsx +0 -20
- package/src/components/HttpStatusCode.tsx +0 -65
- package/src/components/HttpStatusProvider.tsx +0 -21
- package/src/components/Link.tsx +0 -140
- package/src/components/RouteView/RouteView.tsx +0 -59
- package/src/components/RouteView/components.tsx +0 -85
- package/src/components/RouteView/helpers.tsx +0 -0
- package/src/components/RouteView/index.ts +0 -8
- package/src/components/RouteView/types.ts +0 -24
- package/src/components/RouterErrorBoundary.tsx +0 -45
- package/src/components/ServerOnly.tsx +0 -20
- package/src/components/Streamed.tsx +0 -23
- package/src/constants.ts +0 -27
- package/src/context.ts +0 -35
- package/src/createSignalFromSource.ts +0 -71
- package/src/createStoreFromSource.ts +0 -65
- package/src/directives/link.tsx +0 -111
- package/src/directives.d.ts +0 -10
- package/src/hooks/useDeferred.tsx +0 -36
- package/src/hooks/useNavigator.tsx +0 -6
- package/src/hooks/useRoute.tsx +0 -27
- package/src/hooks/useRouteEnter.tsx +0 -121
- package/src/hooks/useRouteExit.tsx +0 -123
- package/src/hooks/useRouteNode.tsx +0 -13
- package/src/hooks/useRouteNodeStore.tsx +0 -12
- package/src/hooks/useRouteStore.tsx +0 -12
- package/src/hooks/useRouteUtils.tsx +0 -50
- package/src/hooks/useRouter.tsx +0 -6
- package/src/hooks/useRouterTransition.tsx +0 -14
- package/src/index.tsx +0 -66
- package/src/ssr.tsx +0 -39
- package/src/types.ts +0 -28
- package/src/utils/createHttpStatusSink.ts +0 -31
- package/src/utils/createMountedSignal.ts +0 -26
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { createDismissableError } from "@real-router/sources";
|
|
2
|
-
import { createEffect, Show } from "solid-js";
|
|
3
|
-
|
|
4
|
-
import { createSignalFromSource } from "../createSignalFromSource";
|
|
5
|
-
import { useRouter } from "../hooks/useRouter";
|
|
6
|
-
|
|
7
|
-
import type { RouterError, State } from "@real-router/core";
|
|
8
|
-
import type { JSX } from "solid-js";
|
|
9
|
-
|
|
10
|
-
export interface RouterErrorBoundaryProps {
|
|
11
|
-
readonly children: JSX.Element;
|
|
12
|
-
readonly fallback: (
|
|
13
|
-
error: RouterError,
|
|
14
|
-
resetError: () => void,
|
|
15
|
-
) => JSX.Element;
|
|
16
|
-
readonly onError?: (
|
|
17
|
-
error: RouterError,
|
|
18
|
-
toRoute: State | null,
|
|
19
|
-
fromRoute: State | null,
|
|
20
|
-
) => void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function RouterErrorBoundary(
|
|
24
|
-
props: RouterErrorBoundaryProps,
|
|
25
|
-
): JSX.Element {
|
|
26
|
-
const router = useRouter();
|
|
27
|
-
const snapshot = createSignalFromSource(createDismissableError(router));
|
|
28
|
-
|
|
29
|
-
createEffect(() => {
|
|
30
|
-
const snap = snapshot();
|
|
31
|
-
|
|
32
|
-
if (snap.error) {
|
|
33
|
-
props.onError?.(snap.error, snap.toRoute, snap.fromRoute);
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<>
|
|
39
|
-
{props.children}
|
|
40
|
-
<Show when={snapshot().error}>
|
|
41
|
-
{(error) => props.fallback(error(), snapshot().resetError)}
|
|
42
|
-
</Show>
|
|
43
|
-
</>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { Show } from "solid-js";
|
|
2
|
-
|
|
3
|
-
import { createMountedSignal } from "../utils/createMountedSignal";
|
|
4
|
-
|
|
5
|
-
import type { JSX } from "solid-js";
|
|
6
|
-
|
|
7
|
-
export interface ServerOnlyProps {
|
|
8
|
-
readonly children: JSX.Element;
|
|
9
|
-
readonly fallback?: JSX.Element;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function ServerOnly(props: ServerOnlyProps): JSX.Element {
|
|
13
|
-
const mounted = createMountedSignal();
|
|
14
|
-
|
|
15
|
-
return (
|
|
16
|
-
<Show when={mounted()} fallback={props.children}>
|
|
17
|
-
{props.fallback}
|
|
18
|
-
</Show>
|
|
19
|
-
);
|
|
20
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { Suspense } from "solid-js";
|
|
2
|
-
|
|
3
|
-
import type { JSX } from "solid-js";
|
|
4
|
-
|
|
5
|
-
export interface StreamedProps {
|
|
6
|
-
/** Shown while any descendant `<Await>` / `createResource` suspends. */
|
|
7
|
-
readonly fallback: JSX.Element;
|
|
8
|
-
readonly children: JSX.Element;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Cross-adapter alias for Solid's `<Suspense fallback={…}>`. Symmetric naming
|
|
13
|
-
* with the React/Preact/Svelte/Vue/Angular `<Streamed>` components — pick
|
|
14
|
-
* `<Streamed>` for cross-framework consistency, or use Solid's native
|
|
15
|
-
* `<Suspense>` directly when team conventions prefer that.
|
|
16
|
-
*
|
|
17
|
-
* Solid's `<Suspense>` is a built-in primitive; out-of-order resolution +
|
|
18
|
-
* splice scripts during `renderToStream` are part of the runtime. See
|
|
19
|
-
* Solid's SSR docs for the wire-format details.
|
|
20
|
-
*/
|
|
21
|
-
export function Streamed(props: StreamedProps): JSX.Element {
|
|
22
|
-
return <Suspense fallback={props.fallback}>{props.children}</Suspense>;
|
|
23
|
-
}
|
package/src/constants.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stable empty object for default params.
|
|
3
|
-
*
|
|
4
|
-
* `Object.freeze` makes mutation throw under ESM strict mode — this guards
|
|
5
|
-
* against accidental writes that would corrupt the shared default across
|
|
6
|
-
* every Link without explicit params.
|
|
7
|
-
*
|
|
8
|
-
* §8.1 audit note (LOW #19): consumers cast `EMPTY_PARAMS as P` at usage
|
|
9
|
-
* sites (e.g. `Link.tsx`, `directives/link.tsx`). The cast is required for
|
|
10
|
-
* type compatibility with the generic `P extends Params` and DOES technically
|
|
11
|
-
* widen the `Readonly<{}>` type, but the underlying object stays frozen at
|
|
12
|
-
* runtime — any attempt to mutate fails at the JS engine level regardless
|
|
13
|
-
* of TS-level visibility. The frozen sentinel is also used by Link's
|
|
14
|
-
* fast-path identity check (`props.routeParams === undefined` after the
|
|
15
|
-
* §8.1 audit fix); changing this object's identity would silently break
|
|
16
|
-
* that path.
|
|
17
|
-
*/
|
|
18
|
-
export const EMPTY_PARAMS = Object.freeze({});
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Stable empty options object.
|
|
22
|
-
*
|
|
23
|
-
* Same freeze/cast guarantees as `EMPTY_PARAMS` — the sentinel is shared
|
|
24
|
-
* across all default `routeOptions` consumers (`Link`, `use:link`) to
|
|
25
|
-
* avoid per-render `{}` allocations.
|
|
26
|
-
*/
|
|
27
|
-
export const EMPTY_OPTIONS = Object.freeze({});
|
package/src/context.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext } from "solid-js";
|
|
2
|
-
|
|
3
|
-
import type { RouteState } from "./types";
|
|
4
|
-
import type { Router, Navigator } from "@real-router/core";
|
|
5
|
-
import type { Accessor } from "solid-js";
|
|
6
|
-
|
|
7
|
-
export interface RouterContextValue {
|
|
8
|
-
router: Router;
|
|
9
|
-
navigator: Navigator;
|
|
10
|
-
routeSelector: (routeName: string) => boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const RouterContext = createContext<RouterContextValue | null>(null);
|
|
14
|
-
|
|
15
|
-
export const RouteContext = createContext<Accessor<RouteState> | null>(null);
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Read the required RouterContext or throw a labelled error. Internal helper
|
|
19
|
-
* — consolidates 4 copies of the same `useContext + null-check + throw`
|
|
20
|
-
* block across the public hooks/components/directives. The `consumerName`
|
|
21
|
-
* parameter keeps each callsite's error message specific (so the consumer
|
|
22
|
-
* sees "useRouter must be used within a RouterProvider", not a generic
|
|
23
|
-
* "context missing" message).
|
|
24
|
-
*/
|
|
25
|
-
export function useRequiredRouterContext(
|
|
26
|
-
consumerName: string,
|
|
27
|
-
): RouterContextValue {
|
|
28
|
-
const ctx = useContext(RouterContext);
|
|
29
|
-
|
|
30
|
-
if (!ctx) {
|
|
31
|
-
throw new Error(`${consumerName} must be used within a RouterProvider`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return ctx;
|
|
35
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { createSignal, onCleanup } from "solid-js";
|
|
2
|
-
|
|
3
|
-
import type { RouterSource } from "@real-router/sources";
|
|
4
|
-
import type { Accessor } from "solid-js";
|
|
5
|
-
|
|
6
|
-
export function createSignalFromSource<T>(
|
|
7
|
-
source: RouterSource<T>,
|
|
8
|
-
): Accessor<T> {
|
|
9
|
-
// Mini-sprint E.5 (audit-5 §4.2 #7) — defensive init-phase snapshot
|
|
10
|
-
// reads. A throwing `getSnapshot()` during construction would
|
|
11
|
-
// propagate up through `createSignal<T>(...)` (or the post-subscribe
|
|
12
|
-
// re-sync below) into the reactive owner, tearing down the entire
|
|
13
|
-
// RouterProvider subtree (and any siblings sharing the owner). Catch
|
|
14
|
-
// + log + fall back to `undefined` (initial) or skip-update (post-
|
|
15
|
-
// subscribe re-sync) so the accessor still constructs; the next
|
|
16
|
-
// emit refreshes the value.
|
|
17
|
-
//
|
|
18
|
-
// Post-init emit-time throws are NOT wrapped — they bubble to Solid's
|
|
19
|
-
// `<ErrorBoundary>` (or surface as unhandled errors in dev) so
|
|
20
|
-
// genuine source bugs aren't silently masked.
|
|
21
|
-
let initial: T;
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
initial = source.getSnapshot();
|
|
25
|
-
} catch (error) {
|
|
26
|
-
console.error(
|
|
27
|
-
"[real-router] createSignalFromSource: initial getSnapshot threw — accessor defaulting to undefined.",
|
|
28
|
-
error,
|
|
29
|
-
);
|
|
30
|
-
initial = undefined as T;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const [value, setValue] = createSignal<T>(initial);
|
|
34
|
-
|
|
35
|
-
// `sync` is a stable reference (defined once at outer scope) so the
|
|
36
|
-
// subscribe callback below does not re-allocate it per emit. Solid's
|
|
37
|
-
// `setValue(fn)` treats fn as an updater `(prev) => next`; our updater
|
|
38
|
-
// ignores `prev` and reads the latest snapshot fresh, which gives a
|
|
39
|
-
// function-form micro-allocation cost (one extra fn call per emit) BUT
|
|
40
|
-
// a much smaller TS surface than the `setValue(value)` direct form —
|
|
41
|
-
// that overload is typed `Exclude<T, Function>`, requiring per-call
|
|
42
|
-
// `as Exclude<T, (...args: never[]) => unknown>` casts for generic T.
|
|
43
|
-
// The micro-opt is not worth the cast complexity.
|
|
44
|
-
// See §8.2 audit note.
|
|
45
|
-
const sync = (): T => source.getSnapshot();
|
|
46
|
-
|
|
47
|
-
const unsubscribe = source.subscribe(() => {
|
|
48
|
-
setValue(sync);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// Re-read after subscribe: lazy sources reconcile their snapshot in
|
|
52
|
-
// onFirstSubscribe (when reused after disconnect via cache). Listener
|
|
53
|
-
// is not notified for that internal update, so we must sync manually.
|
|
54
|
-
// No-op when snapshot is unchanged (signal equality check). Wrapped
|
|
55
|
-
// because this is still init-phase: a throw here ALSO tears down the
|
|
56
|
-
// owner, same as the initial read above.
|
|
57
|
-
try {
|
|
58
|
-
setValue(sync);
|
|
59
|
-
} catch (error) {
|
|
60
|
-
console.error(
|
|
61
|
-
"[real-router] createSignalFromSource: post-subscribe getSnapshot threw — accessor retains initial value.",
|
|
62
|
-
error,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
onCleanup(() => {
|
|
67
|
-
unsubscribe();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
return value;
|
|
71
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { onCleanup } from "solid-js";
|
|
2
|
-
import { createStore, reconcile } from "solid-js/store";
|
|
3
|
-
|
|
4
|
-
import type { RouterSource } from "@real-router/sources";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Bridges a `RouterSource<T>` into a Solid store (`createStore` + `reconcile`).
|
|
8
|
-
*
|
|
9
|
-
* Unlike `createSignalFromSource` (whole-value replacement via `===`), this
|
|
10
|
-
* bridge uses `reconcile` on every emit so **unchanged nested paths retain
|
|
11
|
-
* their object identity**. Components that read only `state.route.name` will
|
|
12
|
-
* not re-run when `state.route.params` changes — granular reactivity without
|
|
13
|
-
* manual memoisation.
|
|
14
|
-
*
|
|
15
|
-
* **Ownership**: calls `onCleanup` — must be called inside a reactive owner
|
|
16
|
-
* (component body or `createRoot`). Same contract as `createSignalFromSource`.
|
|
17
|
-
*
|
|
18
|
-
* **Lazy-source re-sync**: after `source.subscribe()`, a cached lazy source
|
|
19
|
-
* may reconcile its snapshot in `onFirstSubscribe`. The listener is not
|
|
20
|
-
* notified for that internal update, so we re-read immediately after
|
|
21
|
-
* subscribing (`setState(reconcile(source.getSnapshot()))`) — mirrors the
|
|
22
|
-
* same pattern in `createSignalFromSource`. `reconcile` is a no-op when the
|
|
23
|
-
* snapshot is structurally unchanged, so there is no spurious reactivity cost.
|
|
24
|
-
*/
|
|
25
|
-
export function createStoreFromSource<T extends object>(
|
|
26
|
-
source: RouterSource<T>,
|
|
27
|
-
): T {
|
|
28
|
-
const initialSnapshot = source.getSnapshot();
|
|
29
|
-
const [state, setState] = createStore<T>({ ...initialSnapshot });
|
|
30
|
-
|
|
31
|
-
// Track the last reconciled snapshot reference to short-circuit redundant
|
|
32
|
-
// `reconcile` calls. Cached lazy sources (e.g. `createRouteNodeSource`)
|
|
33
|
-
// stabilize their snapshot — the same reference flows through multiple
|
|
34
|
-
// emits when nothing in the node's slice changed. `reconcile` itself
|
|
35
|
-
// handles identity (no-ops on structurally-equal input), but a reference
|
|
36
|
-
// check is cheaper than the structural walk and avoids the function call
|
|
37
|
-
// entirely on every navigation × N store consumers (§8b H10 audit fix).
|
|
38
|
-
let lastSnapshot: T = initialSnapshot;
|
|
39
|
-
|
|
40
|
-
const unsubscribe = source.subscribe(() => {
|
|
41
|
-
const nextSnapshot = source.getSnapshot();
|
|
42
|
-
|
|
43
|
-
if (nextSnapshot === lastSnapshot) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
lastSnapshot = nextSnapshot;
|
|
48
|
-
setState(reconcile(nextSnapshot));
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// Re-read after subscribe: lazy sources reconcile their snapshot in
|
|
52
|
-
// onFirstSubscribe (when reused after disconnect via cache). The listener
|
|
53
|
-
// is not notified for that internal update, so we must reconcile manually.
|
|
54
|
-
// Guarded by the same reference check so a no-op stays free.
|
|
55
|
-
const afterSubscribe = source.getSnapshot();
|
|
56
|
-
|
|
57
|
-
if (afterSubscribe !== lastSnapshot) {
|
|
58
|
-
lastSnapshot = afterSubscribe;
|
|
59
|
-
setState(reconcile(afterSubscribe));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
onCleanup(unsubscribe);
|
|
63
|
-
|
|
64
|
-
return state;
|
|
65
|
-
}
|
package/src/directives/link.tsx
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { createActiveRouteSource } from "@real-router/sources";
|
|
2
|
-
import { createEffect, onCleanup } from "solid-js";
|
|
3
|
-
|
|
4
|
-
import { EMPTY_PARAMS, EMPTY_OPTIONS } from "../constants";
|
|
5
|
-
import { createSignalFromSource } from "../createSignalFromSource";
|
|
6
|
-
import { shouldNavigate, applyLinkA11y, buildHref } from "../dom-utils";
|
|
7
|
-
import { useRouter } from "../hooks/useRouter";
|
|
8
|
-
|
|
9
|
-
import type { Params } from "@real-router/core";
|
|
10
|
-
|
|
11
|
-
export interface LinkDirectiveOptions<P extends Params = Params> {
|
|
12
|
-
routeName: string;
|
|
13
|
-
routeParams?: P;
|
|
14
|
-
routeOptions?: Record<string, unknown>;
|
|
15
|
-
activeClassName?: string;
|
|
16
|
-
activeStrict?: boolean;
|
|
17
|
-
ignoreQueryParams?: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function link<P extends Params = Params>(
|
|
21
|
-
element: HTMLElement,
|
|
22
|
-
accessor: () => LinkDirectiveOptions<P>,
|
|
23
|
-
): void {
|
|
24
|
-
const router = useRouter();
|
|
25
|
-
const options = accessor();
|
|
26
|
-
|
|
27
|
-
// audit-2026-05-17 §8a cleanup — single instanceof probe, single EMPTY_PARAMS
|
|
28
|
-
// default. Previously evaluated three times for the <a>-only branches and
|
|
29
|
-
// twice for routeParams. The directive accessor is read once at init
|
|
30
|
-
// (documented "use:link Options Are Captured Once"), so both lookups are
|
|
31
|
-
// stable and worth hoisting.
|
|
32
|
-
const anchor = element instanceof HTMLAnchorElement ? element : null;
|
|
33
|
-
const resolvedRouteParams = (options.routeParams ?? EMPTY_PARAMS) as P;
|
|
34
|
-
const resolvedRouteOptions = options.routeOptions ?? EMPTY_OPTIONS;
|
|
35
|
-
|
|
36
|
-
// Set href on <a> elements
|
|
37
|
-
if (anchor) {
|
|
38
|
-
const href = buildHref(router, options.routeName, resolvedRouteParams);
|
|
39
|
-
|
|
40
|
-
if (href === undefined) {
|
|
41
|
-
anchor.removeAttribute("href");
|
|
42
|
-
} else {
|
|
43
|
-
anchor.href = href;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
applyLinkA11y(element);
|
|
48
|
-
|
|
49
|
-
// Active class tracking: only `isActive` is reactive (createEffect toggles
|
|
50
|
-
// the class on each emit). The `options` object itself is captured ONCE at
|
|
51
|
-
// init (see gotcha "use:link Options Are Captured Once") — changing
|
|
52
|
-
// `activeClassName` / `routeName` / `routeParams` later has no effect.
|
|
53
|
-
if (options.activeClassName) {
|
|
54
|
-
const activeClassName = options.activeClassName;
|
|
55
|
-
const activeSource = createActiveRouteSource(
|
|
56
|
-
router,
|
|
57
|
-
options.routeName,
|
|
58
|
-
resolvedRouteParams,
|
|
59
|
-
{
|
|
60
|
-
strict: options.activeStrict ?? false,
|
|
61
|
-
ignoreQueryParams: options.ignoreQueryParams ?? true,
|
|
62
|
-
},
|
|
63
|
-
);
|
|
64
|
-
const isActive = createSignalFromSource(activeSource);
|
|
65
|
-
|
|
66
|
-
createEffect(() => {
|
|
67
|
-
element.classList.toggle(activeClassName, isActive());
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Click handler
|
|
72
|
-
function handleClick(evt: MouseEvent) {
|
|
73
|
-
if (!shouldNavigate(evt)) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Mini-sprint E.2 (audit-5 §4.2 #2) — respect upstream
|
|
78
|
-
// preventDefault. `<Link>` checks `local.onClick(evt); if
|
|
79
|
-
// (evt.defaultPrevented) return;` because it owns the React-style
|
|
80
|
-
// onClick prop. The directive has no equivalent prop, but the
|
|
81
|
-
// consumer may register their OWN click listener on the same
|
|
82
|
-
// element (DOM event order is "addEventListener queue, in
|
|
83
|
-
// registration order"). If their listener called preventDefault
|
|
84
|
-
// to opt-out of navigation, the directive must honour that.
|
|
85
|
-
if (evt.defaultPrevented) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Symmetric with <Link> (#P0.6 audit): on an <a target="_blank"> the
|
|
90
|
-
// browser opens the URL in a new tab/window natively. Intercepting the
|
|
91
|
-
// click via preventDefault + router.navigate would suppress the new
|
|
92
|
-
// tab and silently keep the user on the current page.
|
|
93
|
-
if (anchor?.target === "_blank") {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (anchor) {
|
|
98
|
-
evt.preventDefault();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
router
|
|
102
|
-
.navigate(options.routeName, resolvedRouteParams, resolvedRouteOptions)
|
|
103
|
-
.catch(() => {});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
element.addEventListener("click", handleClick);
|
|
107
|
-
|
|
108
|
-
onCleanup(() => {
|
|
109
|
-
element.removeEventListener("click", handleClick);
|
|
110
|
-
});
|
|
111
|
-
}
|
package/src/directives.d.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { useRoute } from "./useRoute";
|
|
2
|
-
|
|
3
|
-
import type { Accessor } from "solid-js";
|
|
4
|
-
|
|
5
|
-
interface DeferredContext {
|
|
6
|
-
ssrDataDeferred?: Record<string, Promise<unknown>>;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const NEVER_PROMISE = new Promise<never>(() => {
|
|
10
|
-
// Intentionally never resolves — surfaces a forever-pending Suspense boundary
|
|
11
|
-
// when a key is requested that the loader never declared.
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Read a deferred promise published by `defer({ deferred: { <key>: Promise } })`
|
|
16
|
-
* inside an SSR data loader.
|
|
17
|
-
*
|
|
18
|
-
* Returns a Solid `Accessor<Promise<T>>` so the value tracks the active route
|
|
19
|
-
* — re-reading on navigation picks up the new state's deferred map. Wrap with
|
|
20
|
-
* `<Await name="key">{(value) => …}</Await>` (this package), which builds on
|
|
21
|
-
* `createResource` + `<Suspense>` for native Solid streaming.
|
|
22
|
-
*
|
|
23
|
-
* Returns a forever-pending promise when the key is missing — surfaces
|
|
24
|
-
* loader/consumer key drift as a visible Suspense fallback rather than a
|
|
25
|
-
* silent runtime error.
|
|
26
|
-
*/
|
|
27
|
-
export function useDeferred<T = unknown>(key: string): Accessor<Promise<T>> {
|
|
28
|
-
const routeAccessor = useRoute();
|
|
29
|
-
|
|
30
|
-
return () => {
|
|
31
|
-
const context = routeAccessor().route.context as DeferredContext;
|
|
32
|
-
const deferred = context.ssrDataDeferred;
|
|
33
|
-
|
|
34
|
-
return (deferred?.[key] ?? NEVER_PROMISE) as Promise<T>;
|
|
35
|
-
};
|
|
36
|
-
}
|
package/src/hooks/useRoute.tsx
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { useContext } from "solid-js";
|
|
2
|
-
|
|
3
|
-
import { RouteContext } from "../context";
|
|
4
|
-
|
|
5
|
-
import type { RouteState } from "../types";
|
|
6
|
-
import type { Params, State } from "@real-router/core";
|
|
7
|
-
import type { Accessor } from "solid-js";
|
|
8
|
-
|
|
9
|
-
export const useRoute = <P extends Params = Params>(): Accessor<
|
|
10
|
-
Omit<RouteState<P>, "route"> & { route: State<P> }
|
|
11
|
-
> => {
|
|
12
|
-
const routeSignal = useContext(RouteContext);
|
|
13
|
-
|
|
14
|
-
if (!routeSignal) {
|
|
15
|
-
throw new Error("useRoute must be used within a RouterProvider");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (!routeSignal().route) {
|
|
19
|
-
throw new Error(
|
|
20
|
-
"useRoute called with no active route. Did you forget to await router.start() before rendering, or is the router stopped/disposed?",
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return routeSignal as Accessor<
|
|
25
|
-
Omit<RouteState<P>, "route"> & { route: State<P> }
|
|
26
|
-
>;
|
|
27
|
-
};
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { createEffect } from "solid-js";
|
|
2
|
-
|
|
3
|
-
import { useRoute } from "./useRoute";
|
|
4
|
-
|
|
5
|
-
import type { State } from "@real-router/core";
|
|
6
|
-
|
|
7
|
-
export interface RouteEnterContext {
|
|
8
|
-
/** The route that was just activated. */
|
|
9
|
-
route: State;
|
|
10
|
-
/** The route that was active immediately before this navigation. */
|
|
11
|
-
previousRoute: State;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export type RouteEnterHandler = (context: RouteEnterContext) => void;
|
|
15
|
-
|
|
16
|
-
export interface UseRouteEnterOptions {
|
|
17
|
-
/**
|
|
18
|
-
* Skip the handler when `route.name === previousRoute.name`
|
|
19
|
-
* (sort/filter/query-only navigations on the same route). Default:
|
|
20
|
-
* `true`. Symmetric with `useRouteExit`'s same-name option.
|
|
21
|
-
*/
|
|
22
|
-
skipSameRoute?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Fire `handler` once when the component mounts as a result of a
|
|
27
|
-
* navigation. Mirror of `useRouteExit` for the entry side.
|
|
28
|
-
*
|
|
29
|
-
* What this hook covers that an ad-hoc `createEffect` + `useRoute()`
|
|
30
|
-
* doesn't:
|
|
31
|
-
*
|
|
32
|
-
* - **Skip-initial**: handler is skipped when there is no
|
|
33
|
-
* `transition.from` (i.e. first-load mount). Most consumers want to
|
|
34
|
-
* fire side effects only on real navigations, not on hydration.
|
|
35
|
-
* - **Same-route skip** (default): handler is skipped when
|
|
36
|
-
* `route.transition.from === route.name`. Sort/filter/query-only
|
|
37
|
-
* navigations re-trigger the effect (because the `route` reference
|
|
38
|
-
* changes), but they are not "entries" in the animation / analytics
|
|
39
|
-
* sense — the component instance has stayed mounted throughout.
|
|
40
|
-
* Opt out with `skipSameRoute: false`.
|
|
41
|
-
* - **Mount-time `route` / `previousRoute` snapshot**: the handler
|
|
42
|
-
* receives the values that were live at the moment of effect
|
|
43
|
-
* activation, not the latest ones (which may have moved on if the
|
|
44
|
-
* user navigated again before the effect drained).
|
|
45
|
-
*
|
|
46
|
-
* **Handler reactivity (Solid)**: Solid components run **once** at mount;
|
|
47
|
-
* `handler` is captured in closure when the hook is called. If you need
|
|
48
|
-
* different behavior across renders, derive it from a signal inside the
|
|
49
|
-
* handler body — do not rely on swapping the handler reference.
|
|
50
|
-
*
|
|
51
|
-
* @example Direction-aware entry animation
|
|
52
|
-
* ```tsx
|
|
53
|
-
* useRouteEnter(({ route }) => {
|
|
54
|
-
* const direction = route.context.browser?.direction;
|
|
55
|
-
* ref?.classList.add(
|
|
56
|
-
* direction === "back" ? "slide-from-left" : "slide-from-right",
|
|
57
|
-
* );
|
|
58
|
-
* });
|
|
59
|
-
* ```
|
|
60
|
-
*
|
|
61
|
-
* @example Analytics page-enter event (skip-initial built-in)
|
|
62
|
-
* ```tsx
|
|
63
|
-
* useRouteEnter(({ route, previousRoute }) => {
|
|
64
|
-
* analytics.track("page_enter", {
|
|
65
|
-
* route: route.name,
|
|
66
|
-
* from: previousRoute.name,
|
|
67
|
-
* });
|
|
68
|
-
* });
|
|
69
|
-
* ```
|
|
70
|
-
*
|
|
71
|
-
* @example Reading rich transition metadata via `route.transition`
|
|
72
|
-
* ```tsx
|
|
73
|
-
* useRouteEnter(({ route }) => {
|
|
74
|
-
* if (route.transition.redirected) {
|
|
75
|
-
* showToast(`Redirected from ${route.transition.from}`);
|
|
76
|
-
* }
|
|
77
|
-
* if (route.transition.segments.activated.includes("products")) {
|
|
78
|
-
* // products subtree just became active
|
|
79
|
-
* }
|
|
80
|
-
* });
|
|
81
|
-
* ```
|
|
82
|
-
*/
|
|
83
|
-
export function useRouteEnter(
|
|
84
|
-
handler: RouteEnterHandler,
|
|
85
|
-
options?: UseRouteEnterOptions,
|
|
86
|
-
): void {
|
|
87
|
-
const routeState = useRoute();
|
|
88
|
-
const skipSameRoute = options?.skipSameRoute ?? true;
|
|
89
|
-
let lastHandledRoute: State | null = null;
|
|
90
|
-
|
|
91
|
-
createEffect(() => {
|
|
92
|
-
const { route, previousRoute } = routeState();
|
|
93
|
-
|
|
94
|
-
// Early-exit guards, top-down:
|
|
95
|
-
//
|
|
96
|
-
// - **Skip-initial**: `state.transition.from` is undefined only
|
|
97
|
-
// for the very first state committed by `router.start()`.
|
|
98
|
-
// - **Skip-same-route**: query-only navigations have
|
|
99
|
-
// `transition.from === route.name`. Opt-out via
|
|
100
|
-
// `skipSameRoute: false`.
|
|
101
|
-
// - **Defensive dedupe + missing `previousRoute`**: same `route`
|
|
102
|
-
// ref between effect activations is unexpected on Solid (effects
|
|
103
|
-
// run once per dependency change); `!previousRoute` is unreachable
|
|
104
|
-
// once `transition.from` is set (the two are populated together by
|
|
105
|
-
// core). Both kept for parity with React; v8-ignored.
|
|
106
|
-
if (!route.transition.from) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (skipSameRoute && route.transition.from === route.name) {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
/* v8 ignore start */
|
|
113
|
-
if (lastHandledRoute === route || !previousRoute) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
/* v8 ignore stop */
|
|
117
|
-
|
|
118
|
-
lastHandledRoute = route;
|
|
119
|
-
handler({ route, previousRoute });
|
|
120
|
-
});
|
|
121
|
-
}
|