@real-router/vue 0.12.1 → 0.13.0
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/README.md +97 -13
- package/dist/cjs/createHttpStatusSink-XDu5aGhc.d.ts +32 -0
- package/dist/cjs/createHttpStatusSink-XDu5aGhc.d.ts.map +1 -0
- package/dist/cjs/index.d.ts +19 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/ssr.d.ts +181 -0
- package/dist/cjs/ssr.d.ts.map +1 -0
- package/dist/cjs/ssr.js +2 -0
- package/dist/cjs/ssr.js.map +1 -0
- package/dist/cjs/useRoute-BT3SkdOc.js +2 -0
- package/dist/cjs/useRoute-BT3SkdOc.js.map +1 -0
- package/dist/esm/createHttpStatusSink-DduXvbGr.d.mts +32 -0
- package/dist/esm/createHttpStatusSink-DduXvbGr.d.mts.map +1 -0
- package/dist/esm/index.d.mts +19 -2
- package/dist/esm/index.d.mts.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/ssr.d.mts +181 -0
- package/dist/esm/ssr.d.mts.map +1 -0
- package/dist/esm/ssr.mjs +2 -0
- package/dist/esm/ssr.mjs.map +1 -0
- package/dist/esm/useRoute-2ocUdDHc.mjs +2 -0
- package/dist/esm/useRoute-2ocUdDHc.mjs.map +1 -0
- package/package.json +20 -4
- package/src/RouterProvider.ts +45 -39
- package/src/components/Await.ts +47 -0
- package/src/components/ClientOnly.ts +16 -0
- package/src/components/HttpStatusCode.ts +74 -0
- package/src/components/HttpStatusProvider.ts +22 -0
- package/src/components/Link.ts +33 -13
- package/src/components/RouteView/RouteView.ts +30 -51
- package/src/components/RouteView/helpers.ts +33 -2
- package/src/components/ServerOnly.ts +16 -0
- package/src/components/Streamed.ts +31 -0
- package/src/composables/useDeferred.ts +37 -0
- package/src/composables/useIsActiveRoute.ts +33 -3
- package/src/composables/useRoute.ts +11 -5
- package/src/context.ts +4 -0
- package/src/directives/vLink.ts +18 -1
- package/src/index.ts +2 -1
- package/src/ssr.ts +39 -0
- package/src/types.ts +10 -0
- package/src/utils/createHttpStatusSink.ts +31 -0
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
} from "vue";
|
|
9
9
|
|
|
10
10
|
import { Match, NotFound, Self } from "./components";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
buildRenderList,
|
|
13
|
+
collectElements,
|
|
14
|
+
isKeepAliveEnabled,
|
|
15
|
+
} from "./helpers";
|
|
12
16
|
import { useRouteNode } from "../../composables/useRouteNode";
|
|
13
17
|
|
|
14
18
|
import type { Component, VNode } from "vue";
|
|
@@ -66,14 +70,23 @@ function wrapWithSuspense(content: VNode, fallback: unknown): VNode {
|
|
|
66
70
|
);
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
// Lazy-initialised — only allocated when a per-Match keepAlive path needs to
|
|
74
|
+
// keep the cache slot "occupied" with a no-render placeholder. Apps without
|
|
75
|
+
// keepAlive never pay the markRaw + defineComponent allocation at import.
|
|
76
|
+
let emptyKeepAlivePlaceholderInstance: Component | null = null;
|
|
77
|
+
|
|
78
|
+
function getEmptyKeepAlivePlaceholder(): Component {
|
|
79
|
+
emptyKeepAlivePlaceholderInstance ??= markRaw(
|
|
80
|
+
defineComponent({
|
|
81
|
+
name: "KeepAlive-placeholder",
|
|
82
|
+
render() {
|
|
83
|
+
return null;
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return emptyKeepAlivePlaceholderInstance;
|
|
89
|
+
}
|
|
77
90
|
|
|
78
91
|
function renderWithRootKA(
|
|
79
92
|
activeChild: VNode,
|
|
@@ -92,17 +105,6 @@ function renderWithRootKA(
|
|
|
92
105
|
return wrapWithSuspense(keepAliveContent, fallback);
|
|
93
106
|
}
|
|
94
107
|
|
|
95
|
-
// Vue compiles boolean-shorthand template attributes (`<Match keepAlive>`) to
|
|
96
|
-
// an empty string instead of `true`, and converts them to `true` only when the
|
|
97
|
-
// receiving component's prop is declared with `type: Boolean`. `Match` is a
|
|
98
|
-
// marker component (`render: null`) — its props are inspected on the VNode
|
|
99
|
-
// without ever going through Vue's prop-casting pipeline, so the raw `""` (or
|
|
100
|
-
// the hyphenated attribute name) reaches us here. Accept the same trio Vue's
|
|
101
|
-
// runtime does.
|
|
102
|
-
function isKeepAliveEnabled(value: unknown): boolean {
|
|
103
|
-
return value === true || value === "" || value === "keep-alive";
|
|
104
|
-
}
|
|
105
|
-
|
|
106
108
|
function renderWithPerMatchKA(
|
|
107
109
|
activeChild: VNode,
|
|
108
110
|
wrapperCache: Map<string, Component>,
|
|
@@ -137,7 +139,9 @@ function renderWithPerMatchKA(
|
|
|
137
139
|
/* v8 ignore stop */
|
|
138
140
|
|
|
139
141
|
return h(Fragment, [
|
|
140
|
-
h(KeepAlive, null, {
|
|
142
|
+
h(KeepAlive, null, {
|
|
143
|
+
default: () => h(getEmptyKeepAlivePlaceholder()),
|
|
144
|
+
}),
|
|
141
145
|
wrapWithSuspense(h(Fragment, content), fallback),
|
|
142
146
|
]);
|
|
143
147
|
}
|
|
@@ -158,33 +162,6 @@ const RouteViewComponent = defineComponent({
|
|
|
158
162
|
const routeContext = useRouteNode(props.nodeName);
|
|
159
163
|
const wrapperCache = new Map<string, Component>();
|
|
160
164
|
|
|
161
|
-
// Cache per-Match `keepAlive` detection by slot output identity. Slot
|
|
162
|
-
// contents change reference only when the parent re-renders with new
|
|
163
|
-
// children, so steady-state navigations skip the O(n) `.some(...)` scan.
|
|
164
|
-
let lastSlotOutput: unknown = null;
|
|
165
|
-
let lastHasPerMatchKA = false;
|
|
166
|
-
|
|
167
|
-
function detectPerMatchKA(elements: VNode[], slotOutput: unknown): boolean {
|
|
168
|
-
/* v8 ignore next 3 -- @preserve: Vue's compiled slot wrapper allocates a
|
|
169
|
-
new array per render call in JSDOM tests; identity-cache hits in
|
|
170
|
-
production where parent compiled templates share slot output, but
|
|
171
|
-
is unobservable through TestBed-style assertions. */
|
|
172
|
-
if (slotOutput === lastSlotOutput) {
|
|
173
|
-
return lastHasPerMatchKA;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
lastSlotOutput = slotOutput;
|
|
177
|
-
lastHasPerMatchKA = elements.some(
|
|
178
|
-
(element) =>
|
|
179
|
-
element.type === Match &&
|
|
180
|
-
isKeepAliveEnabled(
|
|
181
|
-
(element.props as { keepAlive?: unknown } | null)?.keepAlive,
|
|
182
|
-
),
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
return lastHasPerMatchKA;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
165
|
return (): VNode | null => {
|
|
189
166
|
const route = routeContext.route.value;
|
|
190
167
|
|
|
@@ -197,7 +174,11 @@ const RouteViewComponent = defineComponent({
|
|
|
197
174
|
|
|
198
175
|
collectElements(slotOutput, elements);
|
|
199
176
|
|
|
200
|
-
|
|
177
|
+
// `hasPerMatchKA` is a side-channel produced by the same pipeline pass
|
|
178
|
+
// that builds `rendered` — closes the audit §8.1 "double iteration"
|
|
179
|
+
// finding. The previous identity-cache on `slotOutput` is no longer
|
|
180
|
+
// needed: per-render cost is one O(n) walk instead of two.
|
|
181
|
+
const { rendered, fallback, hasPerMatchKA } = buildRenderList(
|
|
201
182
|
elements,
|
|
202
183
|
route.name,
|
|
203
184
|
props.nodeName,
|
|
@@ -223,8 +204,6 @@ const RouteViewComponent = defineComponent({
|
|
|
223
204
|
}
|
|
224
205
|
/* v8 ignore stop */
|
|
225
206
|
|
|
226
|
-
const hasPerMatchKA = detectPerMatchKA(elements, slotOutput);
|
|
227
|
-
|
|
228
207
|
if (hasPerMatchKA) {
|
|
229
208
|
return renderWithPerMatchKA(activeChild, wrapperCache, fallback);
|
|
230
209
|
}
|
|
@@ -14,7 +14,7 @@ interface FallbackSlots {
|
|
|
14
14
|
notFoundChildren: unknown;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function isSegmentMatch(
|
|
17
|
+
export function isSegmentMatch(
|
|
18
18
|
routeName: string,
|
|
19
19
|
fullSegmentName: string,
|
|
20
20
|
exact: boolean,
|
|
@@ -26,6 +26,17 @@ function isSegmentMatch(
|
|
|
26
26
|
return startsWithSegment(routeName, fullSegmentName);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Vue compiles boolean-shorthand template attributes (`<Match keepAlive>`) to
|
|
30
|
+
// an empty string instead of `true`, and converts them to `true` only when the
|
|
31
|
+
// receiving component's prop is declared with `type: Boolean`. `Match` is a
|
|
32
|
+
// marker component (`render: null`) — its props are inspected on the VNode
|
|
33
|
+
// without ever going through Vue's prop-casting pipeline, so the raw `""` (or
|
|
34
|
+
// the hyphenated attribute name) reaches us here. Accept the same trio Vue's
|
|
35
|
+
// runtime does.
|
|
36
|
+
export function isKeepAliveEnabled(value: unknown): boolean {
|
|
37
|
+
return value === true || value === "" || value === "keep-alive";
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
function normalizeChildren(children: unknown): VNode[] {
|
|
30
41
|
if (Array.isArray(children)) {
|
|
31
42
|
const result: VNode[] = [];
|
|
@@ -137,6 +148,14 @@ export function buildRenderList(
|
|
|
137
148
|
rendered: VNode[];
|
|
138
149
|
activeMatchFound: boolean;
|
|
139
150
|
fallback?: FallbackType;
|
|
151
|
+
/**
|
|
152
|
+
* True iff any `<Match>` child in the input has its `keepAlive` prop set
|
|
153
|
+
* to one of Vue's accepted boolean-shorthand forms. Surfaced as a
|
|
154
|
+
* side-channel from the single pipeline pass so the caller doesn't have
|
|
155
|
+
* to re-iterate `elements` after `buildRenderList` returns — closes a MED
|
|
156
|
+
* code-quality finding (audit §8.1).
|
|
157
|
+
*/
|
|
158
|
+
hasPerMatchKA: boolean;
|
|
140
159
|
} {
|
|
141
160
|
const slots: FallbackSlots = {
|
|
142
161
|
selfVNode: null,
|
|
@@ -145,9 +164,21 @@ export function buildRenderList(
|
|
|
145
164
|
};
|
|
146
165
|
let activeMatchFound = false;
|
|
147
166
|
let fallback: FallbackType = undefined;
|
|
167
|
+
let hasPerMatchKA = false;
|
|
148
168
|
const rendered: VNode[] = [];
|
|
149
169
|
|
|
150
170
|
for (const child of elements) {
|
|
171
|
+
// Match-only side-channel: scan for the keepAlive shorthand in the same
|
|
172
|
+
// pass that already inspects every child. Short-circuits once a positive
|
|
173
|
+
// is found to avoid redundant prop reads in big slot trees.
|
|
174
|
+
if (!hasPerMatchKA && child.type === Match) {
|
|
175
|
+
const matchProps = child.props as { keepAlive?: unknown } | null;
|
|
176
|
+
|
|
177
|
+
if (isKeepAliveEnabled(matchProps?.keepAlive)) {
|
|
178
|
+
hasPerMatchKA = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
151
182
|
if (recordFallback(child, slots)) {
|
|
152
183
|
continue;
|
|
153
184
|
}
|
|
@@ -169,5 +200,5 @@ export function buildRenderList(
|
|
|
169
200
|
fallback = appendFallback(rendered, routeName, nodeName, slots, elements);
|
|
170
201
|
}
|
|
171
202
|
|
|
172
|
-
return { rendered, activeMatchFound, fallback };
|
|
203
|
+
return { rendered, activeMatchFound, fallback, hasPerMatchKA };
|
|
173
204
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineComponent, onMounted, ref } from "vue";
|
|
2
|
+
|
|
3
|
+
export const ServerOnly = defineComponent({
|
|
4
|
+
name: "ServerOnly",
|
|
5
|
+
setup(_, { slots }) {
|
|
6
|
+
const mounted = ref(false);
|
|
7
|
+
|
|
8
|
+
onMounted(() => {
|
|
9
|
+
mounted.value = true;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return () => (mounted.value ? slots.fallback?.() : slots.default?.());
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type ServerOnlyProps = InstanceType<typeof ServerOnly>["$props"];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineComponent, h, Suspense } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cross-adapter alias for Vue's native `<Suspense>`. Symmetric naming with
|
|
5
|
+
* the React/Preact/Solid/Svelte/Angular `<Streamed>` components.
|
|
6
|
+
*
|
|
7
|
+
* Slots:
|
|
8
|
+
* - `default` — content (may contain `<Await>` or `async setup()` children).
|
|
9
|
+
* - `fallback` — shown while any descendant suspends.
|
|
10
|
+
*
|
|
11
|
+
* Vue's `<Suspense>` is **blocking** under SSR (no out-of-order placeholder
|
|
12
|
+
* resolution) — render of HTML after `<Streamed>` waits for every
|
|
13
|
+
* `async setup()` inside. This matches Vue 3's stable streaming behaviour
|
|
14
|
+
* (vs React 19 / Solid which support OOO resolution).
|
|
15
|
+
*/
|
|
16
|
+
export const Streamed = defineComponent({
|
|
17
|
+
name: "Streamed",
|
|
18
|
+
setup(_, { slots }) {
|
|
19
|
+
return () =>
|
|
20
|
+
h(
|
|
21
|
+
Suspense,
|
|
22
|
+
{},
|
|
23
|
+
{
|
|
24
|
+
default: () => slots.default?.(),
|
|
25
|
+
fallback: () => slots.fallback?.(),
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type StreamedProps = InstanceType<typeof Streamed>["$props"];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useRoute } from "./useRoute";
|
|
2
|
+
|
|
3
|
+
interface DeferredContext {
|
|
4
|
+
ssrDataDeferred?: Record<string, Promise<unknown>>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const NEVER_PROMISE = new Promise<never>(() => {
|
|
8
|
+
// Intentionally never resolves — surfaces a forever-pending Suspense boundary
|
|
9
|
+
// when a key is requested that the loader never declared.
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read a deferred promise published by `defer({ deferred: { <key>: Promise } })`
|
|
14
|
+
* inside an SSR data loader. Returns the Promise for use inside `async setup()`
|
|
15
|
+
* (Vue's native Suspense pattern) or paired with `<Await name="key">`.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* // Vue async setup pattern
|
|
19
|
+
* export default defineComponent({
|
|
20
|
+
* async setup() {
|
|
21
|
+
* const reviews = await useDeferred<Review[]>("reviews");
|
|
22
|
+
* return () => h("div", reviews.map(...));
|
|
23
|
+
* },
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Returns a forever-pending promise when the key is missing — surfaces
|
|
28
|
+
* loader/consumer key drift as a visible Suspense fallback rather than a
|
|
29
|
+
* silent runtime error.
|
|
30
|
+
*/
|
|
31
|
+
export function useDeferred<T = unknown>(key: string): Promise<T> {
|
|
32
|
+
const { route } = useRoute();
|
|
33
|
+
const context = route.value.context as DeferredContext;
|
|
34
|
+
const deferred = context.ssrDataDeferred;
|
|
35
|
+
|
|
36
|
+
return (deferred?.[key] ?? NEVER_PROMISE) as Promise<T>;
|
|
37
|
+
}
|
|
@@ -6,14 +6,44 @@ import { useRouter } from "./useRouter";
|
|
|
6
6
|
import type { Params } from "@real-router/core";
|
|
7
7
|
import type { ShallowRef } from "vue";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Options object for `useIsActiveRoute`. Replaces the previous trailing
|
|
11
|
+
* positional booleans (`strict`, `ignoreQueryParams`) — positional flags at
|
|
12
|
+
* call sites read as magic numbers and the order was easy to swap silently.
|
|
13
|
+
*
|
|
14
|
+
* The composable is `@internal` (consumed by `<Link>` and tests only), so
|
|
15
|
+
* the signature changes without a deprecation cycle.
|
|
16
|
+
*/
|
|
17
|
+
export interface UseIsActiveRouteOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Match the route name exactly (no descendant match). Default: `false`.
|
|
20
|
+
*/
|
|
21
|
+
strict?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Ignore query params when comparing the active route. Default: `true`.
|
|
24
|
+
*/
|
|
25
|
+
ignoreQueryParams?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Hash-aware active state (#532) — when provided, the route is active only
|
|
28
|
+
* if `state.context.url.hash` equals this value. Default: `undefined`
|
|
29
|
+
* (hash is ignored).
|
|
30
|
+
*/
|
|
31
|
+
hash?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @internal Consumed by `<Link>` via `createActiveRouteSource`. Not exported
|
|
36
|
+
* from `@real-router/vue`.
|
|
37
|
+
*/
|
|
9
38
|
export function useIsActiveRoute(
|
|
10
39
|
routeName: string,
|
|
11
40
|
params?: Params,
|
|
12
|
-
|
|
13
|
-
ignoreQueryParams = true,
|
|
14
|
-
hash?: string,
|
|
41
|
+
options?: UseIsActiveRouteOptions,
|
|
15
42
|
): ShallowRef<boolean> {
|
|
16
43
|
const router = useRouter();
|
|
44
|
+
const strict = options?.strict ?? false;
|
|
45
|
+
const ignoreQueryParams = options?.ignoreQueryParams ?? true;
|
|
46
|
+
const hash = options?.hash;
|
|
17
47
|
|
|
18
48
|
// The `hash` argument (#532) participates in the cache key when defined.
|
|
19
49
|
// exactOptionalPropertyTypes forbids `{ hash: undefined }` literally — we
|
|
@@ -6,10 +6,18 @@ import type { RouteContext } from "../types";
|
|
|
6
6
|
import type { Params, State } from "@real-router/core";
|
|
7
7
|
import type { Ref } from "vue";
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Return shape for `useRoute()` — `RouteContext<P>` with `route` narrowed
|
|
11
|
+
* to the non-nullable variant. The composable throws when `route.value`
|
|
12
|
+
* would be `undefined`, so consumers can read `.value.params.x` without a
|
|
13
|
+
* nullable guard. Extracted from inline duplication at two call sites.
|
|
14
|
+
*/
|
|
15
|
+
export type UseRouteReturn<P extends Params = Params> = Omit<
|
|
10
16
|
RouteContext<P>,
|
|
11
17
|
"route"
|
|
12
|
-
> & { route: Readonly<Ref<State<P>>> }
|
|
18
|
+
> & { route: Readonly<Ref<State<P>>> };
|
|
19
|
+
|
|
20
|
+
export const useRoute = <P extends Params = Params>(): UseRouteReturn<P> => {
|
|
13
21
|
const routeContext = inject(RouteKey);
|
|
14
22
|
|
|
15
23
|
if (!routeContext) {
|
|
@@ -22,7 +30,5 @@ export const useRoute = <P extends Params = Params>(): Omit<
|
|
|
22
30
|
);
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
return routeContext as
|
|
26
|
-
route: Readonly<Ref<State<P>>>;
|
|
27
|
-
};
|
|
33
|
+
return routeContext as UseRouteReturn<P>;
|
|
28
34
|
};
|
package/src/context.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RouteContext as RouteContextType } from "./types";
|
|
2
|
+
import type { HttpStatusSink } from "./utils/createHttpStatusSink";
|
|
2
3
|
import type { Router, Navigator } from "@real-router/core";
|
|
3
4
|
import type { InjectionKey } from "vue";
|
|
4
5
|
|
|
@@ -7,3 +8,6 @@ export const RouterKey: InjectionKey<Router> = Symbol("RouterKey");
|
|
|
7
8
|
export const NavigatorKey: InjectionKey<Navigator> = Symbol("NavigatorKey");
|
|
8
9
|
|
|
9
10
|
export const RouteKey: InjectionKey<RouteContextType> = Symbol("RouteKey");
|
|
11
|
+
|
|
12
|
+
export const HTTP_STATUS_KEY: InjectionKey<HttpStatusSink> =
|
|
13
|
+
Symbol("HttpStatusSink");
|
package/src/directives/vLink.ts
CHANGED
|
@@ -42,7 +42,11 @@ export function pushDirectiveRouter(router: Router): () => void {
|
|
|
42
42
|
/**
|
|
43
43
|
* Backwards-compatible alias. Replaces the active router unconditionally and
|
|
44
44
|
* does NOT participate in the stack — use {@link pushDirectiveRouter} from
|
|
45
|
-
* provider code instead.
|
|
45
|
+
* provider code instead. Not exported from the package entry; retained for
|
|
46
|
+
* unit tests and rare standalone-directive setups (where v-link is mounted
|
|
47
|
+
* outside any RouterProvider).
|
|
48
|
+
*
|
|
49
|
+
* @internal
|
|
46
50
|
*/
|
|
47
51
|
export function setDirectiveRouter(router: Router | null): void {
|
|
48
52
|
if (router === null) {
|
|
@@ -174,6 +178,19 @@ export const vLink: Directive<HTMLElement, LinkDirectiveValue> = {
|
|
|
174
178
|
},
|
|
175
179
|
|
|
176
180
|
updated(element, binding) {
|
|
181
|
+
// Hot-path guard: Vue invokes `updated` on every parent re-render even
|
|
182
|
+
// when the directive's binding value reference has not changed. Without
|
|
183
|
+
// this short-circuit, every parent rerender (which is the common case on
|
|
184
|
+
// Link-heavy pages — any unrelated state change triggers the parent's
|
|
185
|
+
// render fn) would detach + reattach the click/keydown listeners.
|
|
186
|
+
// Comparing references is enough: when consumers pass a stable
|
|
187
|
+
// `LinkDirectiveValue` object (the recommended pattern, since Vue's
|
|
188
|
+
// template compiler hoists `v-link="{ name: 'home' }"` to a stable
|
|
189
|
+
// literal), this guard collapses the work to zero.
|
|
190
|
+
if (binding.value === binding.oldValue) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
177
194
|
const router = getDirectiveRouter();
|
|
178
195
|
|
|
179
196
|
detachHandlers(element);
|
package/src/index.ts
CHANGED
|
@@ -34,7 +34,7 @@ export { RouterProvider } from "./RouterProvider";
|
|
|
34
34
|
export { RouterKey, NavigatorKey, RouteKey } from "./context";
|
|
35
35
|
|
|
36
36
|
// Types
|
|
37
|
-
export type { LinkProps } from "./types";
|
|
37
|
+
export type { RouteContext, LinkProps } from "./types";
|
|
38
38
|
|
|
39
39
|
export type { RouterErrorBoundaryProps } from "./components/RouterErrorBoundary";
|
|
40
40
|
|
|
@@ -43,6 +43,7 @@ export type { LinkDirectiveValue } from "./directives/vLink";
|
|
|
43
43
|
export type {
|
|
44
44
|
RouteViewProps,
|
|
45
45
|
RouteViewMatchProps,
|
|
46
|
+
RouteViewSelfProps,
|
|
46
47
|
RouteViewNotFoundProps,
|
|
47
48
|
} from "./components/RouteView";
|
|
48
49
|
|
package/src/ssr.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SSR-feature entry — Vue 3.3+
|
|
2
|
+
//
|
|
3
|
+
// Server-side and SSR-aware components/composables. Mirror of `@real-router/react/ssr`
|
|
4
|
+
// — same exports, Vue-native idioms (defineComponent + h(), async setup()
|
|
5
|
+
// for Await, native Suspense for Streamed).
|
|
6
|
+
|
|
7
|
+
// Components
|
|
8
|
+
export { ClientOnly } from "./components/ClientOnly";
|
|
9
|
+
|
|
10
|
+
export { ServerOnly } from "./components/ServerOnly";
|
|
11
|
+
|
|
12
|
+
export { Await } from "./components/Await";
|
|
13
|
+
|
|
14
|
+
export { Streamed } from "./components/Streamed";
|
|
15
|
+
|
|
16
|
+
export { HttpStatusCode } from "./components/HttpStatusCode";
|
|
17
|
+
|
|
18
|
+
export { HttpStatusProvider } from "./components/HttpStatusProvider";
|
|
19
|
+
|
|
20
|
+
// Composables
|
|
21
|
+
export { useDeferred } from "./composables/useDeferred";
|
|
22
|
+
|
|
23
|
+
// Utilities
|
|
24
|
+
export { createHttpStatusSink } from "./utils/createHttpStatusSink";
|
|
25
|
+
|
|
26
|
+
// Types
|
|
27
|
+
export type { ClientOnlyProps } from "./components/ClientOnly";
|
|
28
|
+
|
|
29
|
+
export type { ServerOnlyProps } from "./components/ServerOnly";
|
|
30
|
+
|
|
31
|
+
export type { AwaitProps } from "./components/Await";
|
|
32
|
+
|
|
33
|
+
export type { StreamedProps } from "./components/Streamed";
|
|
34
|
+
|
|
35
|
+
export type { HttpStatusCodeProps } from "./components/HttpStatusCode";
|
|
36
|
+
|
|
37
|
+
export type { HttpStatusProviderProps } from "./components/HttpStatusProvider";
|
|
38
|
+
|
|
39
|
+
export type { HttpStatusSink } from "./utils/createHttpStatusSink";
|
package/src/types.ts
CHANGED
|
@@ -27,4 +27,14 @@ export interface LinkProps<P extends Params = Params> {
|
|
|
27
27
|
activeStrict?: boolean;
|
|
28
28
|
ignoreQueryParams?: boolean;
|
|
29
29
|
target?: string;
|
|
30
|
+
/**
|
|
31
|
+
* URL fragment (#532). Decoded, no leading `#`. Tri-state:
|
|
32
|
+
* - `undefined` (default) — preserves current `state.context.url.hash` on click.
|
|
33
|
+
* - `""` — clears the hash.
|
|
34
|
+
* - `"value"` — sets the hash; click routes through `navigateWithHash`,
|
|
35
|
+
* which auto-adds `force: true, hashChange: true` for same-route hash
|
|
36
|
+
* transitions (bypasses core's SAME_STATES check).
|
|
37
|
+
* Active state is hash-aware when `hash` is set.
|
|
38
|
+
*/
|
|
39
|
+
hash?: string;
|
|
30
40
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render-scoped HTTP status sink. Created per request on the server, passed to
|
|
3
|
+
* `<HttpStatusProvider :sink="...">`, and read after `renderToString` /
|
|
4
|
+
* `renderToWebStream` to apply the value to the HTTP response.
|
|
5
|
+
*
|
|
6
|
+
* Last write wins: if the rendered tree mounts more than one
|
|
7
|
+
* `<HttpStatusCode />`, the value reflects the last component that ran during
|
|
8
|
+
* the render pass.
|
|
9
|
+
*
|
|
10
|
+
* No-op on the client — `<HttpStatusCode />` reads the optional injected sink
|
|
11
|
+
* and skips the write when no provider is mounted, so the same component tree
|
|
12
|
+
* can be hydrated without changing behaviour.
|
|
13
|
+
*
|
|
14
|
+
* Constraints:
|
|
15
|
+
* - **Per-request only.** Don't share a sink across requests; the rendered
|
|
16
|
+
* tree mutates `code` in place. Module-level singletons leak status
|
|
17
|
+
* between concurrent requests.
|
|
18
|
+
* - **Don't `Object.freeze` the sink.** The component writes to `.code`;
|
|
19
|
+
* freezing makes the assignment throw under ESM strict mode.
|
|
20
|
+
* - **Hydration symmetry:** mount `<HttpStatusProvider>` on both server and
|
|
21
|
+
* client (with a throwaway client sink). Vue emits `<!--[-->` / `<!--]-->`
|
|
22
|
+
* fragment markers around the provider's slot; an extra provider on one
|
|
23
|
+
* side trips Vue with "Hydration completed but contains mismatches".
|
|
24
|
+
*/
|
|
25
|
+
export interface HttpStatusSink {
|
|
26
|
+
code: number | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createHttpStatusSink(): HttpStatusSink {
|
|
30
|
+
return { code: undefined };
|
|
31
|
+
}
|