@void/react 1.5.0 → 1.6.1

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AnchorHTMLAttributes, ButtonHTMLAttributes, Context, FormHTMLAttributes, JSX, ReactElement, ReactNode } from "react";
2
- import { ActionClient, FormMethod, FormModel, FormResetOptions, NavigationState, VoidActionError, VoidRouter } from "void/pages-client";
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 PrefetchStrategy = "hover" | "click" | "mount" | "visible";
12
- type LinkMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
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, isModifiedEvent, mergeDataIntoHref, parseFormResetArgs, scheduleFormRetry, ssrProxy, submitAction, submitIslandAction } from "void/pages-client";
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: prefetchProp = false, cacheFor, onNavigate, children, style, onClick: onClickProp, onMouseEnter: onMouseEnterProp, onMouseLeave: onMouseLeaveProp, onFocus: onFocusProp, onMouseDown: onMouseDownProp, onTouchStart: onTouchStartProp, ...rest }) {
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 normalizedMethod = useMemo(() => method.toUpperCase(), [method]);
33
- const isGet = normalizedMethod === "GET";
34
- const hrefWithData = useMemo(() => isGet ? mergeDataIntoHref(href, data) : href, [
33
+ const link = useMemo(() => resolveLink({
34
+ href,
35
+ method,
36
+ data,
37
+ prefetch,
38
+ reloadDocument
39
+ }), [
35
40
  data,
36
41
  href,
37
- isGet
42
+ method,
43
+ prefetch,
44
+ reloadDocument
38
45
  ]);
39
- const strategies = useMemo(() => {
40
- if (prefetchProp === false || reloadDocument) return [];
41
- if (prefetchProp === true) return ["hover"];
42
- if (typeof prefetchProp === "string") return [prefetchProp];
43
- return prefetchProp;
44
- }, [prefetchProp, reloadDocument]);
45
- const hoverDelay = router?._hoverDelay ?? 75;
46
- if (prefetchProp !== false && !isGet) throw new Error("Link: prefetch only supports GET links. Remove `prefetch` or use method=\"GET\".");
47
- if (reloadDocument && !isGet) throw new Error("Link: reloadDocument only supports GET links.");
48
- const doPrefetch = useCallback(() => {
49
- if (!router?.prefetch) return;
50
- const current = window.location.pathname + window.location.search;
51
- if (hrefWithData === current) return;
52
- const options = {};
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 shouldHandleClick = useCallback((event) => {
61
- if (reloadDocument || !router || event.defaultPrevented || isModifiedEvent(event)) return false;
62
- if (event.currentTarget.hasAttribute("download")) return false;
63
- if (new URL(hrefWithData, window.location.origin).origin !== window.location.origin) return false;
64
- return true;
65
- }, [
66
- hrefWithData,
67
- reloadDocument,
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
- if (!shouldHandleClick(event)) return;
73
- let navigatePrevented = false;
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
- viewTransition
80
+ reloadDocument,
81
+ viewTransition,
82
+ onNavigate
89
83
  });
90
84
  }, [
91
85
  data,
92
- hrefWithData,
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
- let observer = null;
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
- if (observer) observer.disconnect();
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: hrefWithData,
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 { normalizePath } from "vite";
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: ${prefetch?.hoverDelay ?? 75},
176
- prefetchDefaultCacheFor: parseCacheFor(${JSON.stringify(prefetch?.cacheFor ?? "30s")}),
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 = /* @__PURE__ */ new Map();
193
- const islandCssFiles = /* @__PURE__ */ new Set();
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
- const filesToScan = [
231
- ...pageScan.pages.map((page) => ({
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 mdPlugin = resolvedConfig?.plugins?.find((plugin) => plugin.name === "void-md");
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(islandManifest, islandCssFiles, recordedIslandStyles(pagesDir), islandManifest.size > 0 || computePagesNeedingClientJS(pageScan, pagesDir, resolvedConfig).size > 0, isDev);
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 } = registerIsland(spec, importerDir, strategy);
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" && await hasIslandPages(root, pagesDir)) return { environments: { client: { build: {
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.origin);
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 (prepared.options._resolveOnShell) {
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 || prepared.options._resolveOnShell || isAbortError(error) || isCallSiteActionError(error)) {
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.5.0",
3
+ "version": "1.6.1",
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.5.0"
53
+ "void": "1.6.1"
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.5.0"
59
+ "void": "1.6.1"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "vp pack",