@void/react 1.2.0 → 1.3.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AnchorHTMLAttributes, ButtonHTMLAttributes, Context, FormHTMLAttributes, JSX, ReactElement, ReactNode } from "react";
2
- import { ActionClient, FormMethod, FormModel, NavigationState, VoidActionError, VoidRouter } from "void/pages-client";
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
5
  //#region src/runtime/form.d.ts
@@ -54,7 +54,9 @@ export declare function useForm<U extends ActionUrl & string>(url: U, defaults:
54
54
  put(formData: FormData): void;
55
55
  patch(formData: FormData): void;
56
56
  delete(formData: FormData): void;
57
+ reset(options: FormResetOptions, ...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
57
58
  reset(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
59
+ setError(field: keyof ResolveActionBody<U> & string, message: string): void;
58
60
  clearErrors(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
59
61
  clearError(): void;
60
62
  };
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-D2ctU4yT.mjs";
2
2
  import { n as setActionRouter, t as action } from "./action-4EvQppia.mjs";
3
3
  import { useActionState, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
4
- import { VoidActionError, categorizeActionError, formErrorMessage, formRetryMessage, formSubmissionBlocked, getValidationErrors, interpolateRouteParams, isAbortError, isEqualFormValue, isModifiedEvent, mergeDataIntoHref, scheduleFormRetry, ssrProxy, submitAction } from "void/pages-client";
4
+ import { VoidActionError, categorizeActionError, cloneFormValue, formErrorMessage, formRetryMessage, formSubmissionBlocked, getValidationErrors, interpolateRouteParams, isAbortError, isEqualFormValue, isModifiedEvent, mergeDataIntoHref, parseFormResetArgs, 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 }) {
@@ -231,13 +231,14 @@ function useShared() {
231
231
  //#region src/runtime/use-form.ts
232
232
  function useForm(url, defaults, options) {
233
233
  const router = useContext(RouterContext);
234
- const [data, setDataState] = useState(() => ({ ...defaults }));
235
- const [defaultsState, setDefaultsState] = useState(() => ({ ...defaults }));
234
+ const [data, setDataState] = useState(() => cloneFormValue(defaults));
235
+ const [defaultsState, setDefaultsState] = useState(() => cloneFormValue(defaults));
236
236
  const [errors, setErrors] = useState({});
237
- const [error, setError] = useState(null);
237
+ const [error, setActionError] = useState(null);
238
238
  const [wasSuccessful, setWasSuccessful] = useState(false);
239
239
  const [recentlySuccessful, setRecentlySuccessful] = useState(false);
240
240
  const defaultsRef = useRef(defaultsState);
241
+ const initialRef = useRef(defaultsState);
241
242
  const dataRef = useRef(data);
242
243
  const successTimeoutRef = useRef(null);
243
244
  const hasChanges = useMemo(() => {
@@ -249,12 +250,20 @@ function useForm(url, defaults, options) {
249
250
  [field]: value
250
251
  };
251
252
  setDataState(dataRef.current);
253
+ setErrors((prev) => {
254
+ if (!(field in prev)) return prev;
255
+ const next = { ...prev };
256
+ delete next[field];
257
+ return next;
258
+ });
252
259
  }, []);
253
- const reset = useCallback((...fields) => {
254
- if (fields.length === 0) dataRef.current = { ...defaultsRef.current };
260
+ const reset = useCallback((...args) => {
261
+ const { initial, fields } = parseFormResetArgs(args);
262
+ const source = initial ? initialRef.current : defaultsRef.current;
263
+ if (fields.length === 0) dataRef.current = cloneFormValue(source);
255
264
  else {
256
265
  const next = { ...dataRef.current };
257
- for (const field of fields) next[field] = defaultsRef.current[field];
266
+ for (const field of fields) next[field] = cloneFormValue(source[field]);
258
267
  dataRef.current = next;
259
268
  }
260
269
  setDataState(dataRef.current);
@@ -267,8 +276,14 @@ function useForm(url, defaults, options) {
267
276
  return next;
268
277
  });
269
278
  }, []);
279
+ const setError = useCallback((field, message) => {
280
+ setErrors((prev) => ({
281
+ ...prev,
282
+ [field]: message
283
+ }));
284
+ }, []);
270
285
  const clearError = useCallback(() => {
271
- setError(null);
286
+ setActionError(null);
272
287
  }, []);
273
288
  const [, dispatchAction, pending] = useActionState(async (previousSuccessVersion, payload) => {
274
289
  if (!router) throw new Error("useForm(): requires the Void Router.");
@@ -280,7 +295,7 @@ function useForm(url, defaults, options) {
280
295
  successTimeoutRef.current = null;
281
296
  }
282
297
  setErrors({});
283
- setError(null);
298
+ setActionError(null);
284
299
  let result;
285
300
  try {
286
301
  result = await submitAction(router, interpolateRouteParams(url, options?.params), {
@@ -295,10 +310,14 @@ function useForm(url, defaults, options) {
295
310
  if (!result.ok) {
296
311
  const validationErrors = getValidationErrors(result.error.body);
297
312
  if (validationErrors) setErrors(validationErrors);
298
- else setError(result.error);
313
+ else setActionError(result.error);
299
314
  return previousSuccessVersion;
300
315
  }
301
- const nextDefaults = { ...dataRef.current };
316
+ if (options?.resetOnSuccess) {
317
+ dataRef.current = cloneFormValue(initialRef.current);
318
+ setDataState(dataRef.current);
319
+ }
320
+ const nextDefaults = cloneFormValue(dataRef.current);
302
321
  defaultsRef.current = nextDefaults;
303
322
  setDefaultsState(nextDefaults);
304
323
  setWasSuccessful(true);
@@ -334,6 +353,7 @@ function useForm(url, defaults, options) {
334
353
  patch: useMemo(() => dispatchMethodAction("PATCH"), [dispatchMethodAction]),
335
354
  delete: useMemo(() => dispatchMethodAction("DELETE"), [dispatchMethodAction]),
336
355
  reset,
356
+ setError,
337
357
  clearErrors,
338
358
  clearError
339
359
  };
package/dist/plugin.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import react from "@vitejs/plugin-react";
2
2
  import { generateComponentManifest, resolveEffectiveLayoutChain, resolvePagesDir, resolveStaticPageDataBuildOptions, scanPages } from "void/pages";
3
- import { existsSync, readFileSync, readdirSync } from "node:fs";
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";
@@ -68,21 +68,6 @@ const REACT_REFRESH_PREAMBLE = `
68
68
  window.$RefreshSig$ = () => (type) => type
69
69
  window.__vite_plugin_react_preamble_installed__ = true
70
70
  <\/script>`;
71
- /** Check if the pages directory has any non-island page files (.tsx/.jsx without .island.). */
72
- function hasNonIslandPages(dir) {
73
- try {
74
- for (const entry of readdirSync(dir, {
75
- withFileTypes: true,
76
- recursive: true
77
- })) {
78
- if (!entry.isFile()) continue;
79
- const name = entry.name;
80
- if (name.startsWith("_") || name.startsWith("layout.")) continue;
81
- if (/\.(tsx|jsx)$/.test(name) && !name.includes(".island.")) return true;
82
- }
83
- } catch {}
84
- return false;
85
- }
86
71
  function pagesPlugin(pagesDir, options) {
87
72
  let pageScan = null;
88
73
  let isDev = false;
@@ -92,7 +77,10 @@ function pagesPlugin(pagesDir, options) {
92
77
  const pagesOptimizeEntries = `${relative(root, pagesDir)}/**/*.{js,jsx,ts,tsx}`;
93
78
  let pendingRescan = null;
94
79
  const rescanPages = async () => {
95
- pageScan = existsSync(pagesDir) ? await scanPages(root, { pagesDir }) : null;
80
+ pageScan = existsSync(pagesDir) ? await scanPages(root, {
81
+ pagesDir,
82
+ environment: isDev ? "dev" : "prod"
83
+ }) : null;
96
84
  };
97
85
  const runRescan = () => {
98
86
  pendingRescan ??= rescanPages().finally(() => {
@@ -155,25 +143,24 @@ function pagesPlugin(pagesDir, options) {
155
143
  configResolved(config) {
156
144
  viteConfig = config;
157
145
  },
158
- config(_, env) {
146
+ async config(_, env) {
147
+ isDev = env.command === "serve";
159
148
  if (!existsSync(pagesDir)) return;
160
- if (env.command === "serve") {
161
- isDev = true;
162
- return {
163
- resolve: { dedupe: REACT_DEDUPE },
164
- optimizeDeps: {
165
- exclude: ["@void/react"],
166
- include: CLIENT_OPTIMIZE_DEPS
167
- },
168
- ssr: { optimizeDeps: { include: [...CLIENT_OPTIMIZE_DEPS, "react-dom/server"] } },
169
- environments: { [WORKER_ENVIRONMENT_NAME]: { optimizeDeps: {
170
- exclude: ["@void/react"],
171
- entries: [pagesOptimizeEntries],
172
- include: WORKER_OPTIMIZE_DEPS
173
- } } }
174
- };
175
- }
176
- if (!hasNonIslandPages(pagesDir)) return;
149
+ if (isDev) return {
150
+ resolve: { dedupe: REACT_DEDUPE },
151
+ optimizeDeps: {
152
+ exclude: ["@void/react"],
153
+ include: CLIENT_OPTIMIZE_DEPS
154
+ },
155
+ ssr: { optimizeDeps: { include: [...CLIENT_OPTIMIZE_DEPS, "react-dom/server"] } },
156
+ environments: { [WORKER_ENVIRONMENT_NAME]: { optimizeDeps: {
157
+ exclude: ["@void/react"],
158
+ entries: [pagesOptimizeEntries],
159
+ include: WORKER_OPTIMIZE_DEPS
160
+ } } }
161
+ };
162
+ await runRescan();
163
+ if (!pageScan?.pages.some((page) => !page.island)) return;
177
164
  return {
178
165
  resolve: { dedupe: REACT_DEDUPE },
179
166
  build: {
@@ -272,7 +259,10 @@ function islandsPlugin(pagesDir, options) {
272
259
  },
273
260
  async buildStart() {
274
261
  if (existsSync(pagesDir)) {
275
- pageScan = await scanPages(root, { pagesDir });
262
+ pageScan = await scanPages(root, {
263
+ pagesDir,
264
+ environment: isDev ? "dev" : "prod"
265
+ });
276
266
  const filesToScan = [
277
267
  ...pageScan.pages.map((page) => ({
278
268
  filePath: page.componentPath,
@@ -12,7 +12,8 @@ export type ReactPagesConfig = Readonly<{
12
12
  routeMeta: Record<string, {
13
13
  island?: boolean;
14
14
  }>;
15
- matchRoute(url: string): string | null;
15
+ /** Page id for a decoded pathname. */
16
+ matchRoute(path: string): string | null;
16
17
  staticPageData: boolean;
17
18
  staticPageDataFastPath: boolean;
18
19
  viewTransitions: boolean;
@@ -2,7 +2,7 @@ import { a as RouterContext, i as ParamsContext, o as SharedContext, r as Naviga
2
2
  import { n as setActionRouter } from "../action-4EvQppia.mjs";
3
3
  import { n as DEFERRED_SHELL_END_ID } from "../deferred-shell-DOuP2YAo.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 } from "void/pages-client";
5
+ import { createPageNavigationEngine, createRouterFacade, idleNavigationState, isAbortError, isCallSiteActionError, refreshPage } from "void/pages-client";
6
6
  import { createRoot, hydrateRoot } from "react-dom/client";
7
7
  //#region src/runtime/pages-client.ts
8
8
  const deferredState = /* @__PURE__ */ new WeakMap();
@@ -158,11 +158,7 @@ function createRouter(engine) {
158
158
  return scheduleVisit(normalizeUrl(url), options);
159
159
  },
160
160
  refresh(options = {}) {
161
- return router.visit(window.location.pathname + window.location.search, {
162
- preserveState: true,
163
- preserveScroll: true,
164
- ...options
165
- });
161
+ return refreshPage(router, options);
166
162
  },
167
163
  prefetch(url, options = {}) {
168
164
  return engine.prefetch(normalizeUrl(url), options);
@@ -253,6 +249,7 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
253
249
  prerenderedUrl = null;
254
250
  React.startTransition(() => setHydrated(true));
255
251
  }
252
+ return true;
256
253
  },
257
254
  dispose() {}
258
255
  }), [engine, initialPageData]);
@@ -267,7 +264,7 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
267
264
  const finishNavigation = useCallback((resource, result) => {
268
265
  if (committedResources.has(resource.id)) return;
269
266
  committedResources.add(resource.id);
270
- resource.commit().then(() => {
267
+ resource.commit().then((committed) => {
271
268
  const resolveViewTransition = viewTransitionWaiters.get(resource.id);
272
269
  if (resolveViewTransition) {
273
270
  viewTransitionWaiters.delete(resource.id);
@@ -276,8 +273,11 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
276
273
  const visitSettler = pendingVisitSettlers.get(resource.id);
277
274
  if (visitSettler) {
278
275
  pendingVisitSettlers.delete(resource.id);
279
- visitSettler.resolve(result.pageData);
276
+ if (committed) visitSettler.resolve(result.pageData);
277
+ else if (resource.method.toUpperCase() === "GET") visitSettler.resolve(void 0);
278
+ else visitSettler.reject(new DOMException("Navigation aborted", "AbortError"));
280
279
  }
280
+ if (!committed) return;
281
281
  if (activePendingNavigationId === resource.id) activePendingNavigationId = null;
282
282
  dispatch({
283
283
  type: "finish",
@@ -285,11 +285,22 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
285
285
  location: currentUrl()
286
286
  });
287
287
  }).catch((error) => {
288
+ const resolveViewTransition = viewTransitionWaiters.get(resource.id);
289
+ if (resolveViewTransition) {
290
+ viewTransitionWaiters.delete(resource.id);
291
+ resolveViewTransition();
292
+ }
288
293
  const visitSettler = pendingVisitSettlers.get(resource.id);
289
294
  if (visitSettler) {
290
295
  pendingVisitSettlers.delete(resource.id);
291
296
  visitSettler.reject(error);
292
- }
297
+ } else reportError(error);
298
+ if (activePendingNavigationId === resource.id) activePendingNavigationId = null;
299
+ dispatch({
300
+ type: "fail",
301
+ resource,
302
+ location: currentUrl()
303
+ });
293
304
  });
294
305
  }, []);
295
306
  const schedule = useCallback((url, options = {}) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@void/react",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "React pages, server rendering, forms, and navigation for Void.",
5
5
  "homepage": "https://void.cloud",
6
6
  "bugs": {
@@ -49,13 +49,13 @@
49
49
  "react-dom": "^19.3.1",
50
50
  "vite": "npm:@voidzero-dev/vite-plus-core@1.3.0",
51
51
  "vite-plus": "1.3.0",
52
- "void": "1.2.0"
52
+ "void": "1.3.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "react": "^19.0.0",
56
56
  "react-dom": "^19.0.0",
57
57
  "vite": "^8.0.0",
58
- "void": "1.2.0"
58
+ "void": "1.3.0"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "vp pack",