@void/react 1.2.1 → 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 +10 -2
- package/dist/index.mjs +44 -45
- package/dist/plugin.mjs +60 -104
- package/dist/runtime/pages-client.d.mts +2 -1
- package/dist/runtime/pages-client.mjs +26 -10
- package/package.json +4 -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
|
|
@@ -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>>;
|
|
@@ -54,7 +60,9 @@ export declare function useForm<U extends ActionUrl & string>(url: U, defaults:
|
|
|
54
60
|
put(formData: FormData): void;
|
|
55
61
|
patch(formData: FormData): void;
|
|
56
62
|
delete(formData: FormData): void;
|
|
63
|
+
reset(options: FormResetOptions, ...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
|
|
57
64
|
reset(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
|
|
65
|
+
setError(field: keyof ResolveActionBody<U> & string, message: string): void;
|
|
58
66
|
clearErrors(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
|
|
59
67
|
clearError(): void;
|
|
60
68
|
};
|
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 }) {
|
|
@@ -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), {
|
|
@@ -294,11 +309,20 @@ function useForm(url, defaults, options) {
|
|
|
294
309
|
}
|
|
295
310
|
if (!result.ok) {
|
|
296
311
|
const validationErrors = getValidationErrors(result.error.body);
|
|
297
|
-
if (validationErrors)
|
|
298
|
-
|
|
312
|
+
if (validationErrors) {
|
|
313
|
+
setErrors(validationErrors);
|
|
314
|
+
await options?.onError?.(validationErrors);
|
|
315
|
+
} else {
|
|
316
|
+
setActionError(result.error);
|
|
317
|
+
await options?.onError?.(result.error);
|
|
318
|
+
}
|
|
299
319
|
return previousSuccessVersion;
|
|
300
320
|
}
|
|
301
|
-
|
|
321
|
+
if (options?.resetOnSuccess) {
|
|
322
|
+
dataRef.current = cloneFormValue(initialRef.current);
|
|
323
|
+
setDataState(dataRef.current);
|
|
324
|
+
}
|
|
325
|
+
const nextDefaults = cloneFormValue(dataRef.current);
|
|
302
326
|
defaultsRef.current = nextDefaults;
|
|
303
327
|
setDefaultsState(nextDefaults);
|
|
304
328
|
setWasSuccessful(true);
|
|
@@ -307,6 +331,7 @@ function useForm(url, defaults, options) {
|
|
|
307
331
|
setRecentlySuccessful(false);
|
|
308
332
|
successTimeoutRef.current = null;
|
|
309
333
|
}, 2e3);
|
|
334
|
+
await options?.onSuccess?.();
|
|
310
335
|
return previousSuccessVersion + 1;
|
|
311
336
|
}, 0);
|
|
312
337
|
useEffect(() => {
|
|
@@ -334,6 +359,7 @@ function useForm(url, defaults, options) {
|
|
|
334
359
|
patch: useMemo(() => dispatchMethodAction("PATCH"), [dispatchMethodAction]),
|
|
335
360
|
delete: useMemo(() => dispatchMethodAction("DELETE"), [dispatchMethodAction]),
|
|
336
361
|
reset,
|
|
362
|
+
setError,
|
|
337
363
|
clearErrors,
|
|
338
364
|
clearError
|
|
339
365
|
};
|
|
@@ -345,15 +371,6 @@ function useNavigation() {
|
|
|
345
371
|
}
|
|
346
372
|
//#endregion
|
|
347
373
|
//#region src/runtime/use-island-form.ts
|
|
348
|
-
async function readActionErrorBody(response) {
|
|
349
|
-
const contentType = response.headers.get("content-type") || "";
|
|
350
|
-
try {
|
|
351
|
-
if (contentType.includes("application/json")) return await response.json();
|
|
352
|
-
return await response.text() || null;
|
|
353
|
-
} catch {
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
374
|
/**
|
|
358
375
|
* Form hook for island pages. Uses fetch + page reload instead of the
|
|
359
376
|
* Void Router (which is not available in island mode).
|
|
@@ -404,16 +421,12 @@ function useIslandForm(defaults) {
|
|
|
404
421
|
setErrors({});
|
|
405
422
|
setError(null);
|
|
406
423
|
try {
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
body: JSON.stringify(data)
|
|
411
|
-
});
|
|
412
|
-
if (response.redirected) {
|
|
413
|
-
window.location.href = response.url;
|
|
424
|
+
const result = await submitIslandAction(url, method, data);
|
|
425
|
+
if (result.kind === "redirect") {
|
|
426
|
+
window.location.href = result.url;
|
|
414
427
|
return;
|
|
415
428
|
}
|
|
416
|
-
if (
|
|
429
|
+
if (result.kind === "success") {
|
|
417
430
|
setWasSuccessful(true);
|
|
418
431
|
setRecentlySuccessful(true);
|
|
419
432
|
const nextDefaults = { ...data };
|
|
@@ -421,22 +434,8 @@ function useIslandForm(defaults) {
|
|
|
421
434
|
setDefaultsState(nextDefaults);
|
|
422
435
|
successTimeoutRef.current = setTimeout(() => setRecentlySuccessful(false), 2e3);
|
|
423
436
|
window.location.reload();
|
|
424
|
-
} else
|
|
425
|
-
|
|
426
|
-
const actionError = new VoidActionError({
|
|
427
|
-
body,
|
|
428
|
-
status: response.status,
|
|
429
|
-
statusText: response.statusText,
|
|
430
|
-
url
|
|
431
|
-
});
|
|
432
|
-
const validationErrors = response.status === 422 ? getValidationErrors(body) : null;
|
|
433
|
-
if (validationErrors) {
|
|
434
|
-
setErrors(validationErrors);
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
if (categorizeActionError(response.status) === "boundary") throw actionError;
|
|
438
|
-
setError(actionError);
|
|
439
|
-
}
|
|
437
|
+
} else if (result.kind === "validation") setErrors(result.errors);
|
|
438
|
+
else setError(result.error);
|
|
440
439
|
} finally {
|
|
441
440
|
setPending(false);
|
|
442
441
|
}
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import react from "@vitejs/plugin-react";
|
|
2
|
-
import { generateComponentManifest, resolveEffectiveLayoutChain, resolvePagesDir,
|
|
3
|
-
import { existsSync, readFileSync
|
|
2
|
+
import { createPagesPlugin, generateComponentManifest, resolveEffectiveLayoutChain, resolvePagesDir, scanPages } from "void/pages";
|
|
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";
|
|
@@ -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"];
|
|
@@ -68,112 +67,38 @@ const REACT_REFRESH_PREAMBLE = `
|
|
|
68
67
|
window.$RefreshSig$ = () => (type) => type
|
|
69
68
|
window.__vite_plugin_react_preamble_installed__ = true
|
|
70
69
|
<\/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
70
|
function pagesPlugin(pagesDir, options) {
|
|
87
|
-
let pageScan = null;
|
|
88
|
-
let isDev = false;
|
|
89
|
-
let viteConfig;
|
|
90
71
|
const root = options?.root ?? dirname(pagesDir);
|
|
91
|
-
const normalizedPagesDir = normalizePath(pagesDir);
|
|
92
72
|
const pagesOptimizeEntries = `${relative(root, pagesDir)}/**/*.{js,jsx,ts,tsx}`;
|
|
93
|
-
|
|
94
|
-
const rescanPages = async () => {
|
|
95
|
-
pageScan = existsSync(pagesDir) ? await scanPages(root, { pagesDir }) : null;
|
|
96
|
-
};
|
|
97
|
-
const runRescan = () => {
|
|
98
|
-
pendingRescan ??= rescanPages().finally(() => {
|
|
99
|
-
pendingRescan = null;
|
|
100
|
-
});
|
|
101
|
-
return pendingRescan;
|
|
102
|
-
};
|
|
103
|
-
const invalidatePageModules = (server) => {
|
|
104
|
-
for (const env of Object.values(server.environments)) for (const id of ["\0" + RENDERER_ID, "\0" + CLIENT_ENTRY_ID]) {
|
|
105
|
-
const mod = env.moduleGraph.getModuleById(id);
|
|
106
|
-
if (mod) env.moduleGraph.invalidateModule(mod);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
return {
|
|
73
|
+
return createPagesPlugin(pagesDir, {
|
|
110
74
|
name: "void-react:pages",
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
resolveId: {
|
|
115
|
-
filter: { id: /^virtual:void-pages-(renderer|client)$/ },
|
|
116
|
-
handler(id) {
|
|
117
|
-
return "\0" + id;
|
|
118
|
-
}
|
|
75
|
+
root,
|
|
76
|
+
generateRenderer(scan, isDev) {
|
|
77
|
+
return generateRendererModule(scan, pagesDir, isDev);
|
|
119
78
|
},
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
async handler(id) {
|
|
123
|
-
if (isDev && pendingRescan) await pendingRescan;
|
|
124
|
-
if (!pageScan && existsSync(pagesDir)) await runRescan();
|
|
125
|
-
if (id === "\0" + RENDERER_ID) {
|
|
126
|
-
if (!pageScan) return "export function renderPage() { throw new Error('No pages found'); }";
|
|
127
|
-
return generateRendererModule(pageScan, pagesDir, isDev);
|
|
128
|
-
}
|
|
129
|
-
if (id === "\0" + CLIENT_ENTRY_ID) {
|
|
130
|
-
if (!pageScan) return "";
|
|
131
|
-
return generateClientEntry(pageScan, pagesDir, options?.viewTransitions, options?.prefetch, resolveStaticPageDataBuildOptions(root, { publicDir: viteConfig?.publicDir }));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
79
|
+
generateClient(scan, staticPageData) {
|
|
80
|
+
return generateClientEntry(scan, pagesDir, options?.viewTransitions, options?.prefetch, staticPageData);
|
|
134
81
|
},
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
82
|
+
async config({ isDev, scan }) {
|
|
83
|
+
if (isDev) return {
|
|
84
|
+
resolve: { dedupe: REACT_DEDUPE },
|
|
85
|
+
optimizeDeps: {
|
|
86
|
+
exclude: ["@void/react"],
|
|
87
|
+
entries: clientOptimizeEntries(await scan(), pagesDir, root),
|
|
88
|
+
include: [
|
|
89
|
+
...CLIENT_OPTIMIZE_DEPS,
|
|
90
|
+
"void/pages-client",
|
|
91
|
+
"void/pages-prefetch"
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
ssr: { optimizeDeps: { include: [...CLIENT_OPTIMIZE_DEPS, "react-dom/server"] } },
|
|
95
|
+
environments: { [WORKER_ENVIRONMENT_NAME]: { optimizeDeps: {
|
|
96
|
+
exclude: ["@void/react"],
|
|
97
|
+
entries: [pagesOptimizeEntries],
|
|
98
|
+
include: WORKER_OPTIMIZE_DEPS
|
|
99
|
+
} } }
|
|
150
100
|
};
|
|
151
|
-
|
|
152
|
-
server.watcher.on("change", (path) => handlePagesChange(path, "change"));
|
|
153
|
-
server.watcher.on("unlink", (path) => handlePagesChange(path, "unlink"));
|
|
154
|
-
},
|
|
155
|
-
configResolved(config) {
|
|
156
|
-
viteConfig = config;
|
|
157
|
-
},
|
|
158
|
-
config(_, env) {
|
|
159
|
-
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;
|
|
101
|
+
if (!(await scan())?.pages.some((page) => !page.island)) return;
|
|
177
102
|
return {
|
|
178
103
|
resolve: { dedupe: REACT_DEDUPE },
|
|
179
104
|
build: {
|
|
@@ -182,7 +107,35 @@ function pagesPlugin(pagesDir, options) {
|
|
|
182
107
|
}
|
|
183
108
|
};
|
|
184
109
|
}
|
|
185
|
-
};
|
|
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, "\\$&"));
|
|
186
139
|
}
|
|
187
140
|
function generateRendererModule(scan, pagesDir, isDev) {
|
|
188
141
|
const preambleScript = isDev ? REACT_REFRESH_PREAMBLE : "";
|
|
@@ -272,7 +225,10 @@ function islandsPlugin(pagesDir, options) {
|
|
|
272
225
|
},
|
|
273
226
|
async buildStart() {
|
|
274
227
|
if (existsSync(pagesDir)) {
|
|
275
|
-
pageScan = await scanPages(root, {
|
|
228
|
+
pageScan = await scanPages(root, {
|
|
229
|
+
pagesDir,
|
|
230
|
+
environment: isDev ? "dev" : "prod"
|
|
231
|
+
});
|
|
276
232
|
const filesToScan = [
|
|
277
233
|
...pageScan.pages.map((page) => ({
|
|
278
234
|
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;
|
|
@@ -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];
|
|
@@ -249,6 +250,7 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
|
|
|
249
250
|
prerenderedUrl = null;
|
|
250
251
|
React.startTransition(() => setHydrated(true));
|
|
251
252
|
}
|
|
253
|
+
return true;
|
|
252
254
|
},
|
|
253
255
|
dispose() {}
|
|
254
256
|
}), [engine, initialPageData]);
|
|
@@ -263,7 +265,7 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
|
|
|
263
265
|
const finishNavigation = useCallback((resource, result) => {
|
|
264
266
|
if (committedResources.has(resource.id)) return;
|
|
265
267
|
committedResources.add(resource.id);
|
|
266
|
-
resource.commit().then(() => {
|
|
268
|
+
resource.commit().then((committed) => {
|
|
267
269
|
const resolveViewTransition = viewTransitionWaiters.get(resource.id);
|
|
268
270
|
if (resolveViewTransition) {
|
|
269
271
|
viewTransitionWaiters.delete(resource.id);
|
|
@@ -272,8 +274,11 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
|
|
|
272
274
|
const visitSettler = pendingVisitSettlers.get(resource.id);
|
|
273
275
|
if (visitSettler) {
|
|
274
276
|
pendingVisitSettlers.delete(resource.id);
|
|
275
|
-
visitSettler.resolve(result.pageData);
|
|
277
|
+
if (committed) visitSettler.resolve(result.pageData);
|
|
278
|
+
else if (resource.method.toUpperCase() === "GET") visitSettler.resolve(void 0);
|
|
279
|
+
else visitSettler.reject(new DOMException("Navigation aborted", "AbortError"));
|
|
276
280
|
}
|
|
281
|
+
if (!committed) return;
|
|
277
282
|
if (activePendingNavigationId === resource.id) activePendingNavigationId = null;
|
|
278
283
|
dispatch({
|
|
279
284
|
type: "finish",
|
|
@@ -281,11 +286,22 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
|
|
|
281
286
|
location: currentUrl()
|
|
282
287
|
});
|
|
283
288
|
}).catch((error) => {
|
|
289
|
+
const resolveViewTransition = viewTransitionWaiters.get(resource.id);
|
|
290
|
+
if (resolveViewTransition) {
|
|
291
|
+
viewTransitionWaiters.delete(resource.id);
|
|
292
|
+
resolveViewTransition();
|
|
293
|
+
}
|
|
284
294
|
const visitSettler = pendingVisitSettlers.get(resource.id);
|
|
285
295
|
if (visitSettler) {
|
|
286
296
|
pendingVisitSettlers.delete(resource.id);
|
|
287
297
|
visitSettler.reject(error);
|
|
288
|
-
}
|
|
298
|
+
} else reportError(error);
|
|
299
|
+
if (activePendingNavigationId === resource.id) activePendingNavigationId = null;
|
|
300
|
+
dispatch({
|
|
301
|
+
type: "fail",
|
|
302
|
+
resource,
|
|
303
|
+
location: currentUrl()
|
|
304
|
+
});
|
|
289
305
|
});
|
|
290
306
|
}, []);
|
|
291
307
|
const schedule = useCallback((url, options = {}) => {
|
|
@@ -334,7 +350,7 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
|
|
|
334
350
|
pendingVisitSettlers.delete(prepared.id);
|
|
335
351
|
if (activePendingNavigationId === prepared.id) activePendingNavigationId = null;
|
|
336
352
|
dispatch({
|
|
337
|
-
type:
|
|
353
|
+
type: "abandon",
|
|
338
354
|
resource: prepared,
|
|
339
355
|
location: currentUrl()
|
|
340
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",
|