@void/react 1.5.0 → 1.6.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/dist/index.d.mts +13 -54
- package/dist/index.mjs +51 -65
- package/dist/plugin.d.mts +2 -6
- package/dist/plugin.mjs +41 -99
- package/dist/runtime/pages-client.d.mts +2 -1
- package/dist/runtime/pages-client.mjs +6 -3
- package/dist/runtime/pages-server.d.mts +2 -1
- package/dist/runtime/pages-server.mjs +2 -2
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AnchorHTMLAttributes, ButtonHTMLAttributes, Context, FormHTMLAttributes, JSX, ReactElement, ReactNode } from "react";
|
|
2
|
-
import { ActionClient, FormMethod, FormModel,
|
|
2
|
+
import { ActionClient, BaseLinkProps, FormMethod, FormModel, FormState, IslandFormState, NavigationState, VoidActionError, VoidRouter } from "void/pages-client";
|
|
3
3
|
import { CloudContextVariables, InferProps } from "void";
|
|
4
4
|
import { ActionFormOptions, ActionUrl, ResolveActionBody } from "void/routes";
|
|
5
5
|
type FormProps = Omit<FormHTMLAttributes<HTMLFormElement>, "action" | "onSubmit" | "method" | "onError"> & {
|
|
@@ -8,70 +8,29 @@ type FormProps = Omit<FormHTMLAttributes<HTMLFormElement>, "action" | "onSubmit"
|
|
|
8
8
|
renderError?: (error: VoidActionError) => ReactNode;
|
|
9
9
|
};
|
|
10
10
|
export declare function Form({ form, method, renderError, children, ...props }: FormProps): ReactElement;
|
|
11
|
-
type
|
|
12
|
-
|
|
13
|
-
type CommonProps = {
|
|
14
|
-
href: string;
|
|
15
|
-
method?: LinkMethod | Lowercase<LinkMethod>;
|
|
16
|
-
data?: Record<string, unknown>;
|
|
17
|
-
preserveScroll?: boolean;
|
|
18
|
-
preserveState?: boolean;
|
|
19
|
-
replace?: boolean;
|
|
20
|
-
reloadDocument?: boolean;
|
|
21
|
-
viewTransition?: boolean;
|
|
22
|
-
prefetch?: boolean | PrefetchStrategy | ReadonlyArray<PrefetchStrategy>;
|
|
23
|
-
cacheFor?: number | string | [string, string];
|
|
24
|
-
onNavigate?: (event: {
|
|
25
|
-
preventDefault(): void;
|
|
26
|
-
}) => void;
|
|
27
|
-
};
|
|
28
|
-
type LinkProps = CommonProps & Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof CommonProps> & Omit<ButtonHTMLAttributes<HTMLButtonElement>, keyof CommonProps>;
|
|
29
|
-
export declare function Link({ href, method, data, preserveScroll, preserveState, replace, reloadDocument, viewTransition, prefetch: prefetchProp, cacheFor, onNavigate, children, style, onClick: onClickProp, onMouseEnter: onMouseEnterProp, onMouseLeave: onMouseLeaveProp, onFocus: onFocusProp, onMouseDown: onMouseDownProp, onTouchStart: onTouchStartProp, ...rest }: LinkProps): JSX.Element;
|
|
11
|
+
type LinkProps = BaseLinkProps & Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof BaseLinkProps> & Omit<ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseLinkProps>;
|
|
12
|
+
export declare function Link({ href, method, data, preserveScroll, preserveState, replace, reloadDocument, viewTransition, prefetch, cacheFor, activeMatch, onNavigate, children, style, onClick: onClickProp, onMouseEnter: onMouseEnterProp, onMouseLeave: onMouseLeaveProp, onFocus: onFocusProp, onMouseDown: onMouseDownProp, onTouchStart: onTouchStartProp, ...rest }: LinkProps): JSX.Element;
|
|
30
13
|
export declare function useRouter(): VoidRouter;
|
|
31
14
|
export declare function useParams<T extends Record<string, string> = Record<string, string>>(): T;
|
|
32
15
|
export declare function useShared<T = CloudContextVariables["shared"]>(): T;
|
|
16
|
+
type FormReturn<TData> = FormState<TData, "setters"> & Readonly<{
|
|
17
|
+
setData: <K extends keyof TData & string>(field: K, value: TData[K]) => void;
|
|
18
|
+
post(formData: FormData): void;
|
|
19
|
+
put(formData: FormData): void;
|
|
20
|
+
patch(formData: FormData): void;
|
|
21
|
+
delete(formData: FormData): void;
|
|
22
|
+
}>;
|
|
33
23
|
type FormOptions = {
|
|
34
24
|
params?: Record<string, string>;
|
|
35
25
|
resetOnSuccess?: boolean;
|
|
26
|
+
preserveScroll?: boolean;
|
|
36
27
|
onSuccess?: () => void | Promise<void>;
|
|
37
28
|
onError?: (error: Record<string, string> | VoidActionError) => void | Promise<void>;
|
|
38
29
|
};
|
|
39
|
-
export declare function useForm<U extends ActionUrl & string>(url: U, defaults: ResolveActionBody<U>, options?: ActionFormOptions<U> & Pick<FormOptions, "onSuccess" | "onError">):
|
|
40
|
-
data: ResolveActionBody<U>;
|
|
41
|
-
setData: <K extends keyof ResolveActionBody<U> & string>(field: K, value: ResolveActionBody<U>[K]) => void;
|
|
42
|
-
errors: Partial<Record<keyof ResolveActionBody<U> & string, string>>;
|
|
43
|
-
error: VoidActionError | null;
|
|
44
|
-
pending: boolean;
|
|
45
|
-
hasChanges: boolean;
|
|
46
|
-
wasSuccessful: boolean;
|
|
47
|
-
recentlySuccessful: boolean;
|
|
48
|
-
post(formData: FormData): void;
|
|
49
|
-
put(formData: FormData): void;
|
|
50
|
-
patch(formData: FormData): void;
|
|
51
|
-
delete(formData: FormData): void;
|
|
52
|
-
reset(options: FormResetOptions, ...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
|
|
53
|
-
reset(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
|
|
54
|
-
setError(field: keyof ResolveActionBody<U> & string, message: string): void;
|
|
55
|
-
clearErrors(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
|
|
56
|
-
clearError(): void;
|
|
57
|
-
};
|
|
30
|
+
export declare function useForm<U extends ActionUrl & string>(url: U, defaults: ResolveActionBody<U>, options?: ActionFormOptions<U> & Pick<FormOptions, "onSuccess" | "onError" | "preserveScroll">): FormReturn<ResolveActionBody<U>>;
|
|
58
31
|
export declare function useNavigation(): NavigationState;
|
|
59
|
-
type IslandFormReturn<T extends Record<string, unknown>> = Readonly<{
|
|
60
|
-
data: T;
|
|
32
|
+
type IslandFormReturn<T extends Record<string, unknown>> = IslandFormState<T, "setters"> & Readonly<{
|
|
61
33
|
setData: <K extends keyof T & string>(field: K, value: T[K]) => void;
|
|
62
|
-
errors: Record<string, string>;
|
|
63
|
-
error: VoidActionError | null;
|
|
64
|
-
pending: boolean;
|
|
65
|
-
hasChanges: boolean;
|
|
66
|
-
wasSuccessful: boolean;
|
|
67
|
-
recentlySuccessful: boolean;
|
|
68
|
-
reset: (...fields: ReadonlyArray<keyof T & string>) => void;
|
|
69
|
-
clearErrors: (...fields: ReadonlyArray<string>) => void;
|
|
70
|
-
clearError: () => void;
|
|
71
|
-
post: (url: string) => Promise<void>;
|
|
72
|
-
put: (url: string) => Promise<void>;
|
|
73
|
-
patch: (url: string) => Promise<void>;
|
|
74
|
-
delete: (url: string) => Promise<void>;
|
|
75
34
|
}>;
|
|
76
35
|
/**
|
|
77
36
|
* Form hook for island pages. Uses fetch + page reload instead of the
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as RouterContext, i as ParamsContext, n as FrontmatterContext, o as SharedContext, r as NavigationContext, t as ErrorsContext } from "./context-BQpawTZc.mjs";
|
|
2
2
|
import { n as setActionRouter, t as action } from "./action-CyCVdW12.mjs";
|
|
3
3
|
import { useActionState, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import { cloneFormValue, formErrorMessage, formRetryMessage, formSubmissionBlocked, getValidationErrors, interpolateRouteParams, isAbortError, isEqualFormValue,
|
|
4
|
+
import { cloneFormValue, formErrorMessage, formRetryMessage, formSubmissionBlocked, getValidationErrors, handleLinkClick, interpolateRouteParams, isAbortError, isEqualFormValue, linkActiveAttributes, linkPendingAttributes, linkPrefetchStrategies, observeLinkVisible, parseFormResetArgs, prefetchLink, resolveLink, scheduleFormRetry, ssrProxy, submitAction, submitIslandAction } from "void/pages-client";
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
// src/runtime/form.tsx
|
|
7
7
|
function Form({ form, method = "post", renderError, children, ...props }) {
|
|
@@ -25,80 +25,72 @@ function Form({ form, method = "post", renderError, children, ...props }) {
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
// src/runtime/link.tsx
|
|
28
|
-
function Link({ href, method = "GET", data, preserveScroll = false, preserveState = false, replace = false, reloadDocument = false, viewTransition, prefetch
|
|
28
|
+
function Link({ href, method = "GET", data, preserveScroll = false, preserveState = false, replace = false, reloadDocument = false, viewTransition, prefetch, cacheFor, activeMatch, onNavigate, children, style, onClick: onClickProp, onMouseEnter: onMouseEnterProp, onMouseLeave: onMouseLeaveProp, onFocus: onFocusProp, onMouseDown: onMouseDownProp, onTouchStart: onTouchStartProp, ...rest }) {
|
|
29
29
|
const router = useContext(RouterContext);
|
|
30
|
+
const navigation = useContext(NavigationContext);
|
|
30
31
|
const elementRef = useRef(null);
|
|
31
32
|
const hoverTimerRef = useRef(null);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const link = useMemo(() => resolveLink({
|
|
34
|
+
href,
|
|
35
|
+
method,
|
|
36
|
+
data,
|
|
37
|
+
prefetch,
|
|
38
|
+
reloadDocument
|
|
39
|
+
}), [
|
|
35
40
|
data,
|
|
36
41
|
href,
|
|
37
|
-
|
|
42
|
+
method,
|
|
43
|
+
prefetch,
|
|
44
|
+
reloadDocument
|
|
38
45
|
]);
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (cacheFor !== void 0) options.cacheFor = cacheFor;
|
|
54
|
-
router.prefetch(hrefWithData, options);
|
|
55
|
-
}, [
|
|
56
|
-
cacheFor,
|
|
57
|
-
hrefWithData,
|
|
46
|
+
const { isGet } = link;
|
|
47
|
+
const defaultStrategy = router?._prefetchDefaultStrategy;
|
|
48
|
+
const strategies = useMemo(() => linkPrefetchStrategies({
|
|
49
|
+
prefetch,
|
|
50
|
+
reloadDocument
|
|
51
|
+
}, { isGet }, defaultStrategy), [
|
|
52
|
+
defaultStrategy,
|
|
53
|
+
isGet,
|
|
54
|
+
prefetch,
|
|
55
|
+
reloadDocument
|
|
56
|
+
]);
|
|
57
|
+
const activeAttributes = useMemo(() => linkActiveAttributes(router, link, activeMatch), [
|
|
58
|
+
activeMatch,
|
|
59
|
+
link,
|
|
58
60
|
router
|
|
59
61
|
]);
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
const pendingAttributes = useMemo(() => linkPendingAttributes(router, navigation, link), [
|
|
63
|
+
link,
|
|
64
|
+
navigation,
|
|
65
|
+
router
|
|
66
|
+
]);
|
|
67
|
+
const hoverDelay = router?._hoverDelay ?? 75;
|
|
68
|
+
const doPrefetch = useCallback(() => prefetchLink(router, link.href, cacheFor), [
|
|
69
|
+
cacheFor,
|
|
70
|
+
link.href,
|
|
68
71
|
router
|
|
69
72
|
]);
|
|
70
73
|
const onClick = useCallback((event) => {
|
|
71
74
|
onClickProp?.(event);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
onNavigate?.({ preventDefault() {
|
|
75
|
-
navigatePrevented = true;
|
|
76
|
-
} });
|
|
77
|
-
if (navigatePrevented) {
|
|
78
|
-
event.preventDefault();
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
event.preventDefault();
|
|
82
|
-
router?.visit(hrefWithData, {
|
|
83
|
-
method: normalizedMethod,
|
|
84
|
-
data: isGet ? void 0 : data,
|
|
75
|
+
handleLinkClick(event, router, link, {
|
|
76
|
+
data,
|
|
85
77
|
preserveScroll,
|
|
86
78
|
preserveState,
|
|
87
79
|
replace,
|
|
88
|
-
|
|
80
|
+
reloadDocument,
|
|
81
|
+
viewTransition,
|
|
82
|
+
onNavigate
|
|
89
83
|
});
|
|
90
84
|
}, [
|
|
91
85
|
data,
|
|
92
|
-
|
|
93
|
-
isGet,
|
|
94
|
-
normalizedMethod,
|
|
86
|
+
link,
|
|
95
87
|
onClickProp,
|
|
96
88
|
onNavigate,
|
|
97
89
|
preserveScroll,
|
|
98
90
|
preserveState,
|
|
91
|
+
reloadDocument,
|
|
99
92
|
replace,
|
|
100
93
|
router,
|
|
101
|
-
shouldHandleClick,
|
|
102
94
|
viewTransition
|
|
103
95
|
]);
|
|
104
96
|
const onMouseEnter = useCallback((event) => {
|
|
@@ -133,23 +125,13 @@ function Link({ href, method = "GET", data, preserveScroll = false, preserveStat
|
|
|
133
125
|
useEffect(() => {
|
|
134
126
|
if (strategies.length === 0) return;
|
|
135
127
|
if (strategies.includes("mount")) doPrefetch();
|
|
136
|
-
|
|
137
|
-
if (strategies.includes("visible") && elementRef.current) {
|
|
138
|
-
observer = new IntersectionObserver((entries) => {
|
|
139
|
-
if (entries.some((e) => e.isIntersecting)) {
|
|
140
|
-
doPrefetch();
|
|
141
|
-
observer?.disconnect();
|
|
142
|
-
observer = null;
|
|
143
|
-
}
|
|
144
|
-
}, { rootMargin: "0px" });
|
|
145
|
-
observer.observe(elementRef.current);
|
|
146
|
-
}
|
|
128
|
+
const stopObserving = strategies.includes("visible") && elementRef.current ? observeLinkVisible(elementRef.current, doPrefetch) : null;
|
|
147
129
|
return () => {
|
|
148
130
|
if (hoverTimerRef.current) {
|
|
149
131
|
clearTimeout(hoverTimerRef.current);
|
|
150
132
|
hoverTimerRef.current = null;
|
|
151
133
|
}
|
|
152
|
-
|
|
134
|
+
stopObserving?.();
|
|
153
135
|
};
|
|
154
136
|
}, [doPrefetch, strategies]);
|
|
155
137
|
const eventHandlers = useMemo(() => {
|
|
@@ -190,10 +172,12 @@ function Link({ href, method = "GET", data, preserveScroll = false, preserveStat
|
|
|
190
172
|
touchAction: "manipulation",
|
|
191
173
|
...style
|
|
192
174
|
}), [style]);
|
|
193
|
-
if (isGet) return /* @__PURE__ */ jsx("a", {
|
|
175
|
+
if (link.isGet) return /* @__PURE__ */ jsx("a", {
|
|
194
176
|
ref: elementRef,
|
|
195
|
-
href:
|
|
177
|
+
href: link.href,
|
|
196
178
|
style: resolvedStyle,
|
|
179
|
+
...activeAttributes,
|
|
180
|
+
...pendingAttributes,
|
|
197
181
|
...rest,
|
|
198
182
|
...eventHandlers,
|
|
199
183
|
children
|
|
@@ -202,6 +186,7 @@ function Link({ href, method = "GET", data, preserveScroll = false, preserveStat
|
|
|
202
186
|
ref: elementRef,
|
|
203
187
|
type: "button",
|
|
204
188
|
style: resolvedStyle,
|
|
189
|
+
...pendingAttributes,
|
|
205
190
|
...rest,
|
|
206
191
|
...eventHandlers,
|
|
207
192
|
children
|
|
@@ -296,6 +281,7 @@ function useForm(url, defaults, options) {
|
|
|
296
281
|
result = await submitAction(router, interpolateRouteParams(url, options?.params), {
|
|
297
282
|
method,
|
|
298
283
|
data: { ...dataRef.current },
|
|
284
|
+
preserveScroll: options?.preserveScroll,
|
|
299
285
|
_resolveOnShell: true
|
|
300
286
|
});
|
|
301
287
|
} catch (submitError) {
|
package/dist/plugin.d.mts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import react from "@vitejs/plugin-react";
|
|
2
|
+
import { PrefetchConfig } from "void/pages";
|
|
2
3
|
import { Plugin } from "vite";
|
|
3
|
-
type PrefetchConfig = {
|
|
4
|
-
hoverDelay?: number;
|
|
5
|
-
cacheFor?: number | string | [string, string];
|
|
6
|
-
};
|
|
7
4
|
export type VoidReactOptions = Readonly<{
|
|
8
5
|
react?: Parameters<typeof react>[0];
|
|
9
6
|
viewTransitions?: boolean;
|
|
10
7
|
prefetch?: PrefetchConfig;
|
|
11
8
|
}>;
|
|
12
|
-
export declare function voidReact(options?: VoidReactOptions): Array<Plugin>;
|
|
13
|
-
export {};
|
|
9
|
+
export declare function voidReact(options?: VoidReactOptions): Array<Plugin>;
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import react from "@vitejs/plugin-react";
|
|
2
|
-
import { createPagesPlugin, generateComponentManifest, resolveEffectiveLayoutChain, resolvePagesDir, scanPages } from "void/pages";
|
|
2
|
+
import { createPagesPlugin, generateComponentManifest, resolveEffectiveLayoutChain, resolvePagesDir, resolvePrefetchConfig, scanPages } from "void/pages";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import nodePath from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import { ISLANDS_CLIENT_ENTRY_ID, ISLANDS_RENDERER_ID, computePagesNeedingClientJS, editIslandSource, findIslandImports, findStaticCssImports, hasIslandPages, islandStylesPlugin, recordIslandStyles, recordedIslandStyles, resolveSpecifier, rewriteCssImportForVirtualModule, watchPagesForRescan } from "void/pages-islands-plugin";
|
|
5
|
+
import { ISLANDS_CLIENT_ENTRY_ID, ISLANDS_RENDERER_ID, computePagesNeedingClientJS, createIslandManifest, editIslandSource, findIslandImports, generateIslandClientEntry, islandCssFiles, islandStylesPlugin, islandsBuildConfig, mdClientScriptIds, recordIslandStyles, recordedIslandStyles, resolveSpecifier, watchPagesForRescan } from "void/pages-islands-plugin";
|
|
7
6
|
// ../../node_modules/.pnpm/pathslash@0.1.0/node_modules/pathslash/dist/index.mjs
|
|
8
7
|
const BACKSLASH_RE = /\\/g;
|
|
9
8
|
const isWindows = process.platform === "win32";
|
|
@@ -145,12 +144,13 @@ ${generateComponentManifest(scan, pagesDir)}
|
|
|
145
144
|
|
|
146
145
|
const PREAMBLE_SCRIPT = ${JSON.stringify(preambleScript)};
|
|
147
146
|
|
|
148
|
-
export async function renderPage(pageObj, assetTags) {
|
|
147
|
+
export async function renderPage(pageObj, assetTags, requestUrl) {
|
|
149
148
|
return renderReactPage({
|
|
150
149
|
pageObj,
|
|
151
150
|
assetTags,
|
|
152
151
|
components,
|
|
153
152
|
layoutTree,
|
|
153
|
+
requestUrl,
|
|
154
154
|
preambleScript: PREAMBLE_SCRIPT,
|
|
155
155
|
});
|
|
156
156
|
}
|
|
@@ -158,6 +158,7 @@ renderPage.preambleScript = PREAMBLE_SCRIPT;
|
|
|
158
158
|
`;
|
|
159
159
|
}
|
|
160
160
|
function generateClientEntry(scan, pagesDir, viewTransitions, prefetch, staticPageData) {
|
|
161
|
+
const { defaultStrategy, hoverDelay, cacheFor } = resolvePrefetchConfig(prefetch);
|
|
161
162
|
return `
|
|
162
163
|
import { startReactPages } from "@void/react/pages-client";
|
|
163
164
|
import { parseCacheFor } from "void/pages-prefetch";
|
|
@@ -172,8 +173,9 @@ function generateClientEntry(scan, pagesDir, viewTransitions, prefetch, staticPa
|
|
|
172
173
|
staticPageData: ${!!staticPageData?.enabled},
|
|
173
174
|
staticPageDataFastPath: ${!!staticPageData?.fastPath},
|
|
174
175
|
viewTransitions: ${!!viewTransitions},
|
|
175
|
-
prefetchHoverDelay: ${
|
|
176
|
-
prefetchDefaultCacheFor: parseCacheFor(${JSON.stringify(
|
|
176
|
+
prefetchHoverDelay: ${hoverDelay},
|
|
177
|
+
prefetchDefaultCacheFor: parseCacheFor(${JSON.stringify(cacheFor)}),
|
|
178
|
+
prefetchDefaultStrategy: ${JSON.stringify(defaultStrategy)},
|
|
177
179
|
});
|
|
178
180
|
export { router };
|
|
179
181
|
`;
|
|
@@ -185,31 +187,31 @@ const COMPONENT_EXTENSIONS = [
|
|
|
185
187
|
".ts",
|
|
186
188
|
".js"
|
|
187
189
|
];
|
|
190
|
+
const HYDRATE_ISLAND = `
|
|
191
|
+
async function hydrateIsland(el, mod, props, frontmatter) {
|
|
192
|
+
const { hydrateRoot } = await import("react-dom/client");
|
|
193
|
+
const React = await import("react");
|
|
194
|
+
const Component = mod.default || mod;
|
|
195
|
+
let island = React.createElement(Component, props);
|
|
196
|
+
if (Object.keys(frontmatter).length > 0) {
|
|
197
|
+
const { FrontmatterContext } = await import("@void/react");
|
|
198
|
+
island = React.createElement(FrontmatterContext.Provider, { value: frontmatter }, island);
|
|
199
|
+
}
|
|
200
|
+
function HydratedIsland() {
|
|
201
|
+
React.useLayoutEffect(() => {
|
|
202
|
+
el.setAttribute("data-hydrated", "true");
|
|
203
|
+
}, []);
|
|
204
|
+
return island;
|
|
205
|
+
}
|
|
206
|
+
hydrateRoot(el, React.createElement(HydratedIsland));
|
|
207
|
+
}`;
|
|
188
208
|
function islandsPlugin(pagesDir, options) {
|
|
189
209
|
let pageScan = null;
|
|
190
210
|
let isDev = false;
|
|
191
211
|
const root = options?.root ?? dirname(pagesDir);
|
|
192
|
-
const islandManifest =
|
|
193
|
-
|
|
212
|
+
const islandManifest = createIslandManifest(root, COMPONENT_EXTENSIONS);
|
|
213
|
+
let cssFiles = /* @__PURE__ */ new Set();
|
|
194
214
|
let resolvedConfig = null;
|
|
195
|
-
/** Resolve an island import and record it in the client manifest. */
|
|
196
|
-
function registerIsland(spec, importerDir, hydrate) {
|
|
197
|
-
const absPath = resolveSpecifier(spec, importerDir, COMPONENT_EXTENSIONS);
|
|
198
|
-
if (!absPath) return {
|
|
199
|
-
absPath,
|
|
200
|
-
islandId: spec
|
|
201
|
-
};
|
|
202
|
-
const islandId = normalizePath(relative(root, absPath)).replace(/\.[^.]+$/, "");
|
|
203
|
-
islandManifest.set(islandId, {
|
|
204
|
-
componentPath: absPath,
|
|
205
|
-
islandId,
|
|
206
|
-
hydrate
|
|
207
|
-
});
|
|
208
|
-
return {
|
|
209
|
-
absPath,
|
|
210
|
-
islandId
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
215
|
return [{
|
|
214
216
|
name: "void-react:islands",
|
|
215
217
|
configResolved(config) {
|
|
@@ -227,33 +229,8 @@ function islandsPlugin(pagesDir, options) {
|
|
|
227
229
|
pagesDir,
|
|
228
230
|
environment: isDev ? "dev" : "prod"
|
|
229
231
|
});
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
filePath: page.componentPath,
|
|
233
|
-
island: page.island
|
|
234
|
-
})),
|
|
235
|
-
...pageScan.layouts.map((layout) => ({
|
|
236
|
-
filePath: layout.filePath,
|
|
237
|
-
island: layout.island
|
|
238
|
-
})),
|
|
239
|
-
...pageScan.namedLayouts.map((namedLayout) => ({
|
|
240
|
-
filePath: namedLayout.filePath,
|
|
241
|
-
island: namedLayout.island
|
|
242
|
-
}))
|
|
243
|
-
];
|
|
244
|
-
for (const entry of filesToScan) {
|
|
245
|
-
if (!entry.island) continue;
|
|
246
|
-
const filePath = resolve(pagesDir, entry.filePath);
|
|
247
|
-
const code = readFileSync(filePath, "utf-8");
|
|
248
|
-
const importerDir = dirname(filePath);
|
|
249
|
-
for (const islandImport of findIslandImports(code, filePath)) registerIsland(islandImport.specifier, importerDir, islandImport.hydrate);
|
|
250
|
-
}
|
|
251
|
-
for (const page of pageScan.pages) {
|
|
252
|
-
if (!page.island) continue;
|
|
253
|
-
if (/\.(tsx|jsx)$/.test(page.componentPath)) islandCssFiles.add(normalizePath(resolve(pagesDir, page.componentPath)));
|
|
254
|
-
const layoutChain = resolveEffectiveLayoutChain(page, pageScan.layouts, pageScan.namedLayouts);
|
|
255
|
-
for (const layout of layoutChain) islandCssFiles.add(normalizePath(resolve(pagesDir, layout.filePath)));
|
|
256
|
-
}
|
|
232
|
+
islandManifest.registerPages(pageScan, pagesDir);
|
|
233
|
+
cssFiles = islandCssFiles(pageScan, pagesDir, /\.(tsx|jsx)$/);
|
|
257
234
|
}
|
|
258
235
|
},
|
|
259
236
|
buildEnd() {
|
|
@@ -270,14 +247,20 @@ function islandsPlugin(pagesDir, options) {
|
|
|
270
247
|
handler(id) {
|
|
271
248
|
if (id === "\0" + ISLANDS_RENDERER_ID) {
|
|
272
249
|
if (!pageScan) return "export function renderIslandPage() { throw new Error('No pages found'); }";
|
|
273
|
-
const
|
|
274
|
-
const mdScriptIds = new Set(mdPlugin?.api?.getClientScripts?.()?.keys() ?? []);
|
|
250
|
+
const mdScriptIds = mdClientScriptIds(resolvedConfig);
|
|
275
251
|
const needsJS = computePagesNeedingClientJS(pageScan, pagesDir, resolvedConfig);
|
|
276
252
|
return generateIslandRenderer(pageScan, pagesDir, isDev, mdScriptIds, needsJS);
|
|
277
253
|
}
|
|
278
254
|
if (id === "\0" + ISLANDS_CLIENT_ENTRY_ID) {
|
|
279
255
|
if (!pageScan) return "";
|
|
280
|
-
return generateIslandClientEntry(
|
|
256
|
+
return generateIslandClientEntry({
|
|
257
|
+
manifest: islandManifest,
|
|
258
|
+
cssFiles,
|
|
259
|
+
styles: recordedIslandStyles(pagesDir),
|
|
260
|
+
needsRuntime: islandManifest.entries.size > 0 || computePagesNeedingClientJS(pageScan, pagesDir, resolvedConfig).size > 0,
|
|
261
|
+
isDev,
|
|
262
|
+
hydrateIsland: HYDRATE_ISLAND
|
|
263
|
+
});
|
|
281
264
|
}
|
|
282
265
|
}
|
|
283
266
|
},
|
|
@@ -295,7 +278,7 @@ function islandsPlugin(pagesDir, options) {
|
|
|
295
278
|
const strategy = islandImport.hydrate;
|
|
296
279
|
const spec = islandImport.specifier;
|
|
297
280
|
const rawName = `_raw_island_${idx++}`;
|
|
298
|
-
const { islandId } =
|
|
281
|
+
const { islandId } = islandManifest.register(spec, importerDir, strategy);
|
|
299
282
|
const replacement = `import ${rawName} from ${specRaw}`;
|
|
300
283
|
edits.push({
|
|
301
284
|
start: islandImport.start,
|
|
@@ -311,10 +294,7 @@ function islandsPlugin(pagesDir, options) {
|
|
|
311
294
|
}
|
|
312
295
|
},
|
|
313
296
|
async config(_, env) {
|
|
314
|
-
if (env.command !== "serve"
|
|
315
|
-
manifest: true,
|
|
316
|
-
rollupOptions: { input: { "islands-client": ISLANDS_CLIENT_ENTRY_ID } }
|
|
317
|
-
} } } };
|
|
297
|
+
if (env.command !== "serve") return islandsBuildConfig(root, pagesDir);
|
|
318
298
|
}
|
|
319
299
|
}, islandStylesPlugin(pagesDir)];
|
|
320
300
|
}
|
|
@@ -391,44 +371,6 @@ export async function renderIslandPage(pageObj, assetTags) {
|
|
|
391
371
|
}
|
|
392
372
|
`;
|
|
393
373
|
}
|
|
394
|
-
function generateIslandClientEntry(manifest, cssFiles, styles, needsRuntime, isDev) {
|
|
395
|
-
const entries = [...manifest.values()];
|
|
396
|
-
const uniqueIslands = /* @__PURE__ */ new Map();
|
|
397
|
-
for (const entry of entries) uniqueIslands.set(entry.islandId, entry.componentPath);
|
|
398
|
-
const lines = [];
|
|
399
|
-
if (!isDev && styles) for (const id of styles) lines.push(`import ${JSON.stringify(id)};`);
|
|
400
|
-
else if (!isDev) for (const file of cssFiles) {
|
|
401
|
-
const content = readFileSync(file, "utf-8");
|
|
402
|
-
for (const item of findStaticCssImports(content, file)) lines.push(rewriteCssImportForVirtualModule(item.statement, item.specifier, file));
|
|
403
|
-
}
|
|
404
|
-
if (!needsRuntime && !isDev) return lines.join("\n");
|
|
405
|
-
lines.push("import { initIslands } from \"void/pages-client\";");
|
|
406
|
-
lines.push("const islands = {");
|
|
407
|
-
for (const [id, path] of uniqueIslands) lines.push(` ${JSON.stringify(id)}: () => import(${JSON.stringify(path)}),`);
|
|
408
|
-
lines.push("};");
|
|
409
|
-
lines.push(`
|
|
410
|
-
async function hydrateIsland(el, mod, props, frontmatter) {
|
|
411
|
-
const { hydrateRoot } = await import("react-dom/client");
|
|
412
|
-
const React = await import("react");
|
|
413
|
-
const Component = mod.default || mod;
|
|
414
|
-
let island = React.createElement(Component, props);
|
|
415
|
-
if (Object.keys(frontmatter).length > 0) {
|
|
416
|
-
const { FrontmatterContext } = await import("@void/react");
|
|
417
|
-
island = React.createElement(FrontmatterContext.Provider, { value: frontmatter }, island);
|
|
418
|
-
}
|
|
419
|
-
function HydratedIsland() {
|
|
420
|
-
React.useLayoutEffect(() => {
|
|
421
|
-
el.setAttribute("data-hydrated", "true");
|
|
422
|
-
}, []);
|
|
423
|
-
return island;
|
|
424
|
-
}
|
|
425
|
-
hydrateRoot(el, React.createElement(HydratedIsland));
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
initIslands(islands, hydrateIsland);
|
|
429
|
-
`);
|
|
430
|
-
return lines.join("\n");
|
|
431
|
-
}
|
|
432
374
|
// src/plugin.ts
|
|
433
375
|
function voidReact(options) {
|
|
434
376
|
const root = process.cwd();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ComponentType } from "react";
|
|
2
2
|
import { VoidRouter } from "void/pages-client";
|
|
3
|
-
import { CacheForResult } from "void/pages-prefetch";
|
|
3
|
+
import { CacheForResult, LinkPrefetchValue } from "void/pages-prefetch";
|
|
4
4
|
import { PageObject } from "void/pages-protocol";
|
|
5
5
|
type ComponentModule = {
|
|
6
6
|
default: ComponentType<any>;
|
|
@@ -18,6 +18,7 @@ export type ReactPagesConfig = Readonly<{
|
|
|
18
18
|
viewTransitions: boolean;
|
|
19
19
|
prefetchHoverDelay: number;
|
|
20
20
|
prefetchDefaultCacheFor: CacheForResult;
|
|
21
|
+
prefetchDefaultStrategy?: LinkPrefetchValue;
|
|
21
22
|
cacheVisitedPageData?: boolean;
|
|
22
23
|
/** Show this fallback for client visits whose destination remains pending at the next paint. */
|
|
23
24
|
navigationFallback?: ComponentType<{
|
|
@@ -53,7 +53,7 @@ function currentUrl() {
|
|
|
53
53
|
function normalizeUrl(value) {
|
|
54
54
|
const urlString = toUrlString(value);
|
|
55
55
|
if (urlString.startsWith("#")) return urlString;
|
|
56
|
-
const url = new URL(urlString, window.location.
|
|
56
|
+
const url = new URL(urlString, window.location.href);
|
|
57
57
|
return url.origin === window.location.origin ? url.pathname + url.search + url.hash : url.href;
|
|
58
58
|
}
|
|
59
59
|
/** Page data holds a decoded path. Compare it with browser URLs in their encoded form. */
|
|
@@ -142,6 +142,7 @@ function appReducer(state, action) {
|
|
|
142
142
|
function createRouter(engine) {
|
|
143
143
|
const router = {
|
|
144
144
|
_hoverDelay: engine._hoverDelay,
|
|
145
|
+
_prefetchDefaultStrategy: engine._prefetchDefaultStrategy,
|
|
145
146
|
get url() {
|
|
146
147
|
return new URL(currentUrl(), window.location.origin);
|
|
147
148
|
},
|
|
@@ -307,6 +308,7 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
|
|
|
307
308
|
}, []);
|
|
308
309
|
const schedule = useCallback((url, options = {}) => {
|
|
309
310
|
const prepared = engine.prepareVisit(url, options);
|
|
311
|
+
const resolveOnData = prepared.options.resolveOn === "data" || prepared.options._resolveOnShell;
|
|
310
312
|
if (prepared.immediate) {
|
|
311
313
|
const navigation = navigationForResource(prepared);
|
|
312
314
|
activePendingNavigationId = prepared.id;
|
|
@@ -356,7 +358,7 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
|
|
|
356
358
|
location: currentUrl()
|
|
357
359
|
});
|
|
358
360
|
resolveVisit(void 0);
|
|
359
|
-
} else if (
|
|
361
|
+
} else if (resolveOnData) {
|
|
360
362
|
pendingVisitSettlers.delete(prepared.id);
|
|
361
363
|
resolveVisit(result.pageData);
|
|
362
364
|
}
|
|
@@ -371,7 +373,7 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
|
|
|
371
373
|
if (prepared.method.toUpperCase() === "GET" && !prepared.options._resolveOnShell && isAbortError(error)) resolveVisit(void 0);
|
|
372
374
|
else rejectVisit(error);
|
|
373
375
|
if (activePendingNavigationId === prepared.id) activePendingNavigationId = null;
|
|
374
|
-
if (NavigationFallback ||
|
|
376
|
+
if (NavigationFallback || resolveOnData || isAbortError(error) || isCallSiteActionError(error)) {
|
|
375
377
|
dispatch({
|
|
376
378
|
type: "abandon",
|
|
377
379
|
resource: prepared,
|
|
@@ -491,6 +493,7 @@ async function startReactPages(config) {
|
|
|
491
493
|
viewTransitions: config.viewTransitions,
|
|
492
494
|
prefetchHoverDelay: config.prefetchHoverDelay,
|
|
493
495
|
prefetchDefaultCacheFor: config.prefetchDefaultCacheFor,
|
|
496
|
+
prefetchDefaultStrategy: config.prefetchDefaultStrategy,
|
|
494
497
|
cacheVisitedPageData: config.cacheVisitedPageData
|
|
495
498
|
});
|
|
496
499
|
const router = createRouter(engine);
|
|
@@ -12,7 +12,8 @@ export type ReactPagesServerConfig = Readonly<{
|
|
|
12
12
|
};
|
|
13
13
|
components: Record<string, () => Promise<ComponentModule>>;
|
|
14
14
|
layoutTree: Record<string, Array<string>>;
|
|
15
|
+
requestUrl?: string;
|
|
15
16
|
preambleScript?: string;
|
|
16
17
|
}>;
|
|
17
|
-
export declare function renderReactPage({ pageObj, assetTags, components, layoutTree, preambleScript }: ReactPagesServerConfig): Promise<ReadableStream<Uint8Array>>;
|
|
18
|
+
export declare function renderReactPage({ pageObj, assetTags, components, layoutTree, requestUrl, preambleScript }: ReactPagesServerConfig): Promise<ReadableStream<Uint8Array>>;
|
|
18
19
|
export {};
|
|
@@ -111,14 +111,14 @@ function textStream(text) {
|
|
|
111
111
|
controller.close();
|
|
112
112
|
} });
|
|
113
113
|
}
|
|
114
|
-
async function renderReactPage({ pageObj, assetTags, components, layoutTree, preambleScript = "" }) {
|
|
114
|
+
async function renderReactPage({ pageObj, assetTags, components, layoutTree, requestUrl, preambleScript = "" }) {
|
|
115
115
|
const PageComponent = (await components[pageObj.component]()).default;
|
|
116
116
|
const layoutIds = layoutTree[pageObj.component] || [];
|
|
117
117
|
const layoutComponents = await Promise.all(layoutIds.map(async (id) => (await components[id]()).default));
|
|
118
118
|
const renderProps = propsForReactRender(pageObj);
|
|
119
119
|
let element = React.createElement(PageComponent, renderProps);
|
|
120
120
|
for (let i = layoutComponents.length - 1; i >= 0; i--) element = React.createElement(layoutComponents[i], null, element);
|
|
121
|
-
element = React.createElement(RouterContext.Provider, { value: createSsrRouter(pageObj.url || "/", pageObj.params ?? {}) }, React.createElement(NavigationContext.Provider, { value: idleNavigationState() }, React.createElement(React.Suspense, { fallback: null }, React.createElement(SharedContext.Provider, { value: pageObj.shared || {} }, React.createElement(ParamsContext.Provider, { value: pageObj.params ?? {} }, React.createElement(ErrorsContext.Provider, { value: pageObj.errors || {} }, element))))));
|
|
121
|
+
element = React.createElement(RouterContext.Provider, { value: createSsrRouter(pageObj.url || "/", pageObj.params ?? {}, requestUrl) }, React.createElement(NavigationContext.Provider, { value: idleNavigationState() }, React.createElement(React.Suspense, { fallback: null }, React.createElement(SharedContext.Provider, { value: pageObj.shared || {} }, React.createElement(ParamsContext.Provider, { value: pageObj.params ?? {} }, React.createElement(ErrorsContext.Provider, { value: pageObj.errors || {} }, element))))));
|
|
122
122
|
const renderElement = pageObj.deferredKeys ? React.createElement(React.Fragment, null, element, React.createElement("template", { id: DEFERRED_SHELL_END_ID })) : element;
|
|
123
123
|
let shellError;
|
|
124
124
|
let shellSent = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@void/react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "React pages, server rendering, forms, and navigation for Void.",
|
|
5
5
|
"homepage": "https://void.cloud",
|
|
6
6
|
"bugs": {
|
|
@@ -50,13 +50,13 @@
|
|
|
50
50
|
"react-dom": "^19.3.1",
|
|
51
51
|
"vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
|
|
52
52
|
"vite-plus": "1.4.0",
|
|
53
|
-
"void": "1.
|
|
53
|
+
"void": "1.6.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"react": "^19.0.0",
|
|
57
57
|
"react-dom": "^19.0.0",
|
|
58
58
|
"vite": "^8.0.0",
|
|
59
|
-
"void": "1.
|
|
59
|
+
"void": "1.6.0"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "vp pack",
|