@void/react 1.3.0 → 1.5.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.
@@ -1,7 +1,6 @@
1
1
  import { createActionClient } from "void/pages-client";
2
- //#region src/runtime/action.ts
3
- const client = createActionClient();
2
+ // src/runtime/action.ts
3
+ const client = createActionClient({ _resolveOnShell: true });
4
4
  const action = client.action;
5
5
  const setActionRouter = client.setActionRouter;
6
- //#endregion
7
6
  export { setActionRouter as n, action as t };
@@ -1,11 +1,10 @@
1
1
  import { createContext } from "react";
2
2
  import { idleNavigationState } from "void/pages-client";
3
- //#region src/runtime/context.ts
3
+ // src/runtime/context.ts
4
4
  const SharedContext = createContext(null);
5
5
  const ErrorsContext = createContext(null);
6
6
  const ParamsContext = createContext(null);
7
7
  const RouterContext = createContext(null);
8
8
  const NavigationContext = createContext(idleNavigationState());
9
9
  const FrontmatterContext = createContext({});
10
- //#endregion
11
10
  export { RouterContext as a, ParamsContext as i, FrontmatterContext as n, SharedContext as o, NavigationContext as r, ErrorsContext as t };
@@ -1,6 +1,5 @@
1
- //#region src/runtime/deferred-shell.ts
1
+ // src/runtime/deferred-shell.ts
2
2
  const DEFERRED_SHELL_END_ID = "__VOID_DEFERRED_SHELL_END__";
3
3
  const DEFERRED_SHELL_END_MARKER = `<template id="${DEFERRED_SHELL_END_ID}"></template>`;
4
4
  const DEFERRED_SHELL_CANCEL_REASON = "Void deferred shell complete";
5
- //#endregion
6
5
  export { DEFERRED_SHELL_END_ID as n, DEFERRED_SHELL_END_MARKER as r, DEFERRED_SHELL_CANCEL_REASON as t };
package/dist/index.d.mts CHANGED
@@ -2,15 +2,12 @@ import { AnchorHTMLAttributes, ButtonHTMLAttributes, Context, FormHTMLAttributes
2
2
  import { ActionClient, FormMethod, FormModel, FormResetOptions, NavigationState, VoidActionError, VoidRouter } from "void/pages-client";
3
3
  import { CloudContextVariables, InferProps } from "void";
4
4
  import { ActionFormOptions, ActionUrl, ResolveActionBody } from "void/routes";
5
- //#region src/runtime/form.d.ts
6
5
  type FormProps = Omit<FormHTMLAttributes<HTMLFormElement>, "action" | "onSubmit" | "method" | "onError"> & {
7
6
  form: FormModel<[FormData]>;
8
7
  method?: FormMethod;
9
8
  renderError?: (error: VoidActionError) => ReactNode;
10
9
  };
11
10
  export declare function Form({ form, method, renderError, children, ...props }: FormProps): ReactElement;
12
- //#endregion
13
- //#region src/runtime/link.d.ts
14
11
  type PrefetchStrategy = "hover" | "click" | "mount" | "visible";
15
12
  type LinkMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
16
13
  type CommonProps = {
@@ -30,18 +27,16 @@ type CommonProps = {
30
27
  };
31
28
  type LinkProps = CommonProps & Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof CommonProps> & Omit<ButtonHTMLAttributes<HTMLButtonElement>, keyof CommonProps>;
32
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;
33
- //#endregion
34
- //#region src/runtime/use-router.d.ts
35
30
  export declare function useRouter(): VoidRouter;
36
- //#endregion
37
- //#region src/runtime/use-params.d.ts
38
31
  export declare function useParams<T extends Record<string, string> = Record<string, string>>(): T;
39
- //#endregion
40
- //#region src/runtime/use-shared.d.ts
41
32
  export declare function useShared<T = CloudContextVariables["shared"]>(): T;
42
- //#endregion
43
- //#region src/runtime/use-form.d.ts
44
- export declare function useForm<U extends ActionUrl & string>(url: U, defaults: ResolveActionBody<U>, options?: ActionFormOptions<U>): {
33
+ type FormOptions = {
34
+ params?: Record<string, string>;
35
+ resetOnSuccess?: boolean;
36
+ onSuccess?: () => void | Promise<void>;
37
+ onError?: (error: Record<string, string> | VoidActionError) => void | Promise<void>;
38
+ };
39
+ export declare function useForm<U extends ActionUrl & string>(url: U, defaults: ResolveActionBody<U>, options?: ActionFormOptions<U> & Pick<FormOptions, "onSuccess" | "onError">): {
45
40
  data: ResolveActionBody<U>;
46
41
  setData: <K extends keyof ResolveActionBody<U> & string>(field: K, value: ResolveActionBody<U>[K]) => void;
47
42
  errors: Partial<Record<keyof ResolveActionBody<U> & string, string>>;
@@ -60,11 +55,7 @@ export declare function useForm<U extends ActionUrl & string>(url: U, defaults:
60
55
  clearErrors(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
61
56
  clearError(): void;
62
57
  };
63
- //#endregion
64
- //#region src/runtime/use-navigation.d.ts
65
58
  export declare function useNavigation(): NavigationState;
66
- //#endregion
67
- //#region src/runtime/use-island-form.d.ts
68
59
  type IslandFormReturn<T extends Record<string, unknown>> = Readonly<{
69
60
  data: T;
70
61
  setData: <K extends keyof T & string>(field: K, value: T[K]) => void;
@@ -87,20 +78,13 @@ type IslandFormReturn<T extends Record<string, unknown>> = Readonly<{
87
78
  * Void Router (which is not available in island mode).
88
79
  */
89
80
  export declare function useIslandForm<T extends Record<string, unknown>>(defaults: T): IslandFormReturn<T>;
90
- //#endregion
91
- //#region src/runtime/action.d.ts
92
81
  export declare const action: ActionClient["action"];
93
82
  export declare const setActionRouter: ActionClient["setActionRouter"];
94
- //#endregion
95
- //#region src/runtime/context.d.ts
96
83
  export declare const SharedContext: Context<Record<string, unknown> | null>;
97
84
  export declare const ErrorsContext: Context<Record<string, string> | null>;
98
85
  export declare const ParamsContext: Context<Record<string, string> | null>;
99
86
  export declare const RouterContext: Context<VoidRouter | null>;
100
87
  export declare const NavigationContext: Context<NavigationState>;
101
88
  export declare const FrontmatterContext: Context<Record<string, unknown>>;
102
- //#endregion
103
- //#region src/runtime/index.d.ts
104
89
  export type Deferred<T> = Promise<T>;
105
- //#endregion
106
90
  export type { InferProps };
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
- import { a as RouterContext, i as ParamsContext, n as FrontmatterContext, o as SharedContext, r as NavigationContext, t as ErrorsContext } from "./context-D2ctU4yT.mjs";
2
- import { n as setActionRouter, t as action } from "./action-4EvQppia.mjs";
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
+ 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 { VoidActionError, categorizeActionError, cloneFormValue, formErrorMessage, formRetryMessage, formSubmissionBlocked, getValidationErrors, interpolateRouteParams, isAbortError, isEqualFormValue, isModifiedEvent, mergeDataIntoHref, parseFormResetArgs, scheduleFormRetry, ssrProxy, submitAction } from "void/pages-client";
4
+ import { cloneFormValue, formErrorMessage, formRetryMessage, formSubmissionBlocked, getValidationErrors, interpolateRouteParams, isAbortError, isEqualFormValue, isModifiedEvent, mergeDataIntoHref, parseFormResetArgs, scheduleFormRetry, ssrProxy, submitAction, submitIslandAction } from "void/pages-client";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
- //#region src/runtime/form.tsx
6
+ // src/runtime/form.tsx
7
7
  function Form({ form, method = "post", renderError, children, ...props }) {
8
8
  const [now, setNow] = useState(Date.now);
9
9
  useEffect(() => scheduleFormRetry(form.error, () => setNow(Date.now())), [form.error]);
@@ -24,8 +24,7 @@ function Form({ form, method = "post", renderError, children, ...props }) {
24
24
  }), children]
25
25
  });
26
26
  }
27
- //#endregion
28
- //#region src/runtime/link.tsx
27
+ // src/runtime/link.tsx
29
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 }) {
30
29
  const router = useContext(RouterContext);
31
30
  const elementRef = useRef(null);
@@ -208,27 +207,23 @@ function Link({ href, method = "GET", data, preserveScroll = false, preserveStat
208
207
  children
209
208
  });
210
209
  }
211
- //#endregion
212
- //#region src/runtime/use-router.ts
210
+ // src/runtime/use-router.ts
213
211
  function useRouter() {
214
212
  return useContext(RouterContext) ?? ssrProxy;
215
213
  }
216
- //#endregion
217
- //#region src/runtime/use-params.ts
214
+ // src/runtime/use-params.ts
218
215
  function useParams() {
219
216
  const params = useContext(ParamsContext);
220
217
  if (params === null) throw new Error("useParams(): must be used inside a Void page.");
221
218
  return params;
222
219
  }
223
- //#endregion
224
- //#region src/runtime/use-shared.ts
220
+ // src/runtime/use-shared.ts
225
221
  function useShared() {
226
222
  const shared = useContext(SharedContext);
227
223
  if (!shared) throw new Error("useShared(): must be used inside a Void page.");
228
224
  return shared;
229
225
  }
230
- //#endregion
231
- //#region src/runtime/use-form.ts
226
+ // src/runtime/use-form.ts
232
227
  function useForm(url, defaults, options) {
233
228
  const router = useContext(RouterContext);
234
229
  const [data, setDataState] = useState(() => cloneFormValue(defaults));
@@ -309,8 +304,13 @@ function useForm(url, defaults, options) {
309
304
  }
310
305
  if (!result.ok) {
311
306
  const validationErrors = getValidationErrors(result.error.body);
312
- if (validationErrors) setErrors(validationErrors);
313
- else setActionError(result.error);
307
+ if (validationErrors) {
308
+ setErrors(validationErrors);
309
+ await options?.onError?.(validationErrors);
310
+ } else {
311
+ setActionError(result.error);
312
+ await options?.onError?.(result.error);
313
+ }
314
314
  return previousSuccessVersion;
315
315
  }
316
316
  if (options?.resetOnSuccess) {
@@ -326,6 +326,7 @@ function useForm(url, defaults, options) {
326
326
  setRecentlySuccessful(false);
327
327
  successTimeoutRef.current = null;
328
328
  }, 2e3);
329
+ await options?.onSuccess?.();
329
330
  return previousSuccessVersion + 1;
330
331
  }, 0);
331
332
  useEffect(() => {
@@ -358,22 +359,11 @@ function useForm(url, defaults, options) {
358
359
  clearError
359
360
  };
360
361
  }
361
- //#endregion
362
- //#region src/runtime/use-navigation.ts
362
+ // src/runtime/use-navigation.ts
363
363
  function useNavigation() {
364
364
  return useContext(NavigationContext);
365
365
  }
366
- //#endregion
367
- //#region src/runtime/use-island-form.ts
368
- async function readActionErrorBody(response) {
369
- const contentType = response.headers.get("content-type") || "";
370
- try {
371
- if (contentType.includes("application/json")) return await response.json();
372
- return await response.text() || null;
373
- } catch {
374
- return null;
375
- }
376
- }
366
+ // src/runtime/use-island-form.ts
377
367
  /**
378
368
  * Form hook for island pages. Uses fetch + page reload instead of the
379
369
  * Void Router (which is not available in island mode).
@@ -424,16 +414,12 @@ function useIslandForm(defaults) {
424
414
  setErrors({});
425
415
  setError(null);
426
416
  try {
427
- const response = await fetch(url, {
428
- method,
429
- headers: { "Content-Type": "application/json" },
430
- body: JSON.stringify(data)
431
- });
432
- if (response.redirected) {
433
- window.location.href = response.url;
417
+ const result = await submitIslandAction(url, method, data);
418
+ if (result.kind === "redirect") {
419
+ window.location.href = result.url;
434
420
  return;
435
421
  }
436
- if (response.ok) {
422
+ if (result.kind === "success") {
437
423
  setWasSuccessful(true);
438
424
  setRecentlySuccessful(true);
439
425
  const nextDefaults = { ...data };
@@ -441,22 +427,8 @@ function useIslandForm(defaults) {
441
427
  setDefaultsState(nextDefaults);
442
428
  successTimeoutRef.current = setTimeout(() => setRecentlySuccessful(false), 2e3);
443
429
  window.location.reload();
444
- } else {
445
- const body = await readActionErrorBody(response);
446
- const actionError = new VoidActionError({
447
- body,
448
- status: response.status,
449
- statusText: response.statusText,
450
- url
451
- });
452
- const validationErrors = response.status === 422 ? getValidationErrors(body) : null;
453
- if (validationErrors) {
454
- setErrors(validationErrors);
455
- return;
456
- }
457
- if (categorizeActionError(response.status) === "boundary") throw actionError;
458
- setError(actionError);
459
- }
430
+ } else if (result.kind === "validation") setErrors(result.errors);
431
+ else setError(result.error);
460
432
  } finally {
461
433
  setPending(false);
462
434
  }
@@ -479,5 +451,4 @@ function useIslandForm(defaults) {
479
451
  delete: (url) => submit(url, "DELETE")
480
452
  };
481
453
  }
482
- //#endregion
483
454
  export { ErrorsContext, Form, FrontmatterContext, Link, NavigationContext, ParamsContext, RouterContext, SharedContext, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
package/dist/plugin.d.mts CHANGED
@@ -1,17 +1,13 @@
1
1
  import react from "@vitejs/plugin-react";
2
2
  import { Plugin } from "vite";
3
- //#region src/plugin-pages.d.ts
4
3
  type PrefetchConfig = {
5
4
  hoverDelay?: number;
6
5
  cacheFor?: number | string | [string, string];
7
6
  };
8
- //#endregion
9
- //#region src/plugin.d.ts
10
7
  export type VoidReactOptions = Readonly<{
11
8
  react?: Parameters<typeof react>[0];
12
9
  viewTransitions?: boolean;
13
10
  prefetch?: PrefetchConfig;
14
11
  }>;
15
12
  export declare function voidReact(options?: VoidReactOptions): Array<Plugin>;
16
- //#endregion
17
13
  export {};
package/dist/plugin.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import react from "@vitejs/plugin-react";
2
- import { generateComponentManifest, resolveEffectiveLayoutChain, resolvePagesDir, resolveStaticPageDataBuildOptions, scanPages } from "void/pages";
2
+ import { createPagesPlugin, generateComponentManifest, resolveEffectiveLayoutChain, resolvePagesDir, scanPages } from "void/pages";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import nodePath from "node:path";
5
5
  import { normalizePath } from "vite";
6
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";
7
- //#region ../../node_modules/.pnpm/pathslash@0.1.0/node_modules/pathslash/dist/index.mjs
7
+ // ../../node_modules/.pnpm/pathslash@0.1.0/node_modules/pathslash/dist/index.mjs
8
8
  const BACKSLASH_RE = /\\/g;
9
9
  const isWindows = process.platform === "win32";
10
10
  /**
@@ -42,9 +42,7 @@ const posix = { ...nodePath.posix };
42
42
  posix.posix = win32.posix = posix;
43
43
  posix.win32 = win32.win32 = win32;
44
44
  const { basename, delimiter, dirname, extname, format, isAbsolute, join, matchesGlob, normalize, parse, relative, resolve, sep, toNamespacedPath } = isWindows ? win32 : posix;
45
- //#endregion
46
- //#region src/plugin-pages.ts
47
- const RENDERER_ID = "virtual:void-pages-renderer";
45
+ // src/plugin-pages.ts
48
46
  const CLIENT_ENTRY_ID = "virtual:void-pages-client";
49
47
  const WORKER_ENVIRONMENT_NAME = "void_worker";
50
48
  const REACT_DEDUPE = ["react", "react-dom"];
@@ -69,88 +67,28 @@ const REACT_REFRESH_PREAMBLE = `
69
67
  window.__vite_plugin_react_preamble_installed__ = true
70
68
  <\/script>`;
71
69
  function pagesPlugin(pagesDir, options) {
72
- let pageScan = null;
73
- let isDev = false;
74
- let viteConfig;
75
70
  const root = options?.root ?? dirname(pagesDir);
76
- const normalizedPagesDir = normalizePath(pagesDir);
77
71
  const pagesOptimizeEntries = `${relative(root, pagesDir)}/**/*.{js,jsx,ts,tsx}`;
78
- let pendingRescan = null;
79
- const rescanPages = async () => {
80
- pageScan = existsSync(pagesDir) ? await scanPages(root, {
81
- pagesDir,
82
- environment: isDev ? "dev" : "prod"
83
- }) : null;
84
- };
85
- const runRescan = () => {
86
- pendingRescan ??= rescanPages().finally(() => {
87
- pendingRescan = null;
88
- });
89
- return pendingRescan;
90
- };
91
- const invalidatePageModules = (server) => {
92
- for (const env of Object.values(server.environments)) for (const id of ["\0" + RENDERER_ID, "\0" + CLIENT_ENTRY_ID]) {
93
- const mod = env.moduleGraph.getModuleById(id);
94
- if (mod) env.moduleGraph.invalidateModule(mod);
95
- }
96
- };
97
- return {
72
+ return createPagesPlugin(pagesDir, {
98
73
  name: "void-react:pages",
99
- async buildStart() {
100
- await runRescan();
101
- },
102
- resolveId: {
103
- filter: { id: /^virtual:void-pages-(renderer|client)$/ },
104
- handler(id) {
105
- return "\0" + id;
106
- }
107
- },
108
- load: {
109
- filter: { id: /^\0virtual:void-pages-(renderer|client)$/ },
110
- async handler(id) {
111
- if (isDev && pendingRescan) await pendingRescan;
112
- if (!pageScan && existsSync(pagesDir)) await runRescan();
113
- if (id === "\0" + RENDERER_ID) {
114
- if (!pageScan) return "export function renderPage() { throw new Error('No pages found'); }";
115
- return generateRendererModule(pageScan, pagesDir, isDev);
116
- }
117
- if (id === "\0" + CLIENT_ENTRY_ID) {
118
- if (!pageScan) return "";
119
- return generateClientEntry(pageScan, pagesDir, options?.viewTransitions, options?.prefetch, resolveStaticPageDataBuildOptions(root, { publicDir: viteConfig?.publicDir }));
120
- }
121
- }
74
+ root,
75
+ generateRenderer(scan, isDev) {
76
+ return generateRendererModule(scan, pagesDir, isDev);
122
77
  },
123
- configureServer(server) {
124
- const handlePagesChange = async (path, event) => {
125
- const normalizedPath = normalizePath(path);
126
- if (normalizedPath !== normalizedPagesDir && !normalizedPath.startsWith(normalizedPagesDir + "/")) return;
127
- if (event === "change" && !normalizedPath.endsWith(".md")) return;
128
- try {
129
- await runRescan();
130
- } catch (error) {
131
- server.config.logger.error(error instanceof Error ? error.message : String(error), {
132
- error: error instanceof Error ? error : void 0,
133
- timestamp: true
134
- });
135
- return;
136
- }
137
- invalidatePageModules(server);
138
- };
139
- server.watcher.on("add", (path) => handlePagesChange(path, "add"));
140
- server.watcher.on("change", (path) => handlePagesChange(path, "change"));
141
- server.watcher.on("unlink", (path) => handlePagesChange(path, "unlink"));
142
- },
143
- configResolved(config) {
144
- viteConfig = config;
78
+ generateClient(scan, staticPageData) {
79
+ return generateClientEntry(scan, pagesDir, options?.viewTransitions, options?.prefetch, staticPageData);
145
80
  },
146
- async config(_, env) {
147
- isDev = env.command === "serve";
148
- if (!existsSync(pagesDir)) return;
81
+ async config({ isDev, scan }) {
149
82
  if (isDev) return {
150
83
  resolve: { dedupe: REACT_DEDUPE },
151
84
  optimizeDeps: {
152
85
  exclude: ["@void/react"],
153
- include: CLIENT_OPTIMIZE_DEPS
86
+ entries: clientOptimizeEntries(await scan(), pagesDir, root),
87
+ include: [
88
+ ...CLIENT_OPTIMIZE_DEPS,
89
+ "void/pages-client",
90
+ "void/pages-prefetch"
91
+ ]
154
92
  },
155
93
  ssr: { optimizeDeps: { include: [...CLIENT_OPTIMIZE_DEPS, "react-dom/server"] } },
156
94
  environments: { [WORKER_ENVIRONMENT_NAME]: { optimizeDeps: {
@@ -159,8 +97,7 @@ function pagesPlugin(pagesDir, options) {
159
97
  include: WORKER_OPTIMIZE_DEPS
160
98
  } } }
161
99
  };
162
- await runRescan();
163
- if (!pageScan?.pages.some((page) => !page.island)) return;
100
+ if (!(await scan())?.pages.some((page) => !page.island)) return;
164
101
  return {
165
102
  resolve: { dedupe: REACT_DEDUPE },
166
103
  build: {
@@ -169,7 +106,35 @@ function pagesPlugin(pagesDir, options) {
169
106
  }
170
107
  };
171
108
  }
172
- };
109
+ });
110
+ }
111
+ function clientOptimizeEntries(scan, pagesDir, root) {
112
+ if (!scan) return [];
113
+ const browserFiles = /* @__PURE__ */ new Set();
114
+ const islandFiles = /* @__PURE__ */ new Set();
115
+ for (const page of scan.pages) {
116
+ const layouts = resolveEffectiveLayoutChain(page, scan.layouts, scan.namedLayouts);
117
+ if (page.island) {
118
+ islandFiles.add(resolve(pagesDir, page.componentPath));
119
+ for (const layout of layouts) if (layout.island) islandFiles.add(resolve(pagesDir, layout.filePath));
120
+ } else {
121
+ browserFiles.add(resolve(pagesDir, page.componentPath));
122
+ for (const layout of layouts) browserFiles.add(resolve(pagesDir, layout.filePath));
123
+ }
124
+ }
125
+ for (const file of islandFiles) {
126
+ if (!/\.island\.(tsx|jsx)$/.test(file)) continue;
127
+ for (const { specifier } of findIslandImports(readFileSync(file, "utf8"), file)) {
128
+ const component = resolveSpecifier(specifier, dirname(file), [
129
+ ".tsx",
130
+ ".jsx",
131
+ ".ts",
132
+ ".js"
133
+ ]);
134
+ if (component) browserFiles.add(component);
135
+ }
136
+ }
137
+ return [...browserFiles].map((file) => relative(root, file).replace(/[()[\]{}*?!|]/g, "\\$&"));
173
138
  }
174
139
  function generateRendererModule(scan, pagesDir, isDev) {
175
140
  const preambleScript = isDev ? REACT_REFRESH_PREAMBLE : "";
@@ -213,8 +178,7 @@ function generateClientEntry(scan, pagesDir, viewTransitions, prefetch, staticPa
213
178
  export { router };
214
179
  `;
215
180
  }
216
- //#endregion
217
- //#region src/plugin-islands.ts
181
+ // src/plugin-islands.ts
218
182
  const COMPONENT_EXTENSIONS = [
219
183
  ".tsx",
220
184
  ".jsx",
@@ -465,8 +429,7 @@ initIslands(islands, hydrateIsland);
465
429
  `);
466
430
  return lines.join("\n");
467
431
  }
468
- //#endregion
469
- //#region src/plugin.ts
432
+ // src/plugin.ts
470
433
  function voidReact(options) {
471
434
  const root = process.cwd();
472
435
  const pagesDir = resolvePagesDir(root);
@@ -480,5 +443,4 @@ function voidReact(options) {
480
443
  ...islandsPlugin(pagesDir, { root })
481
444
  ];
482
445
  }
483
- //#endregion
484
446
  export { voidReact };
@@ -2,7 +2,6 @@ import { ComponentType } from "react";
2
2
  import { VoidRouter } from "void/pages-client";
3
3
  import { CacheForResult } from "void/pages-prefetch";
4
4
  import { PageObject } from "void/pages-protocol";
5
- //#region src/runtime/pages-client.d.ts
6
5
  type ComponentModule = {
7
6
  default: ComponentType<any>;
8
7
  };
@@ -29,5 +28,4 @@ export type ReactPagesClient = Readonly<{
29
28
  router: VoidRouter;
30
29
  }>;
31
30
  export declare function startReactPages(config: ReactPagesConfig): Promise<ReactPagesClient>;
32
- //#endregion
33
31
  export {};
@@ -1,10 +1,10 @@
1
- import { a as RouterContext, i as ParamsContext, o as SharedContext, r as NavigationContext, t as ErrorsContext } from "../context-D2ctU4yT.mjs";
2
- import { n as setActionRouter } from "../action-4EvQppia.mjs";
3
- import { n as DEFERRED_SHELL_END_ID } from "../deferred-shell-DOuP2YAo.mjs";
1
+ import { a as RouterContext, i as ParamsContext, o as SharedContext, r as NavigationContext, t as ErrorsContext } from "../context-BQpawTZc.mjs";
2
+ import { n as setActionRouter } from "../action-CyCVdW12.mjs";
3
+ import { n as DEFERRED_SHELL_END_ID } from "../deferred-shell-D5T32zXz.mjs";
4
4
  import React, { Suspense, startTransition, use, useCallback, useEffect, useInsertionEffect, useLayoutEffect, useMemo, useReducer, useState, useTransition } from "react";
5
- import { createPageNavigationEngine, createRouterFacade, idleNavigationState, isAbortError, isCallSiteActionError, refreshPage } from "void/pages-client";
5
+ import { createPageNavigationEngine, createRouterFacade, idleNavigationState, isAbortError, isCallSiteActionError, refreshPage, toUrlString } from "void/pages-client";
6
6
  import { createRoot, hydrateRoot } from "react-dom/client";
7
- //#region src/runtime/pages-client.ts
7
+ // src/runtime/pages-client.ts
8
8
  const deferredState = /* @__PURE__ */ new WeakMap();
9
9
  const viewTransitionWaiters = /* @__PURE__ */ new Map();
10
10
  const pendingVisitSettlers = /* @__PURE__ */ new Map();
@@ -51,8 +51,9 @@ function currentUrl() {
51
51
  return window.location.pathname + window.location.search + window.location.hash;
52
52
  }
53
53
  function normalizeUrl(value) {
54
- if (typeof value === "string" && value.startsWith("#")) return value;
55
- const url = value instanceof URL ? value : new URL(value, window.location.origin);
54
+ const urlString = toUrlString(value);
55
+ if (urlString.startsWith("#")) return urlString;
56
+ const url = new URL(urlString, window.location.origin);
56
57
  return url.origin === window.location.origin ? url.pathname + url.search + url.hash : url.href;
57
58
  }
58
59
  /** Page data holds a decoded path. Compare it with browser URLs in their encoded form. */
@@ -182,12 +183,11 @@ function RouteResource({ resource, onCommit }) {
182
183
  resource,
183
184
  result
184
185
  ]);
185
- if (!result) return null;
186
- const pageData = result.pageData;
187
- const PageComponent = result.component;
188
- const routeParams = pageData.params ?? EMPTY_PARAMS;
186
+ const pageData = result?.pageData;
187
+ const pageDataUrl = pageData?.url ?? "";
188
+ const routeParams = pageData?.params ?? EMPTY_PARAMS;
189
189
  const routeRouter = useMemo(() => {
190
- const pageUrl = encodedPageUrl(pageData.url);
190
+ const pageUrl = encodedPageUrl(pageDataUrl);
191
191
  return createRouterFacade(outerRouter, () => {
192
192
  const outerUrl = outerRouter.url;
193
193
  const outerPath = outerUrl.pathname + outerUrl.search;
@@ -198,11 +198,13 @@ function RouteResource({ resource, onCommit }) {
198
198
  }, () => routeParams);
199
199
  }, [
200
200
  outerRouter,
201
- pageData.url,
201
+ pageDataUrl,
202
202
  resource.fetchUrl,
203
203
  resource.hash,
204
204
  routeParams
205
205
  ]);
206
+ if (!result || !pageData) return null;
207
+ const PageComponent = result.component;
206
208
  let element = React.createElement(PageComponent, pageData.props);
207
209
  for (let i = result.layouts.length - 1; i >= 0; i--) {
208
210
  const Layout = result.layouts[i];
@@ -349,7 +351,7 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
349
351
  pendingVisitSettlers.delete(prepared.id);
350
352
  if (activePendingNavigationId === prepared.id) activePendingNavigationId = null;
351
353
  dispatch({
352
- type: NavigationFallback ? "abandon" : "finish",
354
+ type: "abandon",
353
355
  resource: prepared,
354
356
  location: currentUrl()
355
357
  });
@@ -512,5 +514,4 @@ async function startReactPages(config) {
512
514
  engine.start();
513
515
  return { router };
514
516
  }
515
- //#endregion
516
517
  export { startReactPages };
@@ -1,6 +1,5 @@
1
1
  import { ComponentType } from "react";
2
2
  import { PageObject } from "void/pages-protocol";
3
- //#region src/runtime/pages-server.d.ts
4
3
  type ComponentModule = {
5
4
  default: ComponentType<any>;
6
5
  };
@@ -16,5 +15,4 @@ export type ReactPagesServerConfig = Readonly<{
16
15
  preambleScript?: string;
17
16
  }>;
18
17
  export declare function renderReactPage({ pageObj, assetTags, components, layoutTree, preambleScript }: ReactPagesServerConfig): Promise<ReadableStream<Uint8Array>>;
19
- //#endregion
20
18
  export {};
@@ -1,11 +1,11 @@
1
- import { a as RouterContext, i as ParamsContext, o as SharedContext, r as NavigationContext, t as ErrorsContext } from "../context-D2ctU4yT.mjs";
2
- import { n as DEFERRED_SHELL_END_ID, r as DEFERRED_SHELL_END_MARKER, t as DEFERRED_SHELL_CANCEL_REASON } from "../deferred-shell-DOuP2YAo.mjs";
1
+ import { a as RouterContext, i as ParamsContext, o as SharedContext, r as NavigationContext, t as ErrorsContext } from "../context-BQpawTZc.mjs";
2
+ import { n as DEFERRED_SHELL_END_ID, r as DEFERRED_SHELL_END_MARKER, t as DEFERRED_SHELL_CANCEL_REASON } from "../deferred-shell-D5T32zXz.mjs";
3
3
  import React from "react";
4
4
  import { createSsrRouter, idleNavigationState } from "void/pages-client";
5
5
  import { renderToReadableStream } from "react-dom/server";
6
6
  import { renderBodyAttrs, renderHeadToString, renderHtmlAttrs } from "void/pages-head";
7
7
  import { serializePageData } from "void/pages-server";
8
- //#region src/runtime/page-streams.ts
8
+ // src/runtime/page-streams.ts
9
9
  function concatStreams(streams) {
10
10
  let index = 0;
11
11
  let reader;
@@ -94,8 +94,7 @@ function readDeferredShell(stream) {
94
94
  }
95
95
  }, { highWaterMark: 0 });
96
96
  }
97
- //#endregion
98
- //#region src/runtime/pages-server.ts
97
+ // src/runtime/pages-server.ts
99
98
  function createPendingDeferred() {
100
99
  return new Promise(() => {});
101
100
  }
@@ -155,5 +154,4 @@ async function renderReactPage({ pageObj, assetTags, components, layoutTree, pre
155
154
  </html>`)
156
155
  ]);
157
156
  }
158
- //#endregion
159
157
  export { renderReactPage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@void/react",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "React pages, server rendering, forms, and navigation for Void.",
5
5
  "homepage": "https://void.cloud",
6
6
  "bugs": {
@@ -41,21 +41,22 @@
41
41
  "@vitejs/plugin-react": "^6.1.2"
42
42
  },
43
43
  "devDependencies": {
44
- "@types/node": "^26.6.5",
44
+ "@types/node": "^26.6.6",
45
45
  "@types/react": "^19.3.0",
46
46
  "@types/react-dom": "^19.3.0",
47
+ "happy-dom": "^21.0.4",
47
48
  "pathslash": "^0.1.0",
48
49
  "react": "^19.3.1",
49
50
  "react-dom": "^19.3.1",
50
- "vite": "npm:@voidzero-dev/vite-plus-core@1.3.0",
51
- "vite-plus": "1.3.0",
52
- "void": "1.3.0"
51
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
52
+ "vite-plus": "1.4.0",
53
+ "void": "1.5.0"
53
54
  },
54
55
  "peerDependencies": {
55
56
  "react": "^19.0.0",
56
57
  "react-dom": "^19.0.0",
57
58
  "vite": "^8.0.0",
58
- "void": "1.3.0"
59
+ "void": "1.5.0"
59
60
  },
60
61
  "scripts": {
61
62
  "build": "vp pack",