@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 +3 -1
- package/dist/index.mjs +31 -11
- package/dist/plugin.mjs +26 -36
- package/dist/runtime/pages-client.d.mts +2 -1
- package/dist/runtime/pages-client.mjs +20 -9
- package/package.json +3 -3
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(() => (
|
|
235
|
-
const [defaultsState, setDefaultsState] = useState(() => (
|
|
234
|
+
const [data, setDataState] = useState(() => cloneFormValue(defaults));
|
|
235
|
+
const [defaultsState, setDefaultsState] = useState(() => cloneFormValue(defaults));
|
|
236
236
|
const [errors, setErrors] = useState({});
|
|
237
|
-
const [error,
|
|
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((...
|
|
254
|
-
|
|
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] =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
313
|
+
else setActionError(result.error);
|
|
299
314
|
return previousSuccessVersion;
|
|
300
315
|
}
|
|
301
|
-
|
|
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
|
|
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, {
|
|
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 (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
58
|
+
"void": "1.3.0"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "vp pack",
|