@void/react 1.2.1 → 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;
@@ -249,6 +249,7 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
249
249
  prerenderedUrl = null;
250
250
  React.startTransition(() => setHydrated(true));
251
251
  }
252
+ return true;
252
253
  },
253
254
  dispose() {}
254
255
  }), [engine, initialPageData]);
@@ -263,7 +264,7 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
263
264
  const finishNavigation = useCallback((resource, result) => {
264
265
  if (committedResources.has(resource.id)) return;
265
266
  committedResources.add(resource.id);
266
- resource.commit().then(() => {
267
+ resource.commit().then((committed) => {
267
268
  const resolveViewTransition = viewTransitionWaiters.get(resource.id);
268
269
  if (resolveViewTransition) {
269
270
  viewTransitionWaiters.delete(resource.id);
@@ -272,8 +273,11 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
272
273
  const visitSettler = pendingVisitSettlers.get(resource.id);
273
274
  if (visitSettler) {
274
275
  pendingVisitSettlers.delete(resource.id);
275
- 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"));
276
279
  }
280
+ if (!committed) return;
277
281
  if (activePendingNavigationId === resource.id) activePendingNavigationId = null;
278
282
  dispatch({
279
283
  type: "finish",
@@ -281,11 +285,22 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
281
285
  location: currentUrl()
282
286
  });
283
287
  }).catch((error) => {
288
+ const resolveViewTransition = viewTransitionWaiters.get(resource.id);
289
+ if (resolveViewTransition) {
290
+ viewTransitionWaiters.delete(resource.id);
291
+ resolveViewTransition();
292
+ }
284
293
  const visitSettler = pendingVisitSettlers.get(resource.id);
285
294
  if (visitSettler) {
286
295
  pendingVisitSettlers.delete(resource.id);
287
296
  visitSettler.reject(error);
288
- }
297
+ } else reportError(error);
298
+ if (activePendingNavigationId === resource.id) activePendingNavigationId = null;
299
+ dispatch({
300
+ type: "fail",
301
+ resource,
302
+ location: currentUrl()
303
+ });
289
304
  });
290
305
  }, []);
291
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.1",
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.1"
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.1"
58
+ "void": "1.3.0"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "vp pack",