@void/react 1.3.0 → 1.4.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 +7 -1
- package/dist/index.mjs +15 -36
- package/dist/plugin.mjs +44 -78
- package/dist/runtime/pages-client.mjs +8 -7
- package/package.json +4 -3
package/dist/index.d.mts
CHANGED
|
@@ -41,7 +41,13 @@ export declare function useParams<T extends Record<string, string> = Record<stri
|
|
|
41
41
|
export declare function useShared<T = CloudContextVariables["shared"]>(): T;
|
|
42
42
|
//#endregion
|
|
43
43
|
//#region src/runtime/use-form.d.ts
|
|
44
|
-
|
|
44
|
+
type FormOptions = {
|
|
45
|
+
params?: Record<string, string>;
|
|
46
|
+
resetOnSuccess?: boolean;
|
|
47
|
+
onSuccess?: () => void | Promise<void>;
|
|
48
|
+
onError?: (error: Record<string, string> | VoidActionError) => void | Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
export declare function useForm<U extends ActionUrl & string>(url: U, defaults: ResolveActionBody<U>, options?: ActionFormOptions<U> & Pick<FormOptions, "onSuccess" | "onError">): {
|
|
45
51
|
data: ResolveActionBody<U>;
|
|
46
52
|
setData: <K extends keyof ResolveActionBody<U> & string>(field: K, value: ResolveActionBody<U>[K]) => void;
|
|
47
53
|
errors: Partial<Record<keyof ResolveActionBody<U> & string, string>>;
|
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 {
|
|
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
6
|
//#region src/runtime/form.tsx
|
|
7
7
|
function Form({ form, method = "post", renderError, children, ...props }) {
|
|
@@ -309,8 +309,13 @@ function useForm(url, defaults, options) {
|
|
|
309
309
|
}
|
|
310
310
|
if (!result.ok) {
|
|
311
311
|
const validationErrors = getValidationErrors(result.error.body);
|
|
312
|
-
if (validationErrors)
|
|
313
|
-
|
|
312
|
+
if (validationErrors) {
|
|
313
|
+
setErrors(validationErrors);
|
|
314
|
+
await options?.onError?.(validationErrors);
|
|
315
|
+
} else {
|
|
316
|
+
setActionError(result.error);
|
|
317
|
+
await options?.onError?.(result.error);
|
|
318
|
+
}
|
|
314
319
|
return previousSuccessVersion;
|
|
315
320
|
}
|
|
316
321
|
if (options?.resetOnSuccess) {
|
|
@@ -326,6 +331,7 @@ function useForm(url, defaults, options) {
|
|
|
326
331
|
setRecentlySuccessful(false);
|
|
327
332
|
successTimeoutRef.current = null;
|
|
328
333
|
}, 2e3);
|
|
334
|
+
await options?.onSuccess?.();
|
|
329
335
|
return previousSuccessVersion + 1;
|
|
330
336
|
}, 0);
|
|
331
337
|
useEffect(() => {
|
|
@@ -365,15 +371,6 @@ function useNavigation() {
|
|
|
365
371
|
}
|
|
366
372
|
//#endregion
|
|
367
373
|
//#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
|
-
}
|
|
377
374
|
/**
|
|
378
375
|
* Form hook for island pages. Uses fetch + page reload instead of the
|
|
379
376
|
* Void Router (which is not available in island mode).
|
|
@@ -424,16 +421,12 @@ function useIslandForm(defaults) {
|
|
|
424
421
|
setErrors({});
|
|
425
422
|
setError(null);
|
|
426
423
|
try {
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
body: JSON.stringify(data)
|
|
431
|
-
});
|
|
432
|
-
if (response.redirected) {
|
|
433
|
-
window.location.href = response.url;
|
|
424
|
+
const result = await submitIslandAction(url, method, data);
|
|
425
|
+
if (result.kind === "redirect") {
|
|
426
|
+
window.location.href = result.url;
|
|
434
427
|
return;
|
|
435
428
|
}
|
|
436
|
-
if (
|
|
429
|
+
if (result.kind === "success") {
|
|
437
430
|
setWasSuccessful(true);
|
|
438
431
|
setRecentlySuccessful(true);
|
|
439
432
|
const nextDefaults = { ...data };
|
|
@@ -441,22 +434,8 @@ function useIslandForm(defaults) {
|
|
|
441
434
|
setDefaultsState(nextDefaults);
|
|
442
435
|
successTimeoutRef.current = setTimeout(() => setRecentlySuccessful(false), 2e3);
|
|
443
436
|
window.location.reload();
|
|
444
|
-
} else
|
|
445
|
-
|
|
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
|
-
}
|
|
437
|
+
} else if (result.kind === "validation") setErrors(result.errors);
|
|
438
|
+
else setError(result.error);
|
|
460
439
|
} finally {
|
|
461
440
|
setPending(false);
|
|
462
441
|
}
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import react from "@vitejs/plugin-react";
|
|
2
|
-
import { generateComponentManifest, resolveEffectiveLayoutChain, resolvePagesDir,
|
|
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";
|
|
@@ -44,7 +44,6 @@ 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
45
|
//#endregion
|
|
46
46
|
//#region src/plugin-pages.ts
|
|
47
|
-
const RENDERER_ID = "virtual:void-pages-renderer";
|
|
48
47
|
const CLIENT_ENTRY_ID = "virtual:void-pages-client";
|
|
49
48
|
const WORKER_ENVIRONMENT_NAME = "void_worker";
|
|
50
49
|
const REACT_DEDUPE = ["react", "react-dom"];
|
|
@@ -69,88 +68,28 @@ const REACT_REFRESH_PREAMBLE = `
|
|
|
69
68
|
window.__vite_plugin_react_preamble_installed__ = true
|
|
70
69
|
<\/script>`;
|
|
71
70
|
function pagesPlugin(pagesDir, options) {
|
|
72
|
-
let pageScan = null;
|
|
73
|
-
let isDev = false;
|
|
74
|
-
let viteConfig;
|
|
75
71
|
const root = options?.root ?? dirname(pagesDir);
|
|
76
|
-
const normalizedPagesDir = normalizePath(pagesDir);
|
|
77
72
|
const pagesOptimizeEntries = `${relative(root, pagesDir)}/**/*.{js,jsx,ts,tsx}`;
|
|
78
|
-
|
|
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 {
|
|
73
|
+
return createPagesPlugin(pagesDir, {
|
|
98
74
|
name: "void-react:pages",
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
}
|
|
75
|
+
root,
|
|
76
|
+
generateRenderer(scan, isDev) {
|
|
77
|
+
return generateRendererModule(scan, pagesDir, isDev);
|
|
122
78
|
},
|
|
123
|
-
|
|
124
|
-
|
|
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;
|
|
79
|
+
generateClient(scan, staticPageData) {
|
|
80
|
+
return generateClientEntry(scan, pagesDir, options?.viewTransitions, options?.prefetch, staticPageData);
|
|
145
81
|
},
|
|
146
|
-
async config(
|
|
147
|
-
isDev = env.command === "serve";
|
|
148
|
-
if (!existsSync(pagesDir)) return;
|
|
82
|
+
async config({ isDev, scan }) {
|
|
149
83
|
if (isDev) return {
|
|
150
84
|
resolve: { dedupe: REACT_DEDUPE },
|
|
151
85
|
optimizeDeps: {
|
|
152
86
|
exclude: ["@void/react"],
|
|
153
|
-
|
|
87
|
+
entries: clientOptimizeEntries(await scan(), pagesDir, root),
|
|
88
|
+
include: [
|
|
89
|
+
...CLIENT_OPTIMIZE_DEPS,
|
|
90
|
+
"void/pages-client",
|
|
91
|
+
"void/pages-prefetch"
|
|
92
|
+
]
|
|
154
93
|
},
|
|
155
94
|
ssr: { optimizeDeps: { include: [...CLIENT_OPTIMIZE_DEPS, "react-dom/server"] } },
|
|
156
95
|
environments: { [WORKER_ENVIRONMENT_NAME]: { optimizeDeps: {
|
|
@@ -159,8 +98,7 @@ function pagesPlugin(pagesDir, options) {
|
|
|
159
98
|
include: WORKER_OPTIMIZE_DEPS
|
|
160
99
|
} } }
|
|
161
100
|
};
|
|
162
|
-
await
|
|
163
|
-
if (!pageScan?.pages.some((page) => !page.island)) return;
|
|
101
|
+
if (!(await scan())?.pages.some((page) => !page.island)) return;
|
|
164
102
|
return {
|
|
165
103
|
resolve: { dedupe: REACT_DEDUPE },
|
|
166
104
|
build: {
|
|
@@ -169,7 +107,35 @@ function pagesPlugin(pagesDir, options) {
|
|
|
169
107
|
}
|
|
170
108
|
};
|
|
171
109
|
}
|
|
172
|
-
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function clientOptimizeEntries(scan, pagesDir, root) {
|
|
113
|
+
if (!scan) return [];
|
|
114
|
+
const browserFiles = /* @__PURE__ */ new Set();
|
|
115
|
+
const islandFiles = /* @__PURE__ */ new Set();
|
|
116
|
+
for (const page of scan.pages) {
|
|
117
|
+
const layouts = resolveEffectiveLayoutChain(page, scan.layouts, scan.namedLayouts);
|
|
118
|
+
if (page.island) {
|
|
119
|
+
islandFiles.add(resolve(pagesDir, page.componentPath));
|
|
120
|
+
for (const layout of layouts) if (layout.island) islandFiles.add(resolve(pagesDir, layout.filePath));
|
|
121
|
+
} else {
|
|
122
|
+
browserFiles.add(resolve(pagesDir, page.componentPath));
|
|
123
|
+
for (const layout of layouts) browserFiles.add(resolve(pagesDir, layout.filePath));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const file of islandFiles) {
|
|
127
|
+
if (!/\.island\.(tsx|jsx)$/.test(file)) continue;
|
|
128
|
+
for (const { specifier } of findIslandImports(readFileSync(file, "utf8"), file)) {
|
|
129
|
+
const component = resolveSpecifier(specifier, dirname(file), [
|
|
130
|
+
".tsx",
|
|
131
|
+
".jsx",
|
|
132
|
+
".ts",
|
|
133
|
+
".js"
|
|
134
|
+
]);
|
|
135
|
+
if (component) browserFiles.add(component);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return [...browserFiles].map((file) => relative(root, file).replace(/[()[\]{}*?!|]/g, "\\$&"));
|
|
173
139
|
}
|
|
174
140
|
function generateRendererModule(scan, pagesDir, isDev) {
|
|
175
141
|
const preambleScript = isDev ? REACT_REFRESH_PREAMBLE : "";
|
|
@@ -182,12 +182,11 @@ function RouteResource({ resource, onCommit }) {
|
|
|
182
182
|
resource,
|
|
183
183
|
result
|
|
184
184
|
]);
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const routeParams = pageData.params ?? EMPTY_PARAMS;
|
|
185
|
+
const pageData = result?.pageData;
|
|
186
|
+
const pageDataUrl = pageData?.url ?? "";
|
|
187
|
+
const routeParams = pageData?.params ?? EMPTY_PARAMS;
|
|
189
188
|
const routeRouter = useMemo(() => {
|
|
190
|
-
const pageUrl = encodedPageUrl(
|
|
189
|
+
const pageUrl = encodedPageUrl(pageDataUrl);
|
|
191
190
|
return createRouterFacade(outerRouter, () => {
|
|
192
191
|
const outerUrl = outerRouter.url;
|
|
193
192
|
const outerPath = outerUrl.pathname + outerUrl.search;
|
|
@@ -198,11 +197,13 @@ function RouteResource({ resource, onCommit }) {
|
|
|
198
197
|
}, () => routeParams);
|
|
199
198
|
}, [
|
|
200
199
|
outerRouter,
|
|
201
|
-
|
|
200
|
+
pageDataUrl,
|
|
202
201
|
resource.fetchUrl,
|
|
203
202
|
resource.hash,
|
|
204
203
|
routeParams
|
|
205
204
|
]);
|
|
205
|
+
if (!result || !pageData) return null;
|
|
206
|
+
const PageComponent = result.component;
|
|
206
207
|
let element = React.createElement(PageComponent, pageData.props);
|
|
207
208
|
for (let i = result.layouts.length - 1; i >= 0; i--) {
|
|
208
209
|
const Layout = result.layouts[i];
|
|
@@ -349,7 +350,7 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
|
|
|
349
350
|
pendingVisitSettlers.delete(prepared.id);
|
|
350
351
|
if (activePendingNavigationId === prepared.id) activePendingNavigationId = null;
|
|
351
352
|
dispatch({
|
|
352
|
-
type:
|
|
353
|
+
type: "abandon",
|
|
353
354
|
resource: prepared,
|
|
354
355
|
location: currentUrl()
|
|
355
356
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@void/react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "React pages, server rendering, forms, and navigation for Void.",
|
|
5
5
|
"homepage": "https://void.cloud",
|
|
6
6
|
"bugs": {
|
|
@@ -44,18 +44,19 @@
|
|
|
44
44
|
"@types/node": "^26.6.5",
|
|
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
51
|
"vite": "npm:@voidzero-dev/vite-plus-core@1.3.0",
|
|
51
52
|
"vite-plus": "1.3.0",
|
|
52
|
-
"void": "1.
|
|
53
|
+
"void": "1.4.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.
|
|
59
|
+
"void": "1.4.0"
|
|
59
60
|
},
|
|
60
61
|
"scripts": {
|
|
61
62
|
"build": "vp pack",
|