@void/react 0.25.0 → 0.26.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/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # @void/react
2
2
 
3
- Build server-rendered Void pages with React, server loaders, and typed forms. Requires React 19.
3
+ React adapter for [Void](https://void.cloud). Build server-rendered pages with typed server loaders, forms, layouts, and interactive islands.
4
+
5
+ Requires React 19. Navigation uses transitions and Suspense to keep the UI responsive while the next page loads.
4
6
 
5
7
  ## Setup
6
8
 
7
- Install alongside `void`:
9
+ For a new app, run `npx void init` and choose React. To add the adapter to an existing Void app:
8
10
 
9
11
  ```sh
10
12
  npm install @void/react
@@ -13,15 +15,17 @@ npm install @void/react
13
15
  Add the adapter to your Vite config. It includes the framework's Vite plugin:
14
16
 
15
17
  ```ts
18
+ import { voidReact } from '@void/react/plugin';
16
19
  import { defineConfig } from 'vite';
17
20
  import { voidPlugin } from 'void';
18
- import { voidReact } from '@void/react/plugin';
19
21
 
20
22
  export default defineConfig({
21
23
  plugins: [voidPlugin(), voidReact()],
22
24
  });
23
25
  ```
24
26
 
25
- Follow [Pages Routing](https://void.cloud/guide/pages-routing/overview) to add pages, then use [loaders](https://void.cloud/guide/pages-routing/loaders), [forms](https://void.cloud/guide/pages-routing/actions-and-forms), and [layouts](https://void.cloud/guide/pages-routing/layouts).
27
+ ## Documentation
28
+
29
+ Follow [Pages Routing](https://void.cloud/guide/pages-routing/overview) to add pages, then use [loaders](https://void.cloud/guide/pages-routing/loaders), [forms](https://void.cloud/guide/pages-routing/actions-and-forms), and [layouts](https://void.cloud/guide/pages-routing/layouts). See the [adapter API reference](https://void.cloud/reference/api/pages) for plugin options and runtime helpers.
26
30
 
27
- See the [adapter API reference](https://void.cloud/reference/api/pages) for plugin options and runtime helpers, and [TESTING.md](../../TESTING.md) for repository verification.
31
+ Read the [Void documentation](https://void.cloud) online or use the Markdown guides and references in `node_modules/void/docs/`, included with the `void` package.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as useParams, c as Form, i as useShared, n as useNavigation, o as useRouter, r as useForm, s as Link, t as useIslandForm } from "./runtime-D_Wv52MZ.mjs";
1
+ import { a as useParams, c as Form, i as useShared, n as useNavigation, o as useRouter, r as useForm, s as Link, t as useIslandForm } from "./runtime--GesOVur.mjs";
2
2
  import { a as SharedContext, i as RouterContext, n as NavigationContext, r as ParamsContext, t as ErrorsContext } from "./context-B0GL7p5m.mjs";
3
3
  import { n as setActionRouter, t as action } from "./action-4EvQppia.mjs";
4
4
  export { ErrorsContext, Form, Link, NavigationContext, ParamsContext, RouterContext, SharedContext, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
package/dist/plugin.d.mts CHANGED
@@ -1,10 +1,12 @@
1
1
  import react from "@vitejs/plugin-react";
2
2
  import { Plugin } from "vite";
3
- //#region src/plugin.d.ts
3
+ //#region src/plugin-pages.d.ts
4
4
  type PrefetchConfig = {
5
5
  hoverDelay?: number;
6
6
  cacheFor?: number | string | [string, string];
7
7
  };
8
+ //#endregion
9
+ //#region src/plugin.d.ts
8
10
  export type VoidReactOptions = Readonly<{
9
11
  react?: Parameters<typeof react>[0];
10
12
  viewTransitions?: boolean;
package/dist/plugin.mjs CHANGED
@@ -58,9 +58,23 @@ function islandsPlugin(pagesDir, options) {
58
58
  const islandCssFiles = /* @__PURE__ */ new Set();
59
59
  let viteRoot = "";
60
60
  let resolvedConfig = null;
61
- function computePagesNeedingClientJSLocal() {
62
- if (!pageScan) return /* @__PURE__ */ new Set();
63
- return computePagesNeedingClientJS(pageScan, pagesDir, resolvedConfig);
61
+ /** Resolve an island import and record it in the client manifest. */
62
+ function registerIsland(spec, importerDir, hydrate) {
63
+ const absPath = resolveSpecifier(spec, importerDir, COMPONENT_EXTENSIONS);
64
+ if (!absPath) return {
65
+ absPath,
66
+ islandId: spec
67
+ };
68
+ const islandId = normalizePath(relative(root, absPath)).replace(/\.[^.]+$/, "");
69
+ islandManifest.set(islandId, {
70
+ componentPath: absPath,
71
+ islandId,
72
+ hydrate
73
+ });
74
+ return {
75
+ absPath,
76
+ islandId
77
+ };
64
78
  }
65
79
  return [{
66
80
  name: "void-react:islands",
@@ -96,19 +110,7 @@ function islandsPlugin(pagesDir, options) {
96
110
  const filePath = resolve(pagesDir, entry.filePath);
97
111
  const code = readFileSync(filePath, "utf-8");
98
112
  const importerDir = dirname(filePath);
99
- for (const islandImport of findIslandImports(code, filePath)) {
100
- const spec = islandImport.specifier;
101
- const strategy = islandImport.hydrate;
102
- const absPath = resolveSpecifier(spec, importerDir, COMPONENT_EXTENSIONS);
103
- if (absPath) {
104
- const islandId = normalizePath(relative(root, absPath)).replace(/\.[^.]+$/, "");
105
- islandManifest.set(islandId, {
106
- componentPath: absPath,
107
- islandId,
108
- hydrate: strategy
109
- });
110
- }
111
- }
113
+ for (const islandImport of findIslandImports(code, filePath)) registerIsland(islandImport.specifier, importerDir, islandImport.hydrate);
112
114
  }
113
115
  for (const page of pageScan.pages) {
114
116
  if (!page.island) continue;
@@ -139,7 +141,7 @@ function islandsPlugin(pagesDir, options) {
139
141
  if (!pageScan) return "export function renderIslandPage() { throw new Error('No pages found'); }";
140
142
  const mdPlugin = resolvedConfig?.plugins?.find((plugin) => plugin.name === "void-md");
141
143
  const mdScriptIds = new Set(mdPlugin?.api?.getClientScripts?.()?.keys() ?? []);
142
- const needsJS = computePagesNeedingClientJSLocal();
144
+ const needsJS = computePagesNeedingClientJS(pageScan, pagesDir, resolvedConfig);
143
145
  return generateIslandRenderer(pageScan, pagesDir, isDev, mdScriptIds, needsJS);
144
146
  }
145
147
  if (id === "\0" + ISLANDS_CLIENT_ENTRY_ID) {
@@ -166,16 +168,7 @@ function islandsPlugin(pagesDir, options) {
166
168
  const strategy = islandImport.hydrate;
167
169
  const spec = islandImport.specifier;
168
170
  const rawName = `_raw_island_${idx++}`;
169
- const absPath = resolveSpecifier(spec, importerDir, COMPONENT_EXTENSIONS);
170
- if (absPath) {
171
- const islandId = normalizePath(relative(root, absPath)).replace(/\.[^.]+$/, "");
172
- islandManifest.set(islandId, {
173
- componentPath: absPath,
174
- islandId,
175
- hydrate: strategy
176
- });
177
- }
178
- const islandId = absPath ? normalizePath(relative(root, absPath)).replace(/\.[^.]+$/, "") : spec;
171
+ const { islandId } = registerIsland(spec, importerDir, strategy);
179
172
  const replacement = `import ${rawName} from ${specRaw}`;
180
173
  edits.push({
181
174
  start: islandImport.start,
@@ -439,7 +432,6 @@ import { renderReactPage } from "@void/react/pages-server";
439
432
  ${generateComponentManifest(scan, pagesDir)}
440
433
 
441
434
  const PREAMBLE_SCRIPT = ${JSON.stringify(preambleScript)};
442
- export const renderPreambleScript = PREAMBLE_SCRIPT;
443
435
 
444
436
  export async function renderPage(pageObj, assetTags) {
445
437
  return renderReactPage({
@@ -1,4 +1,4 @@
1
- import { a as useParams, c as Form, i as useShared, n as useNavigation, o as useRouter, r as useForm, s as Link, t as useIslandForm } from "../runtime-D_Wv52MZ.mjs";
1
+ import { a as useParams, c as Form, i as useShared, n as useNavigation, o as useRouter, r as useForm, s as Link, t as useIslandForm } from "../runtime--GesOVur.mjs";
2
2
  import { a as SharedContext, i as RouterContext, n as NavigationContext, r as ParamsContext, t as ErrorsContext } from "../context-B0GL7p5m.mjs";
3
3
  import { n as setActionRouter, t as action } from "../action-4EvQppia.mjs";
4
4
  export { ErrorsContext, Form, Link, NavigationContext, ParamsContext, RouterContext, SharedContext, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
@@ -1,7 +1,7 @@
1
1
  import { a as SharedContext, i as RouterContext, n as NavigationContext, r as ParamsContext } from "./context-B0GL7p5m.mjs";
2
2
  import "./action-4EvQppia.mjs";
3
3
  import { useActionState, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
4
- import { VoidActionError, categorizeActionError, formErrorMessage, formRetryMessage, formSubmissionBlocked, isAbortError, isEqualFormValue, scheduleFormRetry, ssrProxy, submitAction } from "void/pages-client";
4
+ import { VoidActionError, categorizeActionError, formErrorMessage, formRetryMessage, formSubmissionBlocked, getValidationErrors, interpolateRouteParams, isAbortError, isEqualFormValue, isModifiedEvent, mergeDataIntoHref, scheduleFormRetry, ssrProxy, submitAction } from "void/pages-client";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  //#region src/runtime/form.tsx
7
7
  function Form({ form, method = "post", renderError, children, ...props }) {
@@ -26,45 +26,9 @@ function Form({ form, method = "post", renderError, children, ...props }) {
26
26
  }
27
27
  //#endregion
28
28
  //#region src/runtime/link.tsx
29
- function appendQueryValue(params, key, value) {
30
- if (value === null || value === void 0) return false;
31
- if (value instanceof Date) {
32
- params.append(key, value.toISOString());
33
- return true;
34
- }
35
- if (Array.isArray(value)) {
36
- let appended = false;
37
- for (const item of value) appended = appendQueryValue(params, key, item) || appended;
38
- return appended;
39
- }
40
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
41
- params.append(key, String(value));
42
- return true;
43
- }
44
- throw new Error(`Link: GET data only supports primitive values and arrays. Remove nested data for '${key}'.`);
45
- }
46
- function mergeDataIntoHref(href, data) {
47
- if (!data) return href;
48
- const hashIndex = href.indexOf("#");
49
- const beforeHash = hashIndex === -1 ? href : href.slice(0, hashIndex);
50
- const hash = hashIndex === -1 ? "" : href.slice(hashIndex);
51
- const searchIndex = beforeHash.indexOf("?");
52
- const path = searchIndex === -1 ? beforeHash : beforeHash.slice(0, searchIndex);
53
- const search = searchIndex === -1 ? "" : beforeHash.slice(searchIndex + 1);
54
- const params = new URLSearchParams(search);
55
- let appended = false;
56
- for (const [key, value] of Object.entries(data)) appended = appendQueryValue(params, key, value) || appended;
57
- if (!appended) return href;
58
- const query = params.toString();
59
- return `${path}${query ? `?${query}` : ""}${hash}`;
60
- }
61
29
  function hasPrefetch(prefetch) {
62
30
  return prefetch !== false;
63
31
  }
64
- function isModifiedEvent(e) {
65
- const target = e.currentTarget.getAttribute("target");
66
- return target !== null && target !== "_self" || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || e.button !== 0;
67
- }
68
32
  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 }) {
69
33
  const router = useContext(RouterContext);
70
34
  const elementRef = useRef(null);
@@ -270,21 +234,7 @@ function useShared() {
270
234
  }
271
235
  //#endregion
272
236
  //#region src/runtime/use-form.ts
273
- function getValidationErrors$1(error) {
274
- const body = error.body;
275
- if (body && typeof body === "object" && "errors" in body && body.errors && typeof body.errors === "object" && !Array.isArray(body.errors)) return body.errors;
276
- return null;
277
- }
278
237
  function useForm(url, defaults, options) {
279
- let resolvedUrl = url;
280
- let actionQuery = "";
281
- const qIdx = resolvedUrl.indexOf("?");
282
- if (qIdx !== -1) {
283
- actionQuery = resolvedUrl.slice(qIdx);
284
- resolvedUrl = resolvedUrl.slice(0, qIdx);
285
- }
286
- if (options?.params) for (const [key, value] of Object.entries(options.params)) resolvedUrl = resolvedUrl.replace(`:${key}`, encodeURIComponent(value));
287
- resolvedUrl = resolvedUrl + actionQuery;
288
238
  const router = useContext(RouterContext);
289
239
  const [data, setDataState] = useState(() => ({ ...defaults }));
290
240
  const [defaultsState, setDefaultsState] = useState(() => ({ ...defaults }));
@@ -338,7 +288,7 @@ function useForm(url, defaults, options) {
338
288
  setError(null);
339
289
  let result;
340
290
  try {
341
- result = await submitAction(router, resolvedUrl, {
291
+ result = await submitAction(router, interpolateRouteParams(url, options?.params), {
342
292
  method,
343
293
  data: { ...dataRef.current },
344
294
  _resolveOnShell: true
@@ -348,7 +298,7 @@ function useForm(url, defaults, options) {
348
298
  throw submitError;
349
299
  }
350
300
  if (!result.ok) {
351
- const validationErrors = getValidationErrors$1(result.error);
301
+ const validationErrors = getValidationErrors(result.error.body);
352
302
  if (validationErrors) setErrors(validationErrors);
353
303
  else setError(result.error);
354
304
  return previousSuccessVersion;
@@ -400,10 +350,6 @@ function useNavigation() {
400
350
  }
401
351
  //#endregion
402
352
  //#region src/runtime/use-island-form.ts
403
- function getValidationErrors(body) {
404
- if (body && typeof body === "object" && "errors" in body && body.errors && typeof body.errors === "object" && !Array.isArray(body.errors)) return body.errors;
405
- return null;
406
- }
407
353
  async function readActionErrorBody(response) {
408
354
  const contentType = response.headers.get("content-type") || "";
409
355
  try {
@@ -480,7 +426,7 @@ function useIslandForm(defaults) {
480
426
  setDefaultsState(nextDefaults);
481
427
  successTimeoutRef.current = setTimeout(() => setRecentlySuccessful(false), 2e3);
482
428
  window.location.reload();
483
- } else if (!response.ok) {
429
+ } else {
484
430
  const body = await readActionErrorBody(response);
485
431
  const actionError = new VoidActionError({
486
432
  body,
package/package.json CHANGED
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "name": "@void/react",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
+ "description": "React pages, server rendering, forms, and navigation for Void.",
5
+ "homepage": "https://void.cloud",
6
+ "bugs": {
7
+ "url": "https://github.com/voidzero-dev/void/issues"
8
+ },
9
+ "license": "MIT",
4
10
  "repository": {
5
11
  "type": "git",
6
12
  "url": "git+https://github.com/voidzero-dev/void.git",
@@ -32,7 +38,7 @@
32
38
  "access": "public"
33
39
  },
34
40
  "dependencies": {
35
- "@vitejs/plugin-react": "^6.1.1"
41
+ "@vitejs/plugin-react": "^6.1.2"
36
42
  },
37
43
  "devDependencies": {
38
44
  "@types/node": "^26.6.4",
@@ -41,15 +47,15 @@
41
47
  "pathslash": "^0.1.0",
42
48
  "react": "^19.3.0",
43
49
  "react-dom": "^19.3.0",
44
- "vite": "npm:@voidzero-dev/vite-plus-core@1.0.0",
45
- "vite-plus": "1.0.0",
46
- "void": "0.25.0"
50
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
51
+ "vite-plus": "1.1.0",
52
+ "void": "0.26.1"
47
53
  },
48
54
  "peerDependencies": {
49
55
  "react": "^19.0.0",
50
56
  "react-dom": "^19.0.0",
51
57
  "vite": "^8.0.0",
52
- "void": "0.25.0"
58
+ "void": "0.26.1"
53
59
  },
54
60
  "scripts": {
55
61
  "build": "vp pack",